Skip to content

Commit c755a07

Browse files
committed
release: publish manifest + shockwave fan-out on tag
Two new jobs run after the existing release job: publish-manifest builds public/.well-known/latest.json from the tag's checksums.txt, then repository_dispatches it to pilot-protocol/website, which commits the JSON to main and triggers the Cloudflare deploy. The single canonical manifest at pilotprotocol.network/.well-known/latest.json is consumed by install.sh, the Homebrew formula bump workflow, and the SDK release helpers — one shockwave per release. shockwave fans out repository_dispatch(event_type=upstream-release) to homebrew-pilot, sdk-node, sdk-python, and sdk-swift so each consumer can run its own bump workflow. Per-target dispatch is soft-fail with a summary so a missing token on one repo does not block the others. Both jobs require a new SHOCKWAVE_DISPATCH_TOKEN secret with repository_dispatch scope on each downstream repo (prefer a GitHub App token over a PAT). When the secret is absent the steps emit a clear ::warning:: and exit 0 so existing release flow is not broken.
1 parent 4f68df7 commit c755a07

1 file changed

Lines changed: 189 additions & 22 deletions

File tree

.github/workflows/release.yml

Lines changed: 189 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,38 @@
11
# ----------------------------------------------------------------------------
2-
# TODO(org-move): re-create secrets on the destination org before this repo
3-
# is transferred to `pilot-protocol/`. None of the secrets below survive a
4-
# repo transfer — they live on the SOURCE org (TeoSlayer) and need to be
5-
# created fresh on `pilot-protocol` so this workflow keeps working.
2+
# Releases continue from this repo as normal — both before and after the
3+
# planned move to `pilot-protocol/`. This comment is just a reminder for
4+
# migration day; it does NOT gate any release.
65
#
7-
# Secrets currently expected by `release.yml` and other workflows in this
8-
# repo (`ci.yml`, `nightly.yml`, `architecture.yml`, `codeql.yml`):
6+
# Why the reminder: GitHub repo transfers do not carry secrets across orgs.
7+
# The current setup only needs GITHUB_TOKEN (auto-issued), so a transfer
8+
# today would not break anything. But future work will re-introduce
9+
# secrets, and they must be re-created on the destination org BEFORE the
10+
# transfer flips DNS — otherwise the first release after the move silently
11+
# falls back to no-op or fails.
912
#
10-
# GITHUB_TOKEN - auto-issued, no action needed
13+
# Pending (not blocking releases now — will block IF the linked work
14+
# lands before the org migration):
1115
#
12-
# Secrets that need to be added BACK once the matching workflows are
13-
# restored (we removed `update-homebrew.yml` in PR #122 to drop the
14-
# cross-org PAT; auto-publish workflows for npm/PyPI never landed):
15-
#
16-
# HOMEBREW_TAP_TOKEN - PAT or GitHub App token with `contents:write`
17-
# on `pilot-protocol/homebrew-pilot`.
18-
# Prefer a GitHub App over a PAT — see
19-
# `actions/create-github-app-token@v1`.
16+
# HOMEBREW_TAP_TOKEN - re-introduced when Homebrew auto-publish
17+
# returns. Prefer a GitHub App over a PAT
18+
# via `actions/create-github-app-token@v1`.
2019
# NPM_TOKEN - if PILOT-203 lands sdk-node auto-publish.
2120
# PYPI_TOKEN - if PILOT-203 lands sdk-python auto-publish.
2221
# COSIGN_KEY / COSIGN_PASS - if PILOT-114 lands updater binary signing.
2322
#
24-
# When the migration happens, mirror the secrets via:
25-
# gh secret set HOMEBREW_TAP_TOKEN --repo pilot-protocol/<new-repo> --body <value>
26-
# (Reading the value from the old org first; GitHub never exposes secrets,
27-
# so the original cleartext source is required.)
23+
# Migration steps (when the day comes):
24+
# 1. List secrets on the source org with `gh secret list --repo <src>`.
25+
# 2. For each non-auto-issued secret, recreate it on the destination
26+
# using the original cleartext value (GitHub never reveals existing
27+
# secret values).
28+
# 3. Transfer the repo via Settings → "Transfer ownership" or
29+
# `gh api repos/<src>/transfer -f new_owner=<dst>`.
30+
# 4. Re-verify a release tag triggers this workflow successfully.
2831
#
29-
# Track the migration in the org-move runbook; do not delete this comment
30-
# until every workflow file that previously referenced a secret has either
31-
# (a) been re-wired against the new secret, or (b) been confirmed retired.
32+
# Track the migration in the org-move runbook. Do NOT delete this comment
33+
# until either: (a) the migration has completed and every reintroduced
34+
# secret is wired against the destination org, or (b) auto-publish and
35+
# binary signing have been formally retired.
3236
# ----------------------------------------------------------------------------
3337

