Skip to content

Commit 1bdbf83

Browse files
committed
Consolidate Composer registry into single repo
- Combined publish + Pages deploy into one workflow using built-in GITHUB_TOKEN - Migrate packages.json, index.html, and merge-packages-json.sh into this repo - Remove cross-repo resolve-token action and obsolete mono-package-split workflow - Update README with Composer Package Registry section
1 parent 08b9972 commit 1bdbf83

7 files changed

Lines changed: 966 additions & 232 deletions

File tree

.github/actions/resolve-token/action.yml

Lines changed: 0 additions & 84 deletions
This file was deleted.
Lines changed: 153 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,24 @@
1-
name: 'Publish Composer Packages (TEST)'
1+
name: 'Publish Composer Packages + Deploy Pages'
22

3-
# TEST harness for the Satis-replacement pipeline. Runs on
4-
# ASUWebPlatforms/webspark-composer-test (in place of webspark-mirror) and
5-
# publishes to ASUWebPlatforms/composer-packages-test (in place of
6-
# composer-packages). Auth uses a service-user PAT (secrets.SERVICE_USER_PAT).
3+
# Single-repo Composer registry pipeline. Everything happens here in
4+
# ASUWebPlatforms/webspark-composer-test:
75
#
8-
# To promote to production: change TARGET_REPO + the repo gate back to the
9-
# live names and switch the resolve-token action to the GitHub App path.
6+
# ensure-release -> create (once) the GitHub Release that holds this ref's
7+
# zip assets, on THIS repo.
8+
# publish (matrix)-> zip each package subdirectory and upload it as a Release
9+
# asset, then verify it downloads + checksums.
10+
# rebuild -> merge the new version records into the committed
11+
# packages.json accumulator, commit it back to main, and
12+
# deploy packages.json + index.html to GitHub Pages.
13+
#
14+
# This replaces the former two-repo design (publisher + separate registry repo
15+
# wired together by a cross-repo token and repository_dispatch). Because the
16+
# registry now lives in the same repo, the built-in GITHUB_TOKEN can do
17+
# everything -- no PAT, no GitHub App, no resolve-token action.
18+
#
19+
# Note on loops: pushes made with GITHUB_TOKEN do NOT trigger new workflow
20+
# runs, so the bot's packages.json commit cannot re-trigger this workflow. The
21+
# "[skip ci]" marker on that commit is kept as belt-and-suspenders.
1022

1123
on:
1224
push:
@@ -18,15 +30,20 @@ on:
1830
- '[0-9]+.[0-9]+.[0-9]+'
1931
workflow_dispatch:
2032

21-
# Don't let two publishes for the same ref race each other.
33+
# Serialize the whole pipeline so concurrent pushes never race on the release,
34+
# the packages.json commit, or the Pages deploy. Queue them (do NOT cancel) --
35+
# cancelling mid-push/mid-deploy can drop an update or wedge the Pages backend.
2236
concurrency:
23-
group: composer-packages-publish-${{ github.ref }}
37+
group: composer-packages-publish
2438
cancel-in-progress: false
2539

2640
env:
27-
# Single place to flip between test and production targets.
28-
PUBLISHER_REPO: ASUWebPlatforms/webspark-composer-test
29-
TARGET_REPO: ASUWebPlatforms/composer-packages-test
41+
# Single place to flip between test and production. For production this
42+
# becomes the live repo (e.g. ASUWebPlatforms/webspark-mirror) and its Pages
43+
# URL; nothing else in the workflow hard-codes the repo.
44+
REPO: ASUWebPlatforms/webspark-composer-test
45+
BASE_URL: https://github.com/ASUWebPlatforms/webspark-composer-test/releases/download
46+
PAGES_URL: https://asuwebplatforms.github.io/webspark-composer-test
3047

3148
jobs:
3249
# ---------------------------------------------------------------------------
@@ -35,17 +52,14 @@ jobs:
3552
ensure-release:
3653
if: github.repository == 'ASUWebPlatforms/webspark-composer-test'
3754
runs-on: ubuntu-latest
55+
permissions:
56+
contents: write # create the release on this repo
3857
outputs:
3958
tag: ${{ steps.compute.outputs.tag }}
4059
version: ${{ steps.compute.outputs.version }}
4160
steps:
4261
- uses: actions/checkout@v4
4362

