-
-
Notifications
You must be signed in to change notification settings - Fork 91
153 lines (144 loc) · 8.73 KB
/
Copy pathorb-beta-release.yml
File metadata and controls
153 lines (144 loc) · 8.73 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
# 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