orb-beta-release #112
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Automated ORB (self-host container image, ghcr.io/jsonbored/loopover-selfhost) beta channel. | |
| # Daily (or on demand via workflow_dispatch), checks whether any image-relevant commit has landed | |
| # since the last orb-v tag (scripts/check-orb-release-due.ts / scripts/orb-release-core.ts) and, | |
| # if so, cuts the next `orb-vX.Y.Z-beta.N` tag and dispatches release-selfhost.yml to build + publish | |
| # it -- fully unattended: that workflow's `environment:` routes an actual beta version to | |
| # `release-beta` (no required reviewers), while a stable/rc version still requires the human-gated | |
| # `release` environment. Promoting a beta to a stable release stays a manual `git tag orb-vX.Y.Z` by | |
| # a maintainer -- this workflow never bumps orb-manifest.json's version or cuts a non-beta tag. | |
| # | |
| # Deliberately independent of the MCP package's release automation (mcp-release-watch.yml / | |
| # mcp-release-core.ts) -- see scripts/orb-release-core.ts's own header for why. | |
| name: orb-beta-release | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| - cron: "10 6 * * *" | |
| permissions: | |
| contents: write # create + push the beta tag | |
| actions: write # dispatch release-selfhost.yml for the new tag | |
| concurrency: | |
| group: orb-beta-release | |
| cancel-in-progress: false | |
| jobs: | |
| cut-beta: | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 15 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Setup workspace | |
| uses: ./.github/actions/setup-workspace | |
| - name: Check whether an ORB beta is due | |
| id: report | |
| # check-orb-release-due.ts imports orb-release-core.ts directly via a `.js` specifier, so it needs tsx | |
| # (not plain node) to resolve that local .ts import. | |
| run: | | |
| set -euo pipefail | |
| npx tsx scripts/check-orb-release-due.ts --json --output orb-release-due.json | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| const report = JSON.parse(fs.readFileSync("orb-release-due.json", "utf8")); | |
| const version = report.nextTag.replace(/^orb-v/, ""); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `due=${report.due}\n`); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `tag=${report.nextTag}\n`); | |
| fs.appendFileSync(process.env.GITHUB_OUTPUT, `version=${version}\n`); | |
| NODE | |
| # Pushed with the default GITHUB_TOKEN whenever that alone will work, which does NOT fire | |
| # release-selfhost.yml's own `push: tags:` trigger (GitHub suppresses workflow-triggered-workflow | |
| # pushes to prevent recursion) -- that's why the next step dispatches it explicitly instead of | |
| # relying on this push alone. Mirrors publish-engine.yml / publish-mcp.yml's identical reasoning | |
| # and tagging idiom. | |
| # Exposes created=true/false so the dispatch step below never fires against a tag this run didn't | |
| # actually just create -- a defense-in-depth backstop (independent of orb-release-core.ts's own | |
| # correctness) against ever re-triggering a build for an already-published version/tag. | |
| - name: Tag the new beta | |
| id: tag | |
| if: steps.report.outputs.due == 'true' | |
| env: | |
| # A tag pointing at a commit that touches any .github/workflows/* file needs a token with the | |
| # `workflow` OAuth scope (classic PAT) / "Workflows: write" (fine-grained PAT) to push -- GitHub | |
| # hardcodes this against the default GITHUB_TOKEN regardless of this job's own `permissions:` | |
| # block; there is no permissions-block setting that lifts it (confirmed live: the push failed with | |
| # "refusing to allow a GitHub App to create or update workflow .github/workflows/ci.yml without | |
| # `workflows` permission" even with contents/actions both set to write above). ORB_RELEASE_WORKFLOW_TOKEN | |
| # is an OPTIONAL repo secret (a fine-grained PAT scoped to Contents: write + Workflows: write on | |
| # this repo only) held in reserve for exactly that case -- the push below always tries the default | |
| # token FIRST and only reaches for this one on the specific failure it's meant to fix. Using it | |
| # unconditionally would defeat the recursion suppression above on every cut, not just the rare one | |
| # that actually needs it (confirmed live: 2026-07-21, orb-v3.2.0-beta.11 produced duplicate runs | |
| # 29821878879 and 29821880057 for the same tag because this env used to be | |
| # `${{ secrets.ORB_RELEASE_WORKFLOW_TOKEN || github.token }}` unconditionally). | |
| GH_TOKEN: ${{ github.token }} | |
| ORB_RELEASE_WORKFLOW_TOKEN: ${{ secrets.ORB_RELEASE_WORKFLOW_TOKEN }} | |
| TAG: ${{ steps.report.outputs.tag }} | |
| VERSION: ${{ steps.report.outputs.version }} | |
| run: | | |
| set -euo pipefail | |
| if git ls-remote --exit-code --heads origin "$TAG" >/dev/null 2>&1; then | |
| echo "::error::Branch $TAG already exists; refusing to create or dispatch an ambiguous release ref." | |
| exit 1 | |
| fi | |
| if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then | |
| echo "Tag $TAG already exists; skipping (a previous run likely already tagged it, or it collides with an already-published version)." | |
| echo "created=false" >> "$GITHUB_OUTPUT" | |
| else | |
| git config user.name "github-actions[bot]" | |
| git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
| git tag -a "$TAG" -m "loopover-orb ${VERSION}" | |
| git remote set-url origin "https://github.com/${GITHUB_REPOSITORY}.git" | |
| gh auth setup-git | |
| if ! git push origin "$TAG" 2>push-error.log; then | |
| cat push-error.log >&2 | |
| if [ -n "$ORB_RELEASE_WORKFLOW_TOKEN" ] && grep -qi "workflow" push-error.log; then | |
| echo "::warning::Default token can't push $TAG (its history touches .github/workflows/*); retrying with ORB_RELEASE_WORKFLOW_TOKEN. That token is a PAT, not GITHUB_TOKEN, so this push will ALSO fire release-selfhost.yml's own tag-push trigger -- expect a duplicate run alongside the dispatch step below for this cut only." | |
| export GH_TOKEN="$ORB_RELEASE_WORKFLOW_TOKEN" | |
| gh auth setup-git | |
| git push origin "$TAG" | |
| else | |
| exit 1 | |
| fi | |
| fi | |
| echo "created=true" >> "$GITHUB_OUTPUT" | |
| fi | |
| # create_github_release=true: the tag above was just created and pushed, so release-selfhost.yml's | |
| # `--verify-tag` GitHub Release step can run safely (see that workflow's own comments). Gated on | |
| # steps.tag.outputs.created (not just due) so a no-op tag step -- for any reason -- never triggers a | |
| # rebuild/republish of an existing GHCR image tag with different content. | |
| # | |
| # Dispatch against the fully qualified TAG ref, not `main`: `main` is a floating ref, and this repo | |
| # merges fast enough that a commit can land in the gap between the tag push above and this dispatch. | |
| # `--ref main` would then resolve `github.sha` inside release-selfhost.yml to that NEWER commit, while | |
| # $TAG (pushed moments ago, immutable) still points at the older one it was actually cut for -- tripping | |
| # that workflow's own TAG_SHA-must-equal-RELEASE_SHA fail-safe and aborting the release. `refs/tags/$TAG` | |
| # preserves that race fix without letting a same-named branch shadow the tag namespace. | |
| - name: Dispatch the ORB release build | |
| if: steps.report.outputs.due == 'true' && steps.tag.outputs.created == 'true' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| TAG: ${{ steps.report.outputs.tag }} | |
| VERSION: ${{ steps.report.outputs.version }} | |
| run: gh workflow run release-selfhost.yml --ref "refs/tags/$TAG" -f "version=${VERSION}" -f create_github_release=true | |
| - name: Summarize | |
| if: always() | |
| run: | | |
| node <<'NODE' | |
| const fs = require("node:fs"); | |
| const report = JSON.parse(fs.readFileSync("orb-release-due.json", "utf8")); | |
| const lines = [ | |
| "## ORB Beta Release", | |
| "", | |
| `- Due: \`${report.due}\``, | |
| `- Next tag: \`${report.nextTag}\``, | |
| `- Target version: \`${report.targetVersion}\``, | |
| `- Manifest version: \`${report.manifestVersion ?? "none"}\``, | |
| `- Manifest stale (commits imply a bigger bump than the manifest declares): \`${report.manifestStale}\``, | |
| `- Latest stable tag: \`${report.latestStableTag ?? "none"}\``, | |
| `- Latest tag: \`${report.latestTag ?? "none"}\``, | |
| `- Image-relevant commits since last tag: \`${report.commits.length}\``, | |
| ]; | |
| fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`); | |
| NODE |