Build and publish the CLI from a terminal, refusing what it cannot prove

a38b09aa5a08 · AtlantisPleb · · parent 1f1a590141ea

Build and publish the CLI from a terminal, refusing what it cannot prove

The installer at openagents.com/install.sh names three shapes under
/releases: a channel pointer, an artifact per platform, and a sums file.
Nothing produced those shapes, so the installer described a contract no
release filled.

This is the producing half, as a script an operator runs. It builds the five
platforms the installer detects -- macOS on both architectures natively, and
Linux on both plus Windows through cargo-zigbuild -- then signs and notarizes
the macOS artifacts, writes SHA256SUMS, and publishes to the release bucket.

Cross-compilation fails in ways that leave a runnable file at the output path:
a stale artifact from an earlier target, or a host build that ignored the
triple it was handed. So every artifact is read back and its actual Mach-O,
ELF, or PE header matched against the header that platform must have, before
it is staged. Shipping a darwin binary under a linux-x86_64 name would be
discovered by a reader whose kernel refuses the file, which is the wrong
person to find out.

The other refusals share that shape. A build that covers three of five
platforms exits rather than publishing, because the installer reports a bare
download failure rather than "unsupported platform" and the gap reads as an
outage to everyone who is not the operator. A prerelease cannot claim a
channel without saying so, so that the default channel keeps meaning a real
release. A macOS artifact that did not sign, or that came back from Apple as
anything but Accepted, stops the run.

Two details of the installer's contract are asymmetric and easy to get wrong
by hand, so the script derives both: the artifact URL carries no extension on
any platform, while the sums entry for Windows carries .exe, because the
installer appends that suffix to the name it searches for after downloading a
URL without one. Changing either alone breaks every Windows install.

macOS artifacts ship bare rather than zipped. Apple cannot staple a ticket to
a bare Mach-O -- stapler fails with error 73 -- but the installer downloads
one file and chmods it, and curl sets no quarantine attribute, so a stapled
container would change a landed contract to buy something this path never
consults. They are notarized regardless, so the paths where quarantine does
apply still pass; spctl reports source=Notarized Developer ID against the
published binary, and the script prints that assessment on every macOS build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SoZMfWRSGnf6FZX2Ar9rQ2
Co-Authored-By
Claude Fable 5 <noreply@anthropic.com>

Deploy story

What this commit did to the running system — joined from the forge receipt chain, the part a commit page elsewhere cannot show.

pushed
by user · WAL seq 178 · 2026-08-26T00:32:09.460017Z

Changed files

  • added docs/ops/2026-08-25-cli-release-runbook.md
  • added ops/release-cli.sh

Diff

2 files changed, +532 -0

docs/ops/2026-08-25-cli-release-runbook.md added +159

@@ -0,0 +1,159 @@

1
# Release the OpenAgents CLI
2
3
`ops/release-cli.sh` builds the `openagents-cli` crate for every platform the
4
public installer knows how to ask for, signs and notarizes the macOS artifacts,
5
writes a checksum file, and publishes the result to the release bucket. An
6
operator runs it from a terminal. There is no hosted build service and no
7
GitHub Actions workflow.
8
9
## What the installer expects
10
11
The installer served at `https://openagents.com/install.sh` is the contract.
12
Under a base URL of `https://openagents.com/releases` it fetches three shapes:
13
14
| Path | Body |
15
| --- | --- |
16
| `<base>/<channel>` | A bare version string, such as `0.1.0` |
17
| `<base>/openagents-<version>-<platform>` | The executable |
18
| `<base>/SHA256SUMS-<version>` | One `<sha256>  <name>` line per platform |
19
20
The platform strings are `macos-aarch64`, `macos-x86_64`, `linux-x86_64`,
21
`linux-aarch64`, and `windows-x86_64`.
22
23
Two details of that contract are easy to get wrong, so the script derives both
24
rather than leaving them to a human:
25
26
- The artifact URL never carries a file extension, on any platform. The
27
  installer computes the URL before it branches on Windows and never revisits
28
  it.
29
- The `SHA256SUMS` entry for Windows *does* carry `.exe`, because the installer
30
  appends that suffix to the name it searches for. The published object and the
31
  checksum line therefore disagree by design. Changing either one alone breaks
32
  every Windows install with a checksum mismatch.
33
34
## Prerequisites
35
36
- A Rust toolchain with the five targets installed. Add a missing one with
37
  `rustup target add <triple>`.
38
- `cargo-zigbuild` and `zig`, which cross-compile the Linux and Windows targets
39
  from macOS.
40
- `gcloud`, for publishing.
41
- For macOS artifacts, the Developer ID Application certificate in the login
42
  keychain and an App Store Connect API key. The script reads the key id,
43
  issuer, private key path, and signing identity from the operator secret file
44
  named by `OPENAGENTS_NOTARY_ENV`.
45
46
## Build without publishing
47
48
Start here. The script stages everything under `dist/releases/<version>/`,
49
which is ignored by Git, and reports what it built:
50
51
```sh
52
ops/release-cli.sh --version 0.1.0-rc.1
53
```
54
55
Restrict the attempt to a subset while you iterate:
56
57
```sh
58
ops/release-cli.sh --version 0.1.0-rc.1 --targets "macos-aarch64 linux-x86_64"
59
```
60
61
## Publish
62
63
```sh
64
ops/release-cli.sh --version 0.1.0-rc.1 --publish
65
```
66
67
Publishing uploads the artifacts and the checksum file. It does not move any
68
channel, so nothing that resolves `stable` sees the new version yet.
69
70
Point a channel at a version only when you intend readers to receive it:
71
72
```sh
73
ops/release-cli.sh --version 0.1.0 --publish --channel stable
74
```
75
76
## What the script refuses
77
78
Each refusal exists because the failure it prevents is silent.
79
80
**A binary that does not match its platform name.** After every build the
81
script reads the produced file's actual Mach-O, ELF, or PE header and compares
82
it against the header that platform must have. A cross-compilation that fails
83
while leaving a stale or host-native binary at the output path would otherwise
84
publish a macOS executable under a `linux-x86_64` name, and the first person to
85
learn about it would be a reader whose install produced a file their kernel
86
refuses to run.
87
88
**A partial release, unless you say so.** If any requested platform fails to
89
build, the script names exactly which ones and exits rather than publishing.
90
The installer reports a bare download failure, not "unsupported platform", so a
91
channel pointing at a version that only covers three platforms looks to
92
everyone else like an outage. Pass `--allow-partial` when a partial release is
93
what you actually want.
94
95
**A prerelease claiming a channel.** A version with a suffix, such as
96
`0.1.0-rc.1`, cannot become the target of a channel without
97
`--allow-prerelease-channel`. Rehearsals publish release candidates; the
98
channel a reader resolves by default should keep meaning a real release.
99
100
**An unsigned or unnotarized macOS artifact.** Signing failure, a missing
101
secret file, or a notarization result other than `Accepted` stops the run. Pass
102
`--skip-notarization` to build macOS artifacts without submitting them, which
103
is useful while iterating and is recorded as `skipped` in the manifest.
104
105
## Why macOS artifacts ship bare rather than zipped
106
107
Apple cannot staple a notarization ticket to a bare Mach-O executable. Only a
108
container such as a `.zip`, `.dmg`, or `.pkg` carries a stapled ticket, and
109
`xcrun stapler staple` on a bare binary fails with error 73.
110
111
The artifact still ships bare, for two reasons. The installer downloads a
112
single file and marks it executable, so shipping a container would mean
113
changing a landed contract. And the thing stapling buys — offline Gatekeeper
114
verification — is not consulted on this install path at all, because `curl`
115
sets no `com.apple.quarantine` attribute on what it writes.
116
117
The artifacts are signed and notarized anyway, so that the paths where
118
quarantine *does* apply still succeed. `spctl --assess -t install` reports
119
`source=Notarized Developer ID` against the published binary, which is Apple's
120
online lookup of the ticket recorded for that code directory hash. The script
121
prints that assessment during every macOS build.
122
123
## Credential handling
124
125
The script passes the App Store Connect key, key id, and issuer to
126
`notarytool` on each invocation rather than storing a `notarytool` keychain
127
profile. `notarytool store-credentials` copies the private key into the login
128
keychain, which would leave a second durable copy of a credential that already
129
exists on disk. Reading the operator secret file at call time leaves nothing
130
new behind.
131
132
Never print the key, its contents, or a `.p12` password into a log, a commit
133
message, or an issue comment. Reference them by path and variable name.
134
135
## The build manifest
136
137
Every run writes `dist/releases/<version>/release-manifest.json` recording, per
138
platform, the Rust target triple, the builder, the SHA-256, the byte count, the
139
notarization status, and the notarization submission id. It also records the
140
Git commit the artifacts were built from. Keep it with the release record; it
141
is the evidence for what shipped.
142
143
## Verifying a published release by hand
144
145
```sh
146
curl -fsSL https://openagents.com/releases/SHA256SUMS-0.1.0-rc.1
147
curl -fsSL -o oa https://openagents.com/releases/openagents-0.1.0-rc.1-macos-aarch64
148
shasum -a 256 oa
149
codesign -dv --verbose=4 oa
150
spctl --assess -vv -t install oa
151
```
152
153
## Known gap: the embedded version
154
155
The `--version` value names the release, but the version the binary reports
156
comes from the `openagents-cli` crate manifest. A release candidate built from
157
a crate at `0.1.0` installs correctly and then reports `0.1.0` rather than
158
`0.1.0-rc.1`. The two agree for a real release and diverge for a rehearsal.
159
Closing this means threading the release version into the build.
ops/release-cli.sh added +373

@@ -0,0 +1,373 @@

1
#!/bin/sh
2
# Build, sign, and publish the OpenAgents CLI for every platform the installer
3
# knows how to ask for.
4
#
5
# The contract this script fills is written down in the installer served at
6
# https://openagents.com/install.sh. Under the base URL
7
# https://openagents.com/releases it fetches:
8
#
9
#   <base>/<channel>                              a bare version string
10
#   <base>/openagents-<version>-<platform>        the executable, no extension
11
#   <base>/SHA256SUMS-<version>                   "<sha256>  <name>" per line
12
#
13
# Anything this script emits that disagrees with those three shapes is a broken
14
# release, so the names are derived here once and never spelled twice.
15
#
16
# Usage:
17
#   ops/release-cli.sh --version 0.1.0-rc.1
18
#   ops/release-cli.sh --version 0.1.0-rc.1 --publish
19
#   ops/release-cli.sh --version 0.1.0 --publish --channel stable
20
#
21
# Options:
22
#   --version X.Y.Z[-suffix]  Required. The version to build and name.
23
#   --targets "a b c"         Platforms to attempt. Defaults to all five.
24
#   --publish                 Upload to the release bucket. Off by default.
25
#   --channel NAME            Point a channel at this version after publishing.
26
#   --allow-partial           Publish even though some platforms are missing.
27
#   --allow-prerelease-channel  Let a prerelease version claim a channel.
28
#   --skip-notarization       Build macOS artifacts without Apple notarization.
29
30
set -eu
31
32
script_dir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
33
repo_root=$(CDPATH= cd -- "$script_dir/.." && pwd)
34
35
# platform | rust target triple | builder | expected `file` signature
36
#
37
# The fourth column is the anti-forgery check. Cross-compilation fails in ways
38
# that still leave a runnable file sitting at the output path -- a stale
39
# artifact from a previous target, or a host build that silently ignored the
40
# requested triple -- and a release that ships a darwin binary under a
41
# linux-x86_64 name is worse than one that admits it built four of five. Every
42
# artifact is read back and matched against this signature before it is staged.
43
platform_table='macos-aarch64|aarch64-apple-darwin|cargo|Mach-O 64-bit executable arm64
44
macos-x86_64|x86_64-apple-darwin|cargo|Mach-O 64-bit executable x86_64
45
linux-x86_64|x86_64-unknown-linux-gnu|zigbuild|ELF 64-bit LSB*x86-64
46
linux-aarch64|aarch64-unknown-linux-gnu|zigbuild|ELF 64-bit LSB*ARM aarch64
47
windows-x86_64|x86_64-pc-windows-gnu|zigbuild|PE32+ executable*x86-64'
48
49
all_platforms=$(printf '%s\n' "$platform_table" | cut -d'|' -f1 | tr '\n' ' ')
50
51
version=''
52
targets=''
53
publish=0
54
channel=''
55
allow_partial=0
56
allow_prerelease_channel=0
57
skip_notarization=0
58
59
bucket=${OPENAGENTS_RELEASES_BUCKET:-openagentsgemini-cli-releases}
60
gcloud_config=${CLOUDSDK_CONFIG:-/Users/christopherdavid/work/.secrets/gcloud-sa-config}
61
notary_env=${OPENAGENTS_NOTARY_ENV:-/Users/christopherdavid/work/.secrets/appstoreconnect.env}
62
63
# The code signing identifier is pinned rather than derived from the file name.
64
# codesign defaults the identifier to the basename, which would give the same
65
# build a different identity depending on where it was staged.
66
signing_identifier='com.openagents.cli'
67
68
die() {
69
  echo "$@" >&2
70
  exit 1
71
}
72
73
while [ $# -gt 0 ]; do
74
  case "$1" in
75
    --version) version=${2:-}; shift 2 ;;