44-
- uses: ./.github/actions/resolve-token
45-
id: token
46-
with:
47-
service-user-pat: ${{ secrets.SERVICE_USER_PAT }}
48-
4963
- id: compute
5064
shell: bash
5165
run: |
@@ -60,14 +74,14 @@ jobs:
6074
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
6175
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
6276
63-
- name: Ensure release exists on target repo
77+
- name: Ensure release exists
6478
env:
65-
GH_TOKEN: ${{ steps.token.outputs.token }}
79+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
6680
TAG: ${{ steps.compute.outputs.tag }}
6781
run: |
68-
if ! gh release view "$TAG" --repo "$TARGET_REPO" >/dev/null 2>&1; then
82+
if ! gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
6983
gh release create "$TAG" \
70-
--repo "$TARGET_REPO" \
84+
--repo "$REPO" \
7185
--title "$TAG" \
7286
--notes "Composer package artifacts for $TAG" \
7387
${{ steps.compute.outputs.version == 'dev-main' && '--prerelease' || '' }}
@@ -81,6 +95,8 @@ jobs:
8195
publish:
8296
needs: ensure-release
8397
runs-on: ubuntu-latest
98+
permissions:
99+
contents: write # upload assets to the release
84100
strategy:
85101
fail-fast: false
86102
matrix:
@@ -94,11 +110,6 @@ jobs:
94110
steps:
95111
- uses: actions/checkout@v4
96112

97-
- uses: ./.github/actions/resolve-token
98-
id: token
99-
with:
100-
service-user-pat: ${{ secrets.SERVICE_USER_PAT }}
101-
102113
- name: Package subdirectory
103114
env:
104115
TAG: ${{ needs.ensure-release.outputs.tag }}
@@ -108,7 +119,7 @@ jobs:
108119
109120
- name: Upload asset (retry up to 2x, then fail)
110121
env:
111-
GH_TOKEN: ${{ steps.token.outputs.token }}
122+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112123
TAG: ${{ needs.ensure-release.outputs.tag }}
113124
run: |
114125
set -euo pipefail
@@ -117,7 +128,7 @@ jobs:
117128
max=3 # 1 initial + 2 retries
118129
until [ "$attempt" -gt "$max" ]; do
119130
echo "Upload attempt $attempt/$max for $ZIP"
120-
if gh release upload "$TAG" "$ZIP" --repo "$TARGET_REPO" --clobber; then
131+
if gh release upload "$TAG" "$ZIP" --repo "$REPO" --clobber; then
121132
echo "Upload succeeded."
122133
break
123134
fi
@@ -135,7 +146,7 @@ jobs:
135146
run: |
136147
set -euo pipefail
137148
META="$(ls dist/*.json)"
138-
URL="https://github.com/${TARGET_REPO}/releases/download/${TAG}/$(jq -r '.filename' "$META")"
149+
URL="https://github.com/${REPO}/releases/download/${TAG}/$(jq -r '.filename' "$META")"
139150
EXPECTED="$(jq -r '.shasum' "$META")"
140151
# -L follows the 302 to the signed objects URL.
141152
curl -fsSL "$URL" -o downloaded.zip
@@ -146,26 +157,49 @@ jobs:
146157
fi
147158
echo "Verified $URL"
148159
149-
- name: Stash metadata record for dispatch
160+
- name: Stash metadata record for merge
150161
uses: actions/upload-artifact@v4
151162
with:
152163
name: meta-${{ strategy.job-index }}
153164
path: dist/*.json
154165
retention-days: 1
155166

156167
# ---------------------------------------------------------------------------
157-
# 3) After ALL uploads succeed, fire ONE repository_dispatch with metadata.
168+
# 3) Merge records into packages.json, commit, and deploy to Pages.
158169
# ---------------------------------------------------------------------------
159-
dispatch-rebuild:
170+
rebuild:
160171
needs: publish
161172
runs-on: ubuntu-latest
173+
# Fail cleanly if a Pages deploy ever wedges, instead of hanging until a
174+
# human cancels it. A healthy run finishes well under a minute.
175+
timeout-minutes: 15
176+
permissions:
177+
contents: write # commit the updated packages.json back to this repo
178+
pages: write # deploy to Pages
179+
id-token: write # required by actions/deploy-pages (OIDC)
162180
steps:
163181
- uses: actions/checkout@v4
164182

165-
- uses: ./.github/actions/resolve-token
166-
id: token
167-
with:
168-
service-user-pat: ${{ secrets.SERVICE_USER_PAT }}
183+
- name: Guard - packages.json is valid and well-shaped
184+
run: |
185+
set -euo pipefail
186+
# Fail fast (and clearly) if the committed accumulator is missing,
187+
# not valid JSON, or not the expected { "packages": { ... } } shape.
188+
if [ ! -f packages.json ]; then
189+
echo "::error file=packages.json::packages.json is missing from the repo root." >&2
190+
exit 1
191+
fi
192+
if ! jq empty packages.json 2>/dev/null; then
193+
echo "::error file=packages.json::packages.json is not valid JSON." >&2
194+
head -n 20 packages.json >&2 || true
195+
exit 1
196+
fi
197+
if [ "$(jq -r 'type' packages.json)" != "object" ] \
198+
|| [ "$(jq -r '.packages | type' packages.json)" != "object" ]; then
199+
echo "::error file=packages.json::packages.json must be an object with a top-level \"packages\" object." >&2
200+
exit 1
201+
fi
202+
echo "packages.json OK ($(jq '.packages | length' packages.json) package(s))."
169203
170204
- name: Collect all metadata records
171205
uses: actions/download-artifact@v4
@@ -174,27 +208,92 @@ jobs:
174208
path: meta
175209
merge-multiple: true
176210

177-
- name: Build dispatch payload
178-
id: payload
211+
- name: Build records.json
179212
run: |
180213
set -euo pipefail
181-
# Combine every per-package record into a single JSON array.
182-
jq -s '.' meta/*.json > records.json
183-
echo "Records to dispatch:"
214+
# Combine every per-package record into a single JSON array. If there
215+
# are somehow no records (shouldn't happen after a successful publish
216+
# matrix), fall back to an empty array so we just redeploy as-is.
217+
if ls meta/*.json >/dev/null 2>&1; then
218+
jq -s '.' meta/*.json > records.json
219+
else
220+
echo "No metadata records found; redeploying existing packages.json."
221+
echo '[]' > records.json
222+
fi
184223
jq . records.json
185-
# Build the FULL request body with a properly nested client_payload.
186-
# gh api -f/-F send flat form fields and will NOT construct nested
187-
# objects (client_payload[records] would not become real nested JSON),
188-
# so we assemble the body explicitly and send it via --input.
189-
# client_payload has a documented ~64KB cap; 6 small records fit.
190-
jq -n --argjson records "$(cat records.json)" \
191-
'{event_type: "packages-publish", client_payload: {records: $records}}' \
192-
> dispatch-body.json
193-
jq . dispatch-body.json
194-
195-
- name: Dispatch rebuild on target repo
224+
225+
- name: Merge + commit + push (race-safe)
196226
env:
197-
GH_TOKEN: ${{ steps.token.outputs.token }}
227+
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
228+
run: |
229+
set -euo pipefail
230+
chmod +x scripts/merge-packages-json.sh
231+
git config user.name "ws2-release-bot"
232+
git config user.email "ws2-release-bot@asu.edu"
233+
234+
attempt=1
235+
max=5
236+
until [ "$attempt" -gt "$max" ]; do
237+
# Always re-apply the merge onto the freshest main. The merge is
238+
# additive/idempotent, so re-applying after a remote update is
239+
# always correct.
240+
git fetch origin main
241+
git reset --hard origin/main
242+
243+
./scripts/merge-packages-json.sh packages.json records.json "$BASE_URL"
244+
245+
if git diff --quiet -- packages.json; then
246+
echo "packages.json already up to date on origin/main; nothing to push."
247+
break
248+
fi
249+
250+
git add packages.json
251+
git commit -m "Update packages.json [skip ci]"
252+
253+
if git push origin HEAD:main; then
254+
echo "Pushed on attempt $attempt."
255+
break
256+
fi
257+
258+
if [ "$attempt" -eq "$max" ]; then
259+
echo "::error::failed to push packages.json after $max attempts (persistent contention)" >&2
260+
exit 1
261+
fi
262+
echo "Push rejected (remote moved); retrying ($attempt/$max)..."
263+
attempt=$((attempt+1))
264+
sleep $((attempt * 3))
265+
done
266+
267+
- name: Prepare Pages artifact
198268
run: |
199269
set -euo pipefail
200-
gh api --method POST "repos/${TARGET_REPO}/dispatches" --input dispatch-body.json
270+
mkdir -p _site
271+
cp packages.json _site/packages.json
272+
# Optional human index, if present in the repo root.
273+
[ -f index.html ] && cp index.html _site/index.html || true
274+
275+
- uses: actions/configure-pages@v5
276+
- uses: actions/upload-pages-artifact@v3
277+
with:
278+
path: _site
279+
- id: deploy
280+
uses: actions/deploy-pages@v4
281+
282+
- name: Verify deployed packages.json
283+
run: |
284+
set -euo pipefail
285+
# Give Pages a moment to propagate the new deploy.
286+
sleep 10
287+
curl -fsSL "${PAGES_URL}/packages.json" -o served.json
288+
jq empty served.json
289+
# If this run carried records, confirm each name->version is present.
290+
if [ "$(jq 'length' records.json)" -gt 0 ]; then
291+
while read -r name version; do
292+
if [ "$(jq --arg n "$name" --arg v "$version" \
293+
'.packages[$n][$v] != null' served.json)" != "true" ]; then
294+
echo "::error::served packages.json missing ${name}@${version}" >&2
295+
exit 1
296+
fi
297+
done < <(jq -r '.[] | "\(.name) \(.version)"' records.json)
298+
fi
299+
echo "Verified ${PAGES_URL}/packages.json"

0 commit comments

Comments
 (0)