Skip to content

Add release-train freeze/release-PR/unfreeze reusable workflows#32

Open
pnewsam wants to merge 3 commits into
mainfrom
add/release-train-reusable-workflows
Open

Add release-train freeze/release-PR/unfreeze reusable workflows#32
pnewsam wants to merge 3 commits into
mainfrom
add/release-train-reusable-workflows

Conversation

@pnewsam

@pnewsam pnewsam commented Jul 9, 2026

Copy link
Copy Markdown

Summary

Centralizes the weekly staging → main release cycle into three reusable (workflow_call) workflows so all 7 release-train repos share one source of truth, driven by ~25-line per-repo caller wrappers (same pattern as stale-deploy-label.yml). This is the central PR that the per-repo release-train PRs (mindshub_inference#319 et al.) will call once merged.

Reusable workflow Name (callers keep identical) Role
staging-freeze.yml Staging Freeze Activate the staging-freeze ruleset to lock staging (skips if staging == main)
weekly-merge-staging.yml Create staging to main release PR Open the staging → main PR (idempotent)
staging-unfreeze.yml Staging Unfreeze Disable the ruleset when the release PR merges, then sync main back into staging

The chain stays event-driven: Staging Freeze finishing fires the release-PR workflow via workflow_run; merging that PR fires Staging Unfreeze. workflow_run matches on the caller workflow's name, so callers keep the names verbatim.

What changed vs. the original per-repo implementation

Implements the three structural changes from the coordinated review of the 7 release-train PRs:

  1. GitHub App instead of PAT. Per-job tokens minted with actions/create-github-app-token (mindsdb-release-train App). No standing admin on any repo, and no PAT readable by workflows in the public repos. Reads vars.RELEASE_APP_CLIENT_ID + secrets.RELEASE_APP_PRIVATE_KEY (org-provisioned; the secret reaches the reusable workflows via secrets: inherit).
  2. Reusable workflows + thin callers. The logic lives here once; each repo carries only triggers + guards. Workflow names preserved so the workflow_run chain still fires.
  3. Freeze via ruleset toggle. Freeze/unfreeze flip a pre-provisioned staging-freeze ruleset between enforcement: active / disabled instead of round-tripping branch protection. Rulesets layer on top of protection, so the toggle can't clobber the protection config. set-branch-lock.js is deleted (does not exist in this PR).

Folded-in hardening

  • Fork-PR guard on unfreeze (head.repo.full_name == github.repository alongside head.ref == 'staging') — lives in the caller wrapper.
  • SHA-pinned actions with version comments: actions/checkout@34e1148… (v4.3.1), actions/create-github-app-token@bcd2ba4… (v3.2.0).
  • persist-credentials: false on the read-only checkouts (freeze, release-PR). Only the unfreeze sync-back checkout carries the App token, since it is the one step that pushes.
  • Cron off the top of the hour — callers use 47 13 * * 5 (documented in the README; xx:00 is GitHub's high-load slot and the old "6am PST" comment was only true in winter).

Notes

  • github.repository / github.event inside a reusable workflow reflect the caller repo, so the workflows operate on the calling repo with no target-repo input.
  • The release PR is created with the App token (not GITHUB_TOKEN) so CI actually fires on it.
  • The sync-back push is load-bearing on squash-merge repos and lands because the App is the ruleset bypass actor on staging.

Prerequisites (provisioned once, org level, scoped to the 7 repos)

  • mindsdb-release-train GitHub App (Administration / Contents / PRs: write), installed on each repo, set as bypass actor on the staging ruleset.
  • vars.RELEASE_APP_CLIENT_ID (org variable) + secrets.RELEASE_APP_PRIVATE_KEY (org secret).
  • A pre-provisioned staging-freeze ruleset per repo (one update rule on staging, created disabled, App as bypass actor).

Rollout

Merge this first, then add caller wrappers (see README) per repo: canary on cowork-server with one manual full cycle (freeze → release PR → merge → unfreeze → sync-back), then the remaining six.

Refs Linear ENG-669.

🤖 Generated with Claude Code

pnewsam and others added 2 commits July 9, 2026 16:23
Centralizes the weekly staging→main release cycle as three reusable
(workflow_call) workflows so all repos share one source of truth, called
from ~25-line per-repo wrappers (same pattern as stale-deploy-label.yml).

- Auth via the mindsdb-release-train GitHub App (actions/create-github-app-token,
  SHA-pinned) instead of a PAT; no standing admin, no PAT readable by public-repo
  workflows.
- Freeze/unfreeze toggle a pre-provisioned `staging-freeze` ruleset's enforcement
  (active/disabled) rather than round-tripping branch protection, so the toggle
  can never clobber protection config. set-branch-lock.js is gone.
- actions/checkout and create-github-app-token SHA-pinned with version comments;
  persist-credentials: false on the read-only checkouts.
- README documents prerequisites and the caller wrappers.

Callers keep the workflow names identical ("Staging Freeze" etc.) so the
workflow_run chain still fires. Fork-PR guard on unfreeze lives in the caller.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Keeps the README caller wrappers in step with the callers shipped to the repos
(CodeQL actions/missing-workflow-permissions).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Use `client-id` (not `app-id`) for actions/create-github-app-token — the
  stored credential is the App's client ID, not the numeric app id.
- Rename reusable workflows to release-freeze.yml / release-pr.yml /
  release-unfreeze.yml (matches the step-by-step spec).
- Ruleset toggle now GETs the full ruleset, flips only `enforcement`, and PUTs
  the whole body back (partial PUT does not guarantee omitted fields survive).
  Body written to a temp file, never echoed — three consumer repos are public.
- Fail loudly if the staging-freeze ruleset is missing (provisioning drift).
- Expose should_freeze (and ahead) as job outputs on the freeze workflow.
- URL-encode the bot username (%5Bbot%5D) for the users API lookup.
- README: rename table + caller `uses:` paths, note SHA-pinning callers.

Co-Authored-By: Claude Opus 4.8 <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