Repository navigation
166 lines (157 loc) · 8.19 KB
/
Copy pathweekly-release.yml
File metadata and controls
166 lines (157 loc) · 8.19 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
154
155
156
157
158
159
160
161
162
163
164
165
166
name: Stable release
# Tags the newest green commit on main as v<VERSION>, publishes the GitHub
# Release, and fast-forwards refs/heads/stable to it (docs/releases.md). Team
# installs follow stable, so this is the only thing that moves them. Runs
# every day at 14:00 UTC as the daily check (scripts/release-cadence.ts, R74):
# it cuts when a gated change stable lacks carries an important: changelog
# fragment and stable is 24 hours old, when stable is 7 days old and main has
# gated changes, or on Monday and Thursday past the 72-hour floor; otherwise it
# does nothing and says why (the file keeps its weekly-release.yml name so its
# run history and every `gh workflow run` reference stay intact). A hand-run
# cut moves stable at most once per 72 hours: a cut inside
# that floor is refused with REFUSED_FLOOR and NEXT_ALLOWED; a scheduled run
# gets 3 hours of grace for cron start jitter, a hand-run cut none. Two
# exceptions pass it, one per cut and each with a ref: a security cut (the
# security input, naming a GHSA, CVE or #<n> the script ties to the released
# commits) and a regression cut (the regression input, naming a
# regression-labelled #<n> and the earlier stable release v<X> the behavior
# worked in). Run it by hand with a
# ref for a hotfix, or with dry_run to see the plan and change nothing. A pull request that touches
# the release tooling runs the dry run against main with a read-only token,
# which is the only pre-merge exercise of the real workflow. The candidate's
# gate must already be green (free-tests and main-backstop check runs
# succeeded, every check run finished and none red, no active freeze): this
# workflow never runs tests or paid evals. Dispatch it from main: the cut job
# runs only there, with main's tooling.
on:
schedule:
# Every day 14:00 UTC (07:00 PDT, 06:00 PST): the daily check. Monday and
# Thursday are the regular cuts, exactly 72 h, then 96 h, apart; each floor's
# scheduled grace covers a late start the day before.
- cron: '0 14 * * *'
workflow_dispatch:
inputs:
dry_run:
description: 'Print the plan (candidate, tag, stable move, notes) and change nothing'
type: boolean
default: true
ref:
description: 'Hotfix: release this commit of main instead of the newest green one (full 40-hex SHA)'
type: string
default: ''
security:
description: 'Security cut (needs ref): the reason, naming the GHSA-…, CVE-… or #<n> it fixes; passes the 72-hour floor once verified and lands in the release notes'
type: string
default: ''
regression:
description: 'Regression cut (needs ref): the reason, naming the regression-labelled #<n> it fixes and the earlier stable release v<X> it worked in; passes the 72-hour floor once verified and lands in the release notes'
type: string
default: ''
daily:
description: 'Run the daily check now (an important change past 24 h, a forced cut at 7 days, the regular cut on Monday or Thursday) instead of a plain cut; no ref, security or regression'
type: boolean
default: false
pull_request:
paths:
- '.github/workflows/weekly-release.yml'
- 'scripts/release-cut.ts'
- 'scripts/release-cadence.ts'
- 'lib/changelog-fragments.ts'
# One cut at a time; a second dispatch waits for the first. A pull request's
# dry run is its own group.
concurrency:
group: weekly-release-${{ github.event.pull_request.number || 'cut' }}
cancel-in-progress: false
permissions: {}
jobs:
cut:
name: Cut the release
if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main'
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
# The tag, the stable branch and the release.
contents: write
# Reads the candidate's check runs.
checks: read
# A security or regression cut reads the label on the #<n> its reason names.
issues: read
pull-requests: read
steps:
# The dispatched ref supplies the tooling; the release candidate always
# comes from origin/main, which fetch-depth 0 brings along with every
# branch.
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Fetch stable and the tags
run: |
set -euo pipefail
git fetch --quiet origin '+refs/heads/stable:refs/remotes/origin/stable' 2>/dev/null || echo "origin has no stable yet: this cut creates it"
git fetch --quiet --tags origin
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Plan and cut
env:
GH_TOKEN: ${{ github.token }}
DRY_RUN: ${{ github.event_name == 'workflow_dispatch' && inputs.dry_run && 'true' || 'false' }}
RELEASE_REF: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || '' }}
SECURITY_REASON: ${{ github.event_name == 'workflow_dispatch' && inputs.security || '' }}
REGRESSION_REASON: ${{ github.event_name == 'workflow_dispatch' && inputs.regression || '' }}
DAILY: ${{ (github.event_name == 'schedule' || (github.event_name == 'workflow_dispatch' && inputs.daily)) && 'true' || 'false' }}
run: |
set -euo pipefail
if [ -n "$RELEASE_REF" ] && ! [[ "$RELEASE_REF" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::ref must be the full 40-hex SHA of a commit on main"
exit 1
fi
if [ -n "$SECURITY_REASON" ] && [ -z "$RELEASE_REF" ]; then
echo "::error::a security cut needs ref: the full SHA of the fix commit on main"
exit 1
fi
if [ -n "$REGRESSION_REASON" ] && [ -z "$RELEASE_REF" ]; then
echo "::error::a regression cut needs ref: the full SHA of the fix commit on main"
exit 1
fi
if [ "$DAILY" = true ] && [ -n "$RELEASE_REF$SECURITY_REASON$REGRESSION_REASON" ]; then
echo "::error::the daily check picks its own candidate: run ref, security or regression without daily"
exit 1
fi
args=(--repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY")
[ "$DAILY" = true ] && args+=(--daily)
[ "$DRY_RUN" = true ] && args+=(--dry-run)
[ -n "$RELEASE_REF" ] && args+=(--ref "$RELEASE_REF")
[ -n "$SECURITY_REASON" ] && args+=(--security "$SECURITY_REASON")
[ -n "$REGRESSION_REASON" ] && args+=(--regression "$REGRESSION_REASON")
echo "## Stable release ($([ "$DRY_RUN" = true ] && echo 'dry run' || echo 'cut')$([ "$DAILY" = true ] && echo ', daily check'), $GITHUB_EVENT_NAME)" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts "${args[@]}"
dry-run:
name: Dry run against main
if: github.event_name == 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 20
permissions:
contents: read
checks: read
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0
persist-credentials: false
- uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: 1.4.2
- name: Plan only
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
git fetch --quiet origin '+refs/heads/stable:refs/remotes/origin/stable' 2>/dev/null || echo "origin has no stable yet"
git fetch --quiet --tags origin
echo "## Stable release (dry run from PR #${{ github.event.pull_request.number }})" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run
echo "## Daily check (dry run from PR #${{ github.event.pull_request.number }})" >> "$GITHUB_STEP_SUMMARY"
bun --no-env-file --config=/dev/null --no-install run scripts/release-cut.ts --repo "$GITHUB_REPOSITORY" --summary "$GITHUB_STEP_SUMMARY" --dry-run --daily