Skip to content

ci(mirror): chain the S3 mirror off Release instead of a manual dispatch - #15

Merged
krassx merged 1 commit into
mainfrom
ci/automate-s3-mirror
Aug 25, 2026
Merged

ci(mirror): chain the S3 mirror off Release instead of a manual dispatch#15
krassx merged 1 commit into
mainfrom
ci/automate-s3-mirror

Conversation

@krassx

@krassx krassx commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Makes download.bugsee.com part of a release automatically, closing the gap
that left v0.7.4 unmirrored for six weeks.

The problem

mirror-to-s3.yml was workflow_dispatch-only, so a tag push published to
GitHub and npm but stopped short of the CDN unless someone remembered to trigger
it. v0.7.4 is what that looks like in practice: released 2026-08-11, never
mirrored — the CDN served 0.7.3 until yesterday, and bugsee-cli update (which
reads cli/v<major>.x/version.txt, see src/cli/update.rs:25) never offered it.
Nothing failed. The release just quietly stopped halfway.

Why this is safe to automate now

The old header justified the manual trigger: GitHub suppresses downstream events
for activity initiated by the default GITHUB_TOKEN, and dist's release.yml
publishes the Release with exactly that token.

That's true of release: / push: — but not of workflow_run, which fires
on another workflow completing and is not suppressed. npm-publish.yml has
been chaining off Release this way all along and did so successfully for v0.7.5.
Same mechanism and the same guards are adopted here, so the two post-release
workflows now work identically.

OIDC is unaffected. A workflow_run run executes from the default branch, so
github.ref is refs/heads/main — exactly as for a workflow_dispatch from
main, which is how every mirror to date has run. The role's trust policy sees an
unchanged subject.

What changed

  • workflow_run on ["Release"], guarded to a SUCCESSFUL, tag-push
    triggered Release whose ref matches vX.Y.Z (PR-mode and failed releases are
    filtered out). workflow_dispatch is kept for backfills.
  • A resolve job both paths converge on. A hand-typed tag that isn't
    vX.Y.Z now fails loudly rather than "succeeding" without mirroring; a
    non-version tag on the automated path is skipped quietly. It's a separate job
    so the credentialed job needs no per-step if: guards — a step added later
    can't silently run against a tag we meant to skip.
  • cli/latest/ artifact sync is now ADVANCE-ONLY, matching the version
    pointers. It used to be overwritten unconditionally, so mirroring an older tag
    after a newer one would leave cli/latest/version.txt pointing at the new
    version with the old binaries beside it. Hand-sequencing v0.7.4 before
    v0.7.5 yesterday is what prevented that; it's now a property of the workflow.
  • Global concurrency group (was per-tag) for the same reason: cli/latest/
    is one shared mutable prefix and two mirrors must never overlap.

Validation

  • actionlint clean on this file and every other hand-written workflow. (It
    reports shellcheck style nits in release.yml, which is cargo-dist generated
    and deliberately untouched.)
  • The two pieces of new logic were unit-tested against their edge cases:

Advance-only guard — 9/9:

latest is mirroring result
0.7.5 0.7.4 skip — the exact regression this prevents
0.7.3 0.7.4 advance
0.9.0 0.10.0 advance (numeric, not lexical)
0.10.0 0.9.0 skip
absent 0.7.5 write (first ever mirror)
0.7.5 0.7.5 idempotent re-mirror

Tag resolution — 8/8, including vnext (passes startsWith('v') but is
correctly skipped, not failed) and a typo'd manual 0.7.4 without the v
(fails loudly).

  • dist plan, cargo test --all-targets, cargo fmt all still pass.

Remaining verification

The workflow_run path can only be fully exercised by a real release. Once
merged, the workflow_dispatch path can be smoke-tested immediately by
re-mirroring v0.7.5 — idempotent, since the guard treats an equal version as a
harmless overwrite of identical bytes.

Not included

docs/unity-il2cpp-linenumber-mappings.md has unrelated in-progress edits in the
working tree; deliberately left out.

🤖 Generated with Claude Code

The mirror was `workflow_dispatch`-only, so a tag push published to GitHub and
npm but stopped short of download.bugsee.com unless someone remembered to
trigger it. v0.7.4 is what that looks like in practice: released 2026-08-11,
never mirrored, so the CDN served 0.7.3 for six weeks and `bugsee-cli update`
— which reads `cli/v<major>.x/version.txt` — never offered it. Nothing failed;
the release just quietly stopped halfway.

The old header explained the manual trigger by noting that GitHub suppresses
downstream events for activity initiated by the default GITHUB_TOKEN, which is
what dist's release.yml publishes the Release with. That is true of
`release:`/`push:` but NOT of `workflow_run`, which fires on another WORKFLOW
completing and is not suppressed — npm-publish.yml has been chaining off
Release that way all along. Same mechanism and the same guards are adopted
here: a SUCCESSFUL, tag-push-triggered Release whose ref matches `vX.Y.Z`.

`workflow_dispatch` is kept for backfills. Both paths converge on a `resolve`
job that normalizes the tag; a hand-typed tag that isn't `vX.Y.Z` now fails
loudly rather than "succeeding" without mirroring, while a non-version tag on
the automated path is skipped quietly. Splitting resolve into its own job means
the credentialed job needs no per-step `if:` guards, so a step added later
cannot silently run against a tag we meant to skip.

Also makes the `cli/latest/` artifact sync ADVANCE-ONLY, matching the version
pointers. Previously it was overwritten unconditionally, so mirroring an older
tag after a newer one would leave `cli/latest/version.txt` pointing at the new
version with the OLD binaries beside it — a silent mismatch. Sequencing the
v0.7.4-then-v0.7.5 backfill by hand was what kept that from happening; it is
now a property of the workflow. The concurrency group is global rather than
per-tag for the same reason: `cli/latest/` is one shared mutable prefix and two
mirrors must never overlap.

OIDC is unaffected: a `workflow_run` run executes from the default branch, so
`github.ref` is `refs/heads/main` — exactly as for a dispatch from main — and
the role's trust policy sees an unchanged subject.

Verified: actionlint clean; the advance-only guard and the tag-resolution logic
each unit-tested against their edge cases (version regression, absent pointer,
re-mirror, 0.10.0-vs-0.9.0 numeric ordering, `vnext`, typo'd manual input).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
@krassx
krassx merged commit 3364267 into main Aug 25, 2026
12 checks passed
@krassx
krassx deleted the ci/automate-s3-mirror branch August 25, 2026 04:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant