Repository navigation
356 lines (342 loc) · 15 KB
/
Copy pathpublish.yml
File metadata and controls
356 lines (342 loc) · 15 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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
name: Publish and Release
on:
push:
branches:
- main
# Recovery path for a release whose tag exists but whose npm publish or
# registry update did not land, and the manual path for a preview from an
# arbitrary commit. See docs/RELEASES.md.
workflow_dispatch:
inputs:
channel:
description: Which channel to publish
required: true
default: stable
type: choice
options:
- stable
- preview
ref:
description: Tag or commit to publish
required: true
type: string
publish_npm:
description: Publish the package before updating the registry (stable only)
required: true
default: true
type: boolean
permissions: {}
jobs:
# Every push to main re-computes the next version from the conventional commit
# messages and keeps a single release PR open. Merging that PR is what tags,
# releases and publishes; there is no manual release button.
release-please:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
timeout-minutes: 20
environment: release # Optional: for enhanced security
permissions:
contents: write
issues: write
pull-requests: write
steps:
# Generating a GitHub token, so that PRs and tags created by
# the release-please-action can trigger actions workflows.
- name: Generate GitHub token
uses: actions/create-github-app-token@v3
id: generate-token
with:
app-id: ${{ secrets.RELEASE_PLZ_APP_ID }}
private-key: ${{ secrets.RELEASE_PLZ_APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@45996ed1f6d02564a971a2fa1b5860e934307cf7 # v5.0.0
id: release
with:
token: ${{ steps.generate-token.outputs.token }}
# Do not add release-type here. The action ignores config-file whenever
# release-type is set, which would silently drop
# include-component-in-tag and start tagging codex-acp-vX.Y.Z instead of
# vX.Y.Z. The node release type is declared in that config instead, so
# nothing is lost by leaving this out.
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
# The gate for both paths: nothing is published unless this passes. Its `if`
# rather than `needs` alone decides whether a release is happening, because on
# workflow_dispatch the release-please job is skipped.
#
# Deliberately not in the `release` environment: OPENAI_API_KEY is a repository
# secret, so this needs nothing from it, and staying out keeps the job off the
# environment's deployment branch policy.
verify:
needs: [release-please]
if: ${{ always() && ((github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && inputs.publish_npm) || needs.release-please.outputs.release_created == 'true') }}
runs-on: ubuntu-latest
# Without this a stalled step burns the full 6h runner limit before anyone
# notices the release did not publish. A hung apt mirror already cost one
# release that way. Sized for the worst case of its steps: apt retries, then
# the unit and e2e suites, both of which run their files serially.
timeout-minutes: 40
permissions:
contents: read
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
- uses: actions/setup-node@v7
with:
node-version: "24"
check-latest: true
- name: Configure sandboxing
timeout-minutes: 15
run: |
# Acquire::*::Timeout only guards a single socket operation, so a mirror
# that stalls once the headers are in never trips it -- that ate a 6h
# runner limit, then a 5min step cap. Kill each attempt from the outside
# and retry the fetch instead. No Acquire::Retries: apt retrying inside a
# killed window would just burn the budget without ever finishing.
#
# The per-attempt budgets are generous on purpose. When the image's
# package lists are stale, a healthy `update` pulls ~5MB of indexes and
# takes about two minutes; a tighter cap killed it mid-download three
# times in a row and turned "slow but fine" into a failed release.
# Languages=none drops the Translation-* files, which nothing here reads.
apt_opts=(-o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30 -o Acquire::Languages=none)
for attempt in 1 2; do
if timeout 300 sudo apt-get "${apt_opts[@]}" update \
&& timeout 120 sudo apt-get "${apt_opts[@]}" install --yes bubblewrap; then
installed=yes
break
fi
echo "apt attempt ${attempt} stalled or failed, retrying"
sleep 10
done
if [ -z "${installed:-}" ]; then
echo "::error::bubblewrap install failed after 3 attempts"
exit 1
fi
sudo sysctl -w kernel.unprivileged_userns_clone=1
if [ -f /proc/sys/kernel/apparmor_restrict_unprivileged_userns ]; then
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
fi
- run: npm ci
- run: npm run typecheck
- run: npm test
- name: Run e2e tests
# The suite drives a live model, so a wedged turn hangs instead of
# failing. Bound it well above the ~3min happy path.
timeout-minutes: 15
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: npm run test:e2e
publish-npm:
name: Publish to npm
needs: [release-please, verify]
if: ${{ always() && needs.verify.result == 'success' }}
runs-on: ubuntu-latest
timeout-minutes: 15
environment: release # Optional: for enhanced security
permissions:
contents: read
id-token: write # npm trusted publishing, so there is no npm token
outputs:
version: ${{ steps.publish.outputs.version }}
steps:
- uses: actions/checkout@v7
with:
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.ref }}
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v7
with:
node-version: "24"
check-latest: true
registry-url: "https://registry.npmjs.org"
- run: npm ci
# prepublishOnly builds the bundle.
- name: Publish
id: publish
run: |
version="$(node -p "require('./package.json').version")"
npm publish --access public
echo "version=$version" >> "$GITHUB_OUTPUT"
# Same shape as publish-npm. Tagging is a separate job, so nothing here needs
# write access to the repository.
publish-npm-preview:
name: Publish preview to npm
# Every push to main gets a preview without waiting for CI or release-please,
# except release-please's own release merge, which publishes a stable version.
# Check the commit author and the generated release subject to avoid publishing
# the same tree twice; either match is enough to skip the preview.
if: >-
${{
(github.event_name == 'workflow_dispatch' && inputs.channel == 'preview') ||
(github.event_name == 'push'
&& github.event.head_commit.author.name != 'acp-release-bot[bot]'
&& !startsWith(github.event.head_commit.message, 'chore(main): release '))
}}
runs-on: ubuntu-latest
timeout-minutes: 15
# Same environment as the stable job, so the npm trusted-publisher binding holds.
# It carries no required reviewers, so previews never wait for an approval.
environment: release
permissions:
contents: read
id-token: write # npm trusted publishing, so there is no npm token
# Serialise previews so two pushes cannot read the same registry state and compute
# the same -preview.N. Job-level rather than workflow-level: a workflow-level group
# would also serialise release-please, and cancelling a queued release-please run
# means a release PR that silently stops updating. The trade-off is that GitHub
# keeps only one run pending per group, so a third push landing while one preview
# runs and another waits drops the waiting one — that commit gets no preview, but a
# version is never reused.
concurrency:
group: publish-npm-preview
cancel-in-progress: false
steps:
- uses: actions/checkout@v7
with:
# Publish the exact pushed commit, or the explicitly requested manual ref.
ref: ${{ github.event_name == 'workflow_dispatch' && inputs.ref || github.sha }}
# Brings the tags the version calculation reads as its floor. The repo is a
# few megabytes packed, so a full fetch is cheap and sidesteps every
# shallow-clone tag caveat.
fetch-depth: 0
# Setup .npmrc file to publish to npm
- uses: actions/setup-node@v7
with:
node-version: "24"
check-latest: true
registry-url: "https://registry.npmjs.org"
# Ahead of the version bump, so this still validates the committed lockfile.
- run: npm ci
- name: Compute the preview version
id: preview
run: |
version="$(node scripts/next-preview-version.mjs)"
sha="$(git rev-parse HEAD)"
echo "version=$version" >> "$GITHUB_OUTPUT"
echo "sha=$sha" >> "$GITHUB_OUTPUT"
echo "Preview \`$version\` from \`$sha\`" >> "$GITHUB_STEP_SUMMARY"
- name: Apply the version to the working tree only
# Never committed: package.json and .release-please-manifest.json on main stay
# release-please's to own, and this only changes what goes into the tarball.
# `npm version` keeps package-lock.json's version fields in step, and
# --no-git-tag-version skips every git operation.
run: npm version "${{ steps.preview.outputs.version }}" --no-git-tag-version
- name: Publish
id: publish
# prepublishOnly builds the bundle.
#
# --tag is mandatory: npm publish defaults to `latest` even for a semver
# prerelease, which would point every plain `npm install` at a preview.
run: |
npm publish --access public --tag preview
echo "published=true" >> "$GITHUB_OUTPUT"
outputs:
published: ${{ steps.publish.outputs.published }}
version: ${{ steps.preview.outputs.version }}
sha: ${{ steps.preview.outputs.sha }}
publish-tag-preview:
name: Tag the published preview
needs: publish-npm-preview
if: ${{ needs.publish-npm-preview.outputs.published == 'true' }}
runs-on: ubuntu-latest
timeout-minutes: 5
environment: release
permissions:
contents: write # create refs/tags/v<version>
steps:
# A re-run carries over the outputs of jobs it did not re-run. If that ever stops
# holding, fail with the manual recipe rather than tagging the wrong commit.
- name: Check the publish handed over a version and a commit
env:
VERSION: ${{ needs.publish-npm-preview.outputs.version }}
SHA: ${{ needs.publish-npm-preview.outputs.sha }}
run: |
if [ -z "$VERSION" ] || [ -z "$SHA" ]; then
echo "::error::publish-npm-preview reported version='$VERSION' sha='$SHA'." \
"Tag it by hand — see docs/RELEASES.md, 'A preview published but the" \
"commit was not tagged'."
exit 1
fi
- name: Create the tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ needs.publish-npm-preview.outputs.version }}
SHA: ${{ needs.publish-npm-preview.outputs.sha }}
run: |
gh api "repos/$GITHUB_REPOSITORY/git/refs" \
-f ref="refs/tags/v$VERSION" \
-f sha="$SHA"
echo "Tagged \`v$VERSION\` at \`$SHA\`" >> "$GITHUB_STEP_SUMMARY"
trigger-registry-update:
needs: [publish-npm, publish-npm-preview]
# Both channels dispatch this: the registry has its own handling for preview
# versions. The two never fire together — a release merge publishes stable and
# skips the preview, and every other push does the reverse — so the registry sees
# exactly one dispatch per published version. The payload is deliberately
# unchanged: it names no version, and the registry resolves what it needs itself.
if: >-
${{
always() && (
needs.publish-npm.result == 'success'
|| needs.publish-npm-preview.outputs.published == 'true'
|| (github.event_name == 'workflow_dispatch' && inputs.channel == 'stable' && !inputs.publish_npm)
)
}}
runs-on: ubuntu-latest
# npm metadata is normally visible within about five minutes. Leave 2.5x that
# window for propagation, plus enough time to mint the token and dispatch.
timeout-minutes: 15
environment: release
permissions: {}
steps:
- name: Wait for package to become available
if: ${{ needs.publish-npm.result == 'success' || needs.publish-npm-preview.outputs.published == 'true' }}
env:
PACKAGE: "@agentclientprotocol/codex-acp"
VERSION: ${{ needs.publish-npm-preview.outputs.version || needs.publish-npm.outputs.version }}
#language=bash
run: |
if [ -z "$VERSION" ]; then
echo "::error::The publish job did not report a package version."
exit 1
fi
deadline=$((SECONDS + 750))
while (( SECONDS < deadline )); do
if timeout 10s npm pack "$PACKAGE@$VERSION" \
--dry-run \
--ignore-scripts \
--silent \
--registry=https://registry.npmjs.org \
--prefer-online \
--fetch-retries=0 >/dev/null 2>&1; then
echo "$PACKAGE@$VERSION is available from npm."
exit 0
fi
remaining=$((deadline - SECONDS))
if (( remaining > 0 )); then
echo "Waiting for $PACKAGE@$VERSION to become available (${remaining}s remaining)..."
sleep 10
fi
done
echo "::error::$PACKAGE@$VERSION was not available from npm after 12.5 minutes."
exit 1
- name: Generate token scoped to the registry repo
uses: actions/create-github-app-token@v3
id: registry-token
with:
app-id: ${{ secrets.REGISTRY_UPDATER_APP_ID }}
private-key: ${{ secrets.REGISTRY_UPDATER_APP_PRIVATE_KEY }}
owner: ${{ github.repository_owner }}
repositories: registry
- name: Dispatch registry version update
env:
GH_TOKEN: ${{ steps.registry-token.outputs.token }}
run: |
gh workflow run update-versions.yml \
--repo ${{ github.repository_owner }}/registry \
--ref main \
-f apply=true \
-f agents=codex-acp