76
    --targets) targets=${2:-}; shift 2 ;;
77
    --channel) channel=${2:-}; shift 2 ;;
78
    --publish) publish=1; shift ;;
79
    --allow-partial) allow_partial=1; shift ;;
80
    --allow-prerelease-channel) allow_prerelease_channel=1; shift ;;
81
    --skip-notarization) skip_notarization=1; shift ;;
82
    -h | --help) sed -n '2,28p' "$0" | sed 's/^# \{0,1\}//'; exit 0 ;;
83
    *) die "unknown option: $1" ;;
84
  esac
85
done
86
87
[ -n "$version" ] || die "--version is required"
88
89
# The same grammar the installer applies to its argument. A version this script
90
# accepts but the installer rejects would publish an artifact nobody can ask for.
91
printf '%s' "$version" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9._]+)?$' ||
92
  die "invalid version: $version (expected X.Y.Z or X.Y.Z-suffix)"
93
94
case "$version" in
95
  *-*) prerelease=1 ;;
96
  *) prerelease=0 ;;
97
esac
98
99
[ -n "$targets" ] || targets=$all_platforms
100
101
for command_name in cargo file shasum; do
102
  command -v "$command_name" >/dev/null 2>&1 ||
103
    die "$command_name is required to build a release"
104
done
105
106
dist="$repo_root/dist/releases/$version"
107
rm -rf "$dist"
108
mkdir -p "$dist"
109
110
built=''
111
missing=''
112
manifest_entries=''
113
114
notary_loaded=0
115
load_notary_env() {
116
  [ "$notary_loaded" = 0 ] || return 0
117
  [ -f "$notary_env" ] || return 1
118
  # The App Store Connect key id, issuer, and private key path are read from the
119
  # operator's secret file at call time and passed to notarytool per invocation.
120
  # `notarytool store-credentials` would copy the same private key into the
121
  # login keychain, leaving a second durable copy of a credential that already
122
  # exists on disk; passing it per call leaves nothing new behind.
123
  set -a
124
  # shellcheck disable=SC1090
125
  . "$notary_env"
126
  set +a
127
  notary_loaded=1
128
  return 0
129
}
130
131
# Sign, notarize, and verify one macOS artifact.
132
#
133
# Apple cannot staple a notarization ticket to a bare Mach-O executable -- only
134
# a container such as a .zip, .dmg, or .pkg carries the stapled ticket -- and
135
# `xcrun stapler staple` on a bare binary fails with error 73. The installer
136
# downloads a single executable and marks it executable, so a stapled container
137
# would mean changing a landed contract to buy something the install path does
138
# not use: curl sets no com.apple.quarantine attribute, so Gatekeeper is not
139
# consulted on a piped install at all. The artifact therefore ships bare, and
140
# `spctl --assess -t install` confirms Apple's notary service recognizes it
141
# online as "Notarized Developer ID" for the paths where quarantine does apply.
142
sign_and_notarize() {
143
  artifact=$1
144
  platform=$2
145
146
  command -v codesign >/dev/null 2>&1 || die "codesign is required for $platform"
147
148
  load_notary_env ||
149
    die "missing $notary_env; macOS artifacts must be signed. Pass --skip-notarization to build unsigned."
150
151
  [ -n "${OA_DEVELOPER_ID_APPLICATION:-}" ] ||
152
    die "OA_DEVELOPER_ID_APPLICATION is not set in $notary_env"
153
154
  echo "  signing $platform as $signing_identifier"
155
  codesign --force --timestamp --options runtime \
156
    --identifier "$signing_identifier" \
157
    --sign "$OA_DEVELOPER_ID_APPLICATION" \
158
    "$artifact" >/dev/null 2>&1 ||
159
    die "codesign failed for $platform"
160
161
  codesign --verify --strict "$artifact" ||
162
    die "signature does not verify for $platform"
163
164
  if [ "$skip_notarization" = 1 ]; then
165
    echo "  skipping notarization for $platform (--skip-notarization)"
166
    notary_status='skipped'
167
    notary_submission=''
168
    return 0
169
  fi
170
171
  command -v xcrun >/dev/null 2>&1 || die "xcrun is required to notarize $platform"
172
173
  submission_zip="$artifact.notarize.zip"
174
  rm -f "$submission_zip"
175
  /usr/bin/ditto -c -k --keepParent "$artifact" "$submission_zip"
176
177
  echo "  notarizing $platform"
178
  notary_log="$artifact.notary.log"
179
  xcrun notarytool submit "$submission_zip" \
180
    --key "$ASC_API_PRIVATE_KEY_PATH" \
181
    --key-id "$ASC_API_KEY_ID" \
182
    --issuer "$ASC_API_ISSUER_ID" \
183
    --wait --timeout 30m >"$notary_log" 2>&1 ||
184
    { cat "$notary_log" >&2; die "notarization failed for $platform"; }
185
186
  notary_submission=$(awk '/^  id: /{print $2; exit}' "$notary_log")
187
  notary_status=$(awk '/^  status: /{print $2; exit}' "$notary_log")
188
  rm -f "$submission_zip"
189
190
  [ "$notary_status" = "Accepted" ] ||
191
    { cat "$notary_log" >&2; die "notarization for $platform came back $notary_status"; }
192
193
  echo "  notarized $platform ($notary_submission, $notary_status)"
194
195
  # Evidence, not ceremony: this is the assessment an operator would run by hand
196
  # to answer "will Gatekeeper accept this".
197
  spctl --assess -vv -t install "$artifact" 2>&1 | sed 's/^/    /'
198
}
199
200
echo "Building OpenAgents CLI $version"
201
echo
202
203
for platform in $targets; do
204
  row=$(printf '%s\n' "$platform_table" | grep "^$platform|") ||
205
    die "unknown platform: $platform (known: $all_platforms)"
206
207
  triple=$(printf '%s' "$row" | cut -d'|' -f2)
208
  builder=$(printf '%s' "$row" | cut -d'|' -f3)
209
  expected=$(printf '%s' "$row" | cut -d'|' -f4)
210
211
  echo "$platform ($triple, $builder)"
212
213
  if ! rustup target list --installed 2>/dev/null | grep -qx "$triple"; then
214
    echo "  SKIP: rust target $triple is not installed (rustup target add $triple)"
215
    missing="$missing $platform"
216
    continue
217
  fi
218
219
  if [ "$builder" = zigbuild ] && ! command -v cargo-zigbuild >/dev/null 2>&1; then
220
    echo "  SKIP: cargo-zigbuild is not installed and $triple cannot be built natively here"
221
    missing="$missing $platform"
222
    continue
223
  fi
224
225
  # Every artifact is rebuilt from source into its own target directory. Nothing
226
  # is copied forward from a previous run, so a build that fails cannot leave a
227
  # stale binary behind for the verification step to bless.
228
  case "$triple" in
229
    *windows*) output="$repo_root/target/$triple/release/oa.exe" ;;
230
    *) output="$repo_root/target/$triple/release/oa" ;;
231
  esac
232
  rm -f "$output"
233
234
  build_log="$dist/$platform.build.log"
235
  if [ "$builder" = zigbuild ]; then
236
    build_command='cargo zigbuild'
237
  else
238
    build_command='cargo build'
239
  fi
240
241
  if ! (cd "$repo_root" && $build_command --release -p openagents-cli --target "$triple") \
242
    >"$build_log" 2>&1; then
243
    echo "  SKIP: build failed (see $build_log)"
244
    tail -5 "$build_log" | sed 's/^/    /'
245
    missing="$missing $platform"
246
    continue
247
  fi
248
249
  [ -f "$output" ] || { echo "  SKIP: build reported success but produced no binary"; missing="$missing $platform"; continue; }
250
251
  # The forgery check. `file` reads the actual Mach-O/ELF/PE header rather than
252
  # trusting the path the compiler was asked to write to.
253
  signature=$(file -b "$output")
254
  # shellcheck disable=SC2254
255
  case "$signature" in
256
    $expected*) ;;
257
    *)
258
      echo "  REFUSED: $output is '$signature', expected '$expected'"
259
      echo "  Refusing to publish a binary under a platform name it does not match."
260
      missing="$missing $platform"
261
      continue
262
      ;;
263
  esac
264
265
  # The installer never appends an extension to the artifact URL, on any
266
  # platform: it computes artifact_base before it branches on windows and never
267
  # revisits it. The staged file name matches that URL exactly.
268
  artifact="$dist/openagents-$version-$platform"
269
  cp "$output" "$artifact"
270
  chmod +x "$artifact"
271
272
  notary_status='not-applicable'
273
  notary_submission=''
274
  case "$platform" in
275
    macos-*) sign_and_notarize "$artifact" "$platform" ;;
276
  esac