3438
name: Release
@@ -273,3 +277,166 @@ jobs:
273277
generate_release_notes: true
274278
draft: false
275279
prerelease: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}
280+
281+
# ----------------------------------------------------------------------------
282+
# publish-manifest
283+
#
284+
# Regenerates `pilotprotocol.network/.well-known/latest.json` from the tag we
285+
# just shipped, then hands it off to `pilot-protocol/website` via
286+
# `repository_dispatch`. The website side commits the JSON to main, which
287+
# triggers the Cloudflare Pages deploy.
288+
#
289+
# The manifest is the single source of truth that every install surface
290+
# (install.sh, Homebrew formula, SDK release helpers) reads to decide which
291+
# version is current. Failing here does NOT roll the release back — the
292+
# GitHub release is already live — but it does mean install.sh will keep
293+
# serving the old tag until the manifest is republished. The step is best-
294+
# effort and prints a clear hint when the dispatch token is missing.
295+
# ----------------------------------------------------------------------------
296+
publish-manifest:
297+
name: Publish version manifest
298+
needs: release
299+
runs-on: ubuntu-latest
300+
steps:
301+
- name: Build manifest JSON from this release
302+
id: build
303+
env:
304+
TAG: ${{ github.ref_name }}
305+
IS_PRE: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}
306+
run: |
307+
# Pull the just-published checksums.txt directly from the GitHub
308+
# release (it landed there in the previous job).
309+
curl -fsSL -o checksums.txt \
310+
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/checksums.txt"
311+
312+
sha_for() {
313+
grep " $1\$" checksums.txt | awk '{print $1}'
314+
}
315+
316+
DARWIN_AMD64=$(sha_for "pilot-darwin-amd64.tar.gz")
317+
DARWIN_ARM64=$(sha_for "pilot-darwin-arm64.tar.gz")
318+
LINUX_AMD64=$(sha_for "pilot-linux-amd64.tar.gz")
319+
LINUX_ARM64=$(sha_for "pilot-linux-arm64.tar.gz")
320+
321+
# Refuse to publish a manifest with missing checksums — install.sh
322+
# would silently skip verification.
323+
for v in "$DARWIN_AMD64" "$DARWIN_ARM64" "$LINUX_AMD64" "$LINUX_ARM64"; do
324+
if [ -z "$v" ]; then
325+
echo "error: checksums.txt missing one or more platform entries"
326+
cat checksums.txt
327+
exit 1
328+
fi
329+
done
330+
331+
# When the new tag is a prerelease, leave latest_stable alone and
332+
# bump only latest_prerelease + channels.edge. The website receiver
333+
# merges into the existing manifest.
334+
if [ "$IS_PRE" = "true" ]; then
335+
STABLE="" ; EDGE="$TAG"
336+
else
337+
STABLE="$TAG" ; EDGE="$TAG"
338+
fi
339+
340+
UPDATED_AT=$(date -u +%Y-%m-%dT%H:%M:%SZ)
341+
OWNER_REPO="${GITHUB_REPOSITORY}"
342+
343+
cat > manifest.json <<EOF
344+
{
345+
"\$schema": "https://pilotprotocol.network/.well-known/latest.schema.json",
346+
"updated_at": "${UPDATED_AT}",
347+
"tag": "${TAG}",
348+
"is_prerelease": ${IS_PRE},
349+
"proposed_stable": "${STABLE}",
350+
"proposed_edge": "${EDGE}",
351+
"release_notes_url": "https://github.com/${OWNER_REPO}/releases/tag/${TAG}",
352+
"homebrew_formula_url": "https://github.com/TeoSlayer/homebrew-pilot/raw/main/Formula/pilotprotocol.rb",
353+
"platforms": {
354+
"darwin-amd64": { "url": "https://github.com/${OWNER_REPO}/releases/download/${TAG}/pilot-darwin-amd64.tar.gz", "sha256": "${DARWIN_AMD64}" },
355+
"darwin-arm64": { "url": "https://github.com/${OWNER_REPO}/releases/download/${TAG}/pilot-darwin-arm64.tar.gz", "sha256": "${DARWIN_ARM64}" },
356+
"linux-amd64": { "url": "https://github.com/${OWNER_REPO}/releases/download/${TAG}/pilot-linux-amd64.tar.gz", "sha256": "${LINUX_AMD64}" },
357+
"linux-arm64": { "url": "https://github.com/${OWNER_REPO}/releases/download/${TAG}/pilot-linux-arm64.tar.gz", "sha256": "${LINUX_ARM64}" }
358+
}
359+
}
360+
EOF
361+
cat manifest.json
362+
363+
# Escape for JSON embedding in the dispatch payload.
364+
PAYLOAD=$(jq -c '{event_type:"manifest-update", client_payload:{manifest:.}}' manifest.json)
365+
echo "payload=$PAYLOAD" >> "$GITHUB_OUTPUT"
366+
367+
- name: Dispatch to website
368+
env:
369+
# SHOCKWAVE_DISPATCH_TOKEN must have `repository_dispatch` scope on
370+
# pilot-protocol/website. Prefer a GitHub App token over a PAT —
371+
# see actions/create-github-app-token@v1.
372+
SHOCKWAVE_TOKEN: ${{ secrets.SHOCKWAVE_DISPATCH_TOKEN }}
373+
run: |
374+
if [ -z "$SHOCKWAVE_TOKEN" ]; then
375+
echo "::warning::SHOCKWAVE_DISPATCH_TOKEN secret unset — skipping manifest dispatch."
376+
echo "Action required: set the secret and re-run this workflow to publish ${{ github.ref_name }}."
377+
exit 0
378+
fi
379+
HTTP_CODE=$(curl -sS -o /tmp/resp -w '%{http_code}' \
380+
-X POST \
381+
-H "Accept: application/vnd.github+json" \
382+
-H "Authorization: Bearer ${SHOCKWAVE_TOKEN}" \
383+
"https://api.github.com/repos/pilot-protocol/website/dispatches" \
384+
-d '${{ steps.build.outputs.payload }}')
385+
if [ "$HTTP_CODE" != "204" ]; then
386+
echo "dispatch failed (HTTP $HTTP_CODE):"
387+
cat /tmp/resp
388+
exit 1
389+
fi
390+
echo "manifest dispatch accepted (HTTP 204)"
391+
392+
# ----------------------------------------------------------------------------
393+
# shockwave
394+
#
395+
# Notify every package that derives from web4 (Homebrew formula + SDKs) that
396+
# a new release exists. Each downstream repo runs its own bump workflow on
397+
# receiving `repository_dispatch` event_type=upstream-release.
398+
#
399+
# Receivers (each must have a workflow listening for `upstream-release`):
400+
# - pilot-protocol/homebrew-pilot → bump Formula/pilot.rb
401+
# - pilot-protocol/sdk-node → bump pkg version + npm publish
402+
# - pilot-protocol/sdk-python → bump pyproject + PyPI publish
403+
# - pilot-protocol/sdk-swift → bump Package.swift binaryTarget
404+
#
405+
# Soft-fail per receiver: a missing token or a 404 on one repo MUST NOT block
406+
# the others. The job summary at the end lists which targets succeeded so a
407+
# missed dispatch is visible without grepping logs.
408+
# ----------------------------------------------------------------------------
409+
shockwave:
410+
name: Shockwave fan-out
411+
needs: release
412+
runs-on: ubuntu-latest
413+
steps:
414+
- name: Dispatch to downstream consumers
415+
env:
416+
SHOCKWAVE_TOKEN: ${{ secrets.SHOCKWAVE_DISPATCH_TOKEN }}
417+
TAG: ${{ github.ref_name }}
418+
IS_PRE: ${{ contains(github.ref_name, '-rc') || contains(github.ref_name, '-beta') }}
419+
run: |
420+
if [ -z "$SHOCKWAVE_TOKEN" ]; then
421+
echo "::warning::SHOCKWAVE_DISPATCH_TOKEN secret unset — skipping fan-out."
422+
exit 0
423+
fi
424+
summary=""
425+
for repo in homebrew-pilot sdk-node sdk-python sdk-swift; do
426+
payload=$(jq -nc --arg tag "$TAG" --argjson pre "$IS_PRE" \
427+
'{event_type:"upstream-release", client_payload:{tag:$tag, is_prerelease:$pre}}')
428+
HTTP_CODE=$(curl -sS -o /tmp/resp -w '%{http_code}' \
429+
-X POST \
430+
-H "Accept: application/vnd.github+json" \
431+
-H "Authorization: Bearer ${SHOCKWAVE_TOKEN}" \
432+
"https://api.github.com/repos/pilot-protocol/${repo}/dispatches" \
433+
-d "$payload")
434+
if [ "$HTTP_CODE" = "204" ]; then
435+
summary="${summary} ✓ ${repo}\n"
436+
else
437+
summary="${summary} ✗ ${repo} (HTTP ${HTTP_CODE})\n"
438+
echo "::warning::shockwave dispatch failed for ${repo}: HTTP ${HTTP_CODE}"
439+
cat /tmp/resp
440+
fi
441+
done
442+
printf "Shockwave fan-out summary:\n${summary}" >> "$GITHUB_STEP_SUMMARY"

0 commit comments

Comments
 (0)