-
Notifications
You must be signed in to change notification settings - Fork 0
288 lines (271 loc) · 12.5 KB
/
Copy pathsplit.yml
File metadata and controls
288 lines (271 loc) · 12.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
name: Split monorepo
# Manual trigger only for now. Once the framework is production-ready and
# the deferred test coverage + PHPStan baseline cleanup land, change the
# trigger to:
# on:
# push:
# branches: [master]
# tags: ['v*']
# and the workflow will run automatically on every push and tag.
#
# See issue #14 for the full plan.
on:
workflow_dispatch:
inputs:
package:
description: 'Single split to publish (leave blank to publish all 42: 40 packages + docs + univeros starter)'
required: false
type: choice
default: ''
options:
- ''
- agent-spec
- bootstrap
- cache
- cli
- common
- configuration
- container
- cookie
- courier
- data
- doctor
- docs
- eval
- events
- examples
- filesystem
- happen
- http
- idempotency
- index
- introspection
- logging
- mcp
- messaging
- middleware
- migration-intelligence
- module
- observability
- observatory
- persistence
- profiling
- sanitation
- scaffold
- security
- session
- structure
- suggest
- test-reporter
- tinker
- univeros
- validation
- webhooks
dry_run:
description: 'Compute splits but skip pushing to sub-repos'
required: false
type: boolean
default: false
env:
SPLITSH_VERSION: v1.0.1
# Opt into Node 24 ahead of the June 2026 default flip; silences the
# "Node.js 20 actions are deprecated" warning emitted by actions/checkout
# and other JS-based actions on the current runner image.
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true'
jobs:
# GitHub Actions doesn't allow matrix.* in job-level if: conditions —
# the matrix is what defines the jobs. To support a "single package only"
# input, we compute the matrix dynamically in this setup job and the
# split job consumes it via fromJSON.
setup-matrix:
name: Compute matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.compute.outputs.matrix }}
steps:
- name: Checkout (for drift check)
uses: actions/checkout@v4
with:
fetch-depth: 1
persist-credentials: false
- name: Build matrix JSON
id: compute
env:
PACKAGE: ${{ github.event.inputs.package }}
run: |
set -euo pipefail
# The matrix has two kinds of entries:
# 1. Per-package splits under src/Altair/* (the bulk of the work).
# The drift guard below enforces that EVERY src/Altair/*/composer.json
# is represented here — the gap that left 19 packages unpublished
# for months can't recur.
# 2. Non-package splits (docs, univeros starter). These are managed
# manually and intentionally NOT in src/Altair/.
#
# Keep alphabetically sorted by name so the workflow_dispatch dropdown
# stays in lockstep.
full='[
{"name":"agent-spec", "path":"src/Altair/AgentSpec"},
{"name":"bootstrap", "path":"src/Altair/Bootstrap"},
{"name":"cache", "path":"src/Altair/Cache"},
{"name":"cli", "path":"src/Altair/Cli"},
{"name":"common", "path":"src/Altair/Common"},
{"name":"configuration", "path":"src/Altair/Configuration"},
{"name":"container", "path":"src/Altair/Container"},
{"name":"cookie", "path":"src/Altair/Cookie"},
{"name":"courier", "path":"src/Altair/Courier"},
{"name":"data", "path":"src/Altair/Data"},
{"name":"doctor", "path":"src/Altair/Doctor"},
{"name":"docs", "path":"docs"},
{"name":"eval", "path":"src/Altair/Eval"},
{"name":"events", "path":"src/Altair/Events"},
{"name":"examples", "path":"src/Altair/Examples"},
{"name":"filesystem", "path":"src/Altair/Filesystem"},
{"name":"happen", "path":"src/Altair/Happen"},
{"name":"http", "path":"src/Altair/Http"},
{"name":"idempotency", "path":"src/Altair/Idempotency"},
{"name":"index", "path":"src/Altair/Index"},
{"name":"introspection", "path":"src/Altair/Introspection"},
{"name":"logging", "path":"src/Altair/Logging"},
{"name":"mcp", "path":"src/Altair/Mcp"},
{"name":"messaging", "path":"src/Altair/Messaging"},
{"name":"middleware", "path":"src/Altair/Middleware"},
{"name":"migration-intelligence", "path":"src/Altair/MigrationIntelligence"},
{"name":"module", "path":"src/Altair/Module"},
{"name":"observability", "path":"src/Altair/Observability"},
{"name":"observatory", "path":"src/Altair/Observatory"},
{"name":"persistence", "path":"src/Altair/Persistence"},
{"name":"profiling", "path":"src/Altair/Profiling"},
{"name":"sanitation", "path":"src/Altair/Sanitation"},
{"name":"scaffold", "path":"src/Altair/Scaffold"},
{"name":"security", "path":"src/Altair/Security"},
{"name":"session", "path":"src/Altair/Session"},
{"name":"structure", "path":"src/Altair/Structure"},
{"name":"suggest", "path":"src/Altair/Suggest"},
{"name":"test-reporter", "path":"src/Altair/TestReporter"},
{"name":"tinker", "path":"src/Altair/Tinker"},
{"name":"univeros", "path":"src/Altair/Bootstrap/resources/skeleton"},
{"name":"validation", "path":"src/Altair/Validation"},
{"name":"webhooks", "path":"src/Altair/Webhooks"}
]'
# Drift guard: every src/Altair/* directory that ships a composer.json
# is a publishable sub-package, so it MUST appear in the matrix above.
# Non-package matrix entries (docs, univeros) are filtered out before
# comparing — they're allowed to exist without a filesystem twin.
expected=$(find src/Altair -mindepth 2 -maxdepth 2 -name composer.json -print0 \
| xargs -0 -n1 dirname \
| sort)
actual=$(jq -r '.[].path | select(startswith("src/Altair/") and (split("/") | length) == 3)' <<< "$full" | sort)
if ! diff -u <(echo "$expected") <(echo "$actual") >/tmp/matrix-drift.diff; then
echo "::error::split.yml matrix is out of sync with src/Altair/*. Diff:"
cat /tmp/matrix-drift.diff
echo "::error::Add or remove entries in the 'full' JSON array above to match the filesystem."
exit 1
fi
if [[ -z "${PACKAGE}" ]]; then
matrix=$(jq -c '.' <<< "$full")
else
matrix=$(jq -c --arg p "$PACKAGE" '[.[] | select(.name == $p)]' <<< "$full")
if [[ "$(jq 'length' <<< "$matrix")" -eq 0 ]]; then
echo "::error::Unknown package '${PACKAGE}'"
exit 1
fi
fi
echo "matrix=$matrix" >> "$GITHUB_OUTPUT"
echo "Selected packages:"
jq '.[].name' <<< "$matrix"
split:
name: Split ${{ matrix.package.name }}
needs: setup-matrix
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
package: ${{ fromJSON(needs.setup-matrix.outputs.matrix) }}
steps:
- name: Checkout (full history)
uses: actions/checkout@v4
with:
fetch-depth: 0
# Don't write the default GITHUB_TOKEN into git's extraheader config —
# that bot token only has write access to univeros/framework, and its
# Authorization header would shadow the SPLIT_TOKEN we embed in the
# push URL, causing pushes to sub-repos to be rejected with 403.
persist-credentials: false
- name: Install splitsh/lite
run: |
set -euo pipefail
mkdir -p /tmp/splitsh && cd /tmp/splitsh
curl -sSL -o splitsh.tar.gz \
"https://github.com/splitsh/lite/releases/download/${SPLITSH_VERSION}/lite_linux_amd64.tar.gz"
tar xzf splitsh.tar.gz
sudo mv splitsh-lite /usr/local/bin/
splitsh-lite --version
- name: Compute split for ${{ matrix.package.path }}
id: split
run: |
set -euo pipefail
sha=$(splitsh-lite --prefix="${{ matrix.package.path }}")
if [[ -z "$sha" ]]; then
echo "::error::splitsh-lite produced an empty SHA for ${{ matrix.package.path }}"
exit 1
fi
echo "Split SHA for ${{ matrix.package.name }}: $sha"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
- name: Push split to univeros/${{ matrix.package.name }}
if: github.event.inputs.dry_run != 'true'
env:
SPLIT_TOKEN: ${{ secrets.SPLIT_TOKEN }}
run: |
set -euo pipefail
if [[ -z "${SPLIT_TOKEN:-}" ]]; then
echo "::error::SPLIT_TOKEN secret is not configured on this repository"
exit 1
fi
remote="https://x-access-token:${SPLIT_TOKEN}@github.com/univeros/${{ matrix.package.name }}.git"
sha="${{ steps.split.outputs.sha }}"
# Belt-and-suspenders: explicitly clear any Authorization extraheader
# for github.com on this single command so the URL-embedded PAT is the
# only credential git presents. This survives even if some upstream
# step has persisted the runner's GITHUB_TOKEN into git config.
git_push() {
git -c http.https://github.com/.extraheader= push "$@"
}
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
# Tag push: propagate the tag at the split SHA
echo "Pushing tag $GITHUB_REF to univeros/${{ matrix.package.name }}"
git_push "$remote" "${sha}:${GITHUB_REF}"
else
# Branch push: force-update master on the sub-repo
echo "Pushing $sha to univeros/${{ matrix.package.name }} master"
git_push "$remote" "${sha}:refs/heads/master" --force
fi
# Auto-update Packagist. The univeros/* repos are read-only mirrors that
# only change when this workflow pushes to them, so pinging Packagist's
# update-package endpoint here is the canonical auto-update path — no
# per-repo webhooks to maintain. 'docs' is not a Composer package, so it
# is skipped. Best-effort: a failed ping warns but never fails the split.
- name: Notify Packagist
if: github.event.inputs.dry_run != 'true' && matrix.package.name != 'docs'
env:
PACKAGIST_AUTH: ${{ secrets.PACKAGIST_AUTH }}
run: |
set -uo pipefail
if [[ -z "${PACKAGIST_AUTH:-}" ]]; then
echo "::warning::PACKAGIST_AUTH secret not set — skipping Packagist auto-update for ${{ matrix.package.name }}"
exit 0
fi
# PACKAGIST_AUTH is "username:apiToken". update-package is a SAFE
# operation, so a safe token works here (the main token also works).
# Pass the bearer credential via a curl --config fd so it never lands
# in the process argv or the run log.
code=$(curl -s -o /tmp/packagist-resp.json -w '%{http_code}' \
--config <(printf 'header = "Authorization: Bearer %s"\nheader = "Content-Type: application/json"\n' "$PACKAGIST_AUTH") \
-X POST 'https://packagist.org/api/update-package' \
-d "{\"repository\":\"https://github.com/univeros/${{ matrix.package.name }}\"}")
body=$(tr -d '\n' < /tmp/packagist-resp.json | cut -c1-200)
if [[ "$code" == "200" || "$code" == "202" ]]; then
echo "Packagist update OK for univeros/${{ matrix.package.name }} (HTTP $code): $body"
else
echo "::warning::Packagist update for univeros/${{ matrix.package.name }} returned HTTP $code: $body"
fi