277
278
  sha=$(shasum -a 256 "$artifact" | awk '{print $1}')
279
  size=$(wc -c <"$artifact" | tr -d ' ')
280
  echo "  ok  $signature"
281
  echo "      sha256 $sha  ($size bytes)"
282
283
  built="$built $platform"
284
  manifest_entries="$manifest_entries
285
    {\"platform\": \"$platform\", \"target\": \"$triple\", \"builder\": \"$builder\", \"sha256\": \"$sha\", \"bytes\": $size, \"notarization\": \"$notary_status\", \"notarization_submission\": \"$notary_submission\"},"
286
  echo
287
done
288
289
[ -n "$built" ] || die "no platform built; nothing to publish"
290
291
# The sums file the installer reads. Its lookup is
292
#   awk '$2 == name || $2 == "*" name'
293
# over "<sha256>  <name>", and for windows it appends ".exe" to the name it
294
# looks up even though it downloaded a URL without one. That asymmetry lives in
295
# a landed installer, so the sums file reproduces it exactly: the artifact keeps
296
# its extensionless name and the sums entry carries the .exe the installer will
297
# search for. Diverging here would checksum-fail every Windows install.
298
sums="$dist/SHA256SUMS-$version"
299
: >"$sums"
300
for platform in $built; do
301
  artifact="openagents-$version-$platform"
302
  sums_name=$artifact
303
  case "$platform" in
304
    windows-*) sums_name="$artifact.exe" ;;
305
  esac
306
  sha=$(shasum -a 256 "$dist/$artifact" | awk '{print $1}')
307
  printf '%s  %s\n' "$sha" "$sums_name" >>"$sums"
308
done
309
310
cat >"$dist/release-manifest.json" <<EOF
311
{
312
  "schema": "openagents.cli-release.v1",
313
  "version": "$version",
314
  "built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
315
  "git_sha": "$(git -C "$repo_root" rev-parse --verify HEAD)",
316
  "host": "$(uname -sm)",
317
  "artifacts": [$(printf '%s' "$manifest_entries" | sed '$ s/,$//')
318
  ]
319
}
320
EOF
321
322
echo "Built:  $(printf '%s' "$built" | tr -s ' ')"
323
if [ -n "$missing" ]; then
324
  echo "Missing:$missing"
325
fi
326
echo "Staged in $dist"
327
echo
328
329
if [ -n "$missing" ] && [ "$allow_partial" = 0 ]; then
330
  echo "This release is missing:$missing" >&2
331
  echo "" >&2
332
  echo "A channel that points at a version some platforms cannot install is a" >&2
333
  echo "broken channel, and the installer reports a bare download failure rather" >&2
334
  echo "than 'unsupported platform'. Fix the toolchain, restrict --targets to" >&2
335
  echo "what you meant to ship, or pass --allow-partial to publish anyway." >&2
336
  exit 1
337
fi
338
339
if [ "$publish" = 0 ]; then
340
  echo "Not publishing (--publish was not passed)."
341
  exit 0
342
fi
343
344
command -v gcloud >/dev/null 2>&1 || die "gcloud is required to publish"
345
CLOUDSDK_CONFIG=$gcloud_config
346
export CLOUDSDK_CONFIG
347
348
echo "Publishing to gs://$bucket"
349
for platform in $built; do
350
  artifact="openagents-$version-$platform"
351
  gcloud storage cp "$dist/$artifact" "gs://$bucket/$artifact" \
352
    --content-type=application/octet-stream --quiet
353
done
354
gcloud storage cp "$sums" "gs://$bucket/SHA256SUMS-$version" \
355
  --content-type=text/plain --quiet
356
357
echo "Published $version"
358
359
[ -n "$channel" ] || { echo "No channel updated (--channel was not passed)."; exit 0; }
360
361
if [ "$prerelease" = 1 ] && [ "$allow_prerelease_channel" = 0 ]; then
362
  die "refusing to point '$channel' at prerelease $version; pass --allow-prerelease-channel if that is the intent"
363
fi
364
365
# The pointer file is the whole body, no trailing newline beyond the one the
366
# installer strips with tr -d '[:space:]'.
367
pointer=$(mktemp)
368
printf '%s\n' "$version" >"$pointer"
369
gcloud storage cp "$pointer" "gs://$bucket/$channel" \
370
  --content-type=text/plain --cache-control='public, max-age=60' --quiet
371
rm -f "$pointer"
372
373
echo "Channel '$channel' now points at $version"

This page updates live while a promote is in flight · changelog