Skip to content

ci: automated beta → latest → stable release pipeline - #97

Merged
plumppig merged 9 commits into
masterfrom
chore/ci-cd
Oct 10, 2026
Merged

plumppig merged 9 commits into
masterfrom
chore/ci-cd

Conversation

@plumppig

@plumppig plumppig commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Adds an automated release pipeline: PRs into master are type-checked, tested and built, and each merge that changes the package publishes the next beta. A version becomes an rc after its first beta is 7 days old and its newest has been quiet for 3 (capped at 14), then reaches latest after 7 more days with no Jira bugs labeled ryuu.js-X.Y.Z, and a release:hotfix label applied by a maintainer skips both soaks. Releases stay off until the RELEASE_ENABLED repo variable is true; setup is in RELEASING.md, and this supersedes #65.

PRs into master are type-checked, tested and built. Each merge that changes the
published package cuts the next X.Y.Z-beta.N under the npm `beta` tag; a beta that
soaks 14 days with no Jira bugs labelled ryuu.js-X.Y.Z ships as `latest` (with a
release/vX.Y.Z branch), and a GA with 30 bug-free days is tagged `stable`.
Publishing uses npm trusted publishing; automatic runs stay off until the
RELEASE_ENABLED repo variable is `true`. Setup and runbook: RELEASING.md.

Supersedes #65.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
JSON and others added 8 commits October 9, 2026 13:58
Customers who resolve `latest` (plain installs, CDN URLs, and npm ranges like
^6.0.9) now only move to a version that has passed both soaks. A beta that soaks
14 bug-free days is republished unchanged as X.Y.Z-rc.N under `rc`; an rc that
soaks 30 bug-free days becomes X.Y.Z under `latest` with its release/vX.Y.Z
branch. Betas and rcs are prereleases, so no package manager resolves them from a
normal range. The separate `stable` dist-tag, its jobs and the second npm trusted
publisher are gone. force_rc/force_ga cover urgent fixes, and verify-tag.sh now
checks the whole GA → rc → beta → master chain.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
RELEASING.md now walks through the beta → rc → latest cycle with a Mermaid
flowchart, an example rollout (Gantt chart plus a day-by-day table), what each
interrupt does, every variable, secret and Run workflow option with its UI path
and gh command, and a one-time setup checklist. Adds `npm run release:plan`
(what Release would do now, with SIMULATE_NOW / MASTER_REF) and
`npm run release:rehearse` (merge → beta → publish rehearsal in a throwaway
clone that never pushes or publishes). The main README gains a release
channels table and a pointer to RELEASING.md.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Soaks drop from 14 + 30 days to about two weeks: a line becomes an rc once its
first beta is 7 days old and its newest beta has been quiet for 3 days, and an
rc reaches latest after 7 bug-free days. New betas restart only the quiet clock,
so steady merging can no longer hold a release back forever.

A merged PR labelled release:hotfix sends its release straight through: Release
looks up the label when it cuts the beta (scripts/ci/github.ts), records
"(hotfix)" in the tag annotation, and the following runs promote the beta, rc
and latest back to back without the soaks or the Jira check. Every build, test,
tag verification and package-identity check still runs.

RELEASING.md, README and CLAUDE.md are updated for the new timings, with a
Hotfixes section and a recomputed example rollout.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Acting on five independent reviews of the release pipeline:

Trust boundary
- Resolve master as refs/remotes/origin/master everywhere; a tag named
  origin/master used to shadow it and let an attacker's commit be built.
- Split release.yml into plan / prepare / push. Build and tests now run in a
  job with no secrets; the deploy key only reaches a job that runs git and bash
  and re-verifies the tag. force_rc/force_ga need the admin role. The
  release:hotfix label only counts if a maintainer or admin applied it before
  the PR merged (scripts/ci/github.ts).
- verify-tag.sh no longer fails open on big lockfiles or npm errors.

Decision logic
- Retry a GA publish even when the next version's betas are already on npm.
- A labelled hotfix always releases and is looked up before the soak gates, so
  a no-op labelled PR cannot make a later PR a hotfix and a Jira outage cannot
  block one.
- The first-beta clock is capped at 14 days so frequent merging cannot starve a
  release; a one-hour grace makes cron timing predictable.
- Lowering master's version never moves the line back, a rolled-back latest
  cannot re-release a superseded rc, and master's own version counts as taken
  so "start 6.1.0" no longer wedges the pipeline.

Workflows: queue: max so pending runs are not cancelled, job timeouts, and
dispatch on refs/tags/. Adds tests for verify-tag, push-release and
same-artifact against real git repos, and updates RELEASING.md.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Takes master's Dependabot bumps and re-applies this PR's two dependency
changes (standard-version removed, @types/node added) to package-lock.json.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
verify-tag's "not a pipeline tag" test made an annotated tag with no identity.
It passed on a laptop with a global git identity and failed on the CI runner,
which has none. The helper now configures one per repo, and the ci project
passes with HOME and the global git config removed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ent the scripts

Because every PR is squash-merged, the commit being released is one PR, so the
hotfix check now looks only at master's latest commit instead of scanning a
range of up to 50. That removes the verdict cache and the case where a labelled
PR that shipped nothing made a later, unrelated merge look like a hotfix, so
prepare-release.sh no longer needs to release an unchanged package for a
hotfix. If another PR lands before Release runs, the change takes the normal
soak, which can only be slower, never faster.

github.ts follows CONVENTIONS.md (no push/pop, no guard-then-assign) and uses
clearer names (verifyLabel, isTrustedLabel). Every named function in lib.ts,
jira.ts, github.ts and release.ts now has a plain-language JSDoc block with
@PARAM and @returns; the compiled output is unchanged.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@plumppig
plumppig merged commit c51bf99 into master Oct 10, 2026
4 of 6 checks passed
@plumppig
plumppig deleted the chore/ci-cd branch October 10, 2026 05:23
plumppig added a commit that referenced this pull request Oct 10, 2026
With no RELEASE_DEPLOY_KEY secret, actions/checkout quietly falls back to the
read-only workflow token. A dry run then passed, and a real run only failed at
the very end of the push job with a bare 403 (seen on the first run after
#97 merged).

The push job now starts by failing if the secret is empty, and asserts that
checkout used an SSH remote. push-release.sh turns a rejected push into a
message that names the likely causes: a missing or read-only deploy key, or
rulesets that deploy keys cannot bypass. RELEASING.md describes what a green
dry run does and does not prove, and lists the three failure messages.

Co-authored-by: JSON <Jason.Hansen@domo.com>
Co-authored-by: Claude Sonnet 5.5 <noreply@anthropic.com>
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