Skip to content

Commit 2145472

Browse files
kovtcharov-amdOvtcharovkovtcharov
authored
feat(email-release): publish frozen binaries to R2 via rclone (#1688)
## Why this matters Before: the email release workflow targeted the agent-hub Worker `POST /publish` API, and `@amd-gaia/agent-email` shipped with placeholder hashes — not actually installable. After: one `agent-pkg-email-v*` tag freezes all four platforms, uploads them by `rclone` to a **dedicated** `gaia-hub` R2 bucket (served at its own subdomain `hub.amd-gaia.ai`), regenerates `binaries.lock.json` with **real** SHA-256s, fetch-verifies every platform via the real `fetch` CLI, then `npm publish`es via OIDC trusted publishing. This is the last code piece before email v0.1.0 can ship for real (#1685). Distribution model: a plain public GET serves each binary at `https://hub.amd-gaia.ai/agents/python/email/<version>/...`; the SHA-256 in `binaries.lock.json` is the integrity gate the `fetch` CLI enforces (no server-side checksum). Atomic + idempotent: rclone copy is content-addressed by the pre-computed hash, the post-upload fetch-verify fails the release on any mismatch, and `npm publish` is skipped if the version already exists. The hub gets its own bucket + subdomain rather than riding on the videos' `amd-gaia`/`assets.amd-gaia.ai` store — so its API token is scoped to `gaia-hub` only and can never touch the docs assets. `publish_to_r2.py` (the Worker `POST /publish` path) is **kept as a testing helper** — its summary JSON is a valid `--meta` for the lock generator. > Maintainer prerequisites (one-time, before tagging): create the `gaia-hub` R2 bucket and connect the `hub.amd-gaia.ai` custom domain to it; create an Object-Read/Write token scoped to `gaia-hub` and set repo secrets `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` / `R2_ACCOUNT_ID`; optional var `HUB_BASE_URL` (defaults to `https://hub.amd-gaia.ai`); register the npm trusted publisher for `@amd-gaia/agent-email` against `release_agent_email.yml`. First release ships **unsigned** binaries (#732 / #733 / #1650). ## Test plan - [x] `gen_binaries_lock.py` functional round-trip: verbatim `--base-url`, meta-driven win32 entry, placeholders preserved for absent platforms, bad/non-http URL fails loudly - [x] `python -m py_compile` + `black` + `isort` clean on changed Python - [x] `bash -n upload_to_r2.sh` + `release_agent_email.yml` parses as valid YAML - [ ] Dry-run a real `agent-pkg-email-v0.1.0` once the `gaia-hub` bucket + repo secrets are set (CI freezes 4 platforms → R2 → lock → fetch-verify → npm) --------- Co-authored-by: Ovtcharov <kovtchar@amd.com> Co-authored-by: Kalin Ovtcharov <kalin@extropolis.ai>
1 parent 0b3dcc9 commit 2145472

9 files changed

Lines changed: 626 additions & 109 deletions

File tree

.github/workflows/release_agent_email.yml

Lines changed: 107 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -5,31 +5,42 @@
55
# (milestone #49, issue #1648). One namespaced tag does the WHOLE release:
66
#
77
# 1. build — freeze the email REST sidecar into a native one-file binary on
8-
# each of the 4 target platforms (no cross-compile), compute its
9-
# SHA-256 + size, upload the binary + a meta sidecar.
10-
# 2. publish — POST /publish every binary to the Agent Hub R2 Worker (Bearer
11-
# token from a repo secret), regenerate binaries.lock.json with
12-
# the REAL hashes, then `npm publish` via npm trusted publishing
13-
# (OIDC, provenance — NO npm token).
8+
# each of the 4 target platforms (no cross-compile), smoke-test it,
9+
# compute SHA-256 + size, upload the binary + a meta sidecar.
10+
# 2. publish — upload every binary to the GAIA hub R2 bucket (gaia-hub,
11+
# served at hub.amd-gaia.ai) via rclone,
12+
# regenerate binaries.lock.json with the REAL hashes, verify every
13+
# published object is fetchable + hash-matches via the real fetch
14+
# CLI, then `npm publish` via npm trusted publishing (OIDC,
15+
# provenance — NO npm token).
1416
#
15-
# Atomic + idempotent: the Worker computes SHA-256 server-side and rejects any
16-
# attempt to overwrite a published filename, so a re-run of an already-published
17-
# version is a no-op (publish_to_r2.py treats 409-with-identical-bytes as
18-
# success and fails loudly only on a genuine hash divergence). The npm publish
19-
# is skipped if that exact version already exists on the registry.
17+
# Distribution model: the gaia-hub R2 bucket is a public static object store.
18+
# Binaries are served by a plain public
19+
# GET at hub.amd-gaia.ai/...; the SHA-256 in binaries.lock.json is the
20+
# integrity gate the fetch CLI enforces on download (no server-side checksum).
21+
#
22+
# Atomic + idempotent: rclone copy is overwrite-by-key (re-uploading identical
23+
# bytes is a no-op); the post-upload fetch-verify — not rclone's own size/ETag
24+
# skip check — is the real integrity gate and fails the release if any object's
25+
# bytes don't match the lock; npm publish is skipped if that exact version
26+
# already exists on the registry.
2027
#
2128
# Tag namespace: `agent-pkg-email-*` (NOT `v*`). This is the established Agent
2229
# Hub package-share namespace (see build_agent_package.yml) and deliberately
2330
# does NOT fire publish.yml (v* only) or the paused publish_agents.yml (no tag
2431
# trigger). Example: `git tag agent-pkg-email-v0.1.0 && git push origin --tags`.
2532
#
2633
# Maintainer setup (one-time):
27-
# * Repo secret AGENT_HUB_PUBLISH_TOKEN — Bearer token for POST /publish.
28-
# * Repo variable AGENT_HUB_BASE_URL — Worker origin, e.g.
29-
# https://gaia-agent-hub.<acct>.workers.dev (no trailing slash, no /publish).
30-
# * npm trusted publisher for @amd-gaia/agent-email registered against THIS
31-
# workflow file name: release_agent_email.yml (repo amd/gaia). The OIDC
32-
# subject is tied to the exact filename — renaming this file breaks publish.
34+
# Secrets:
35+
# R2_ACCESS_KEY_ID — R2 S3 API token (Object Read & Write on gaia-hub).
36+
# R2_SECRET_ACCESS_KEY — its secret.
37+
# R2_ACCOUNT_ID — Cloudflare account id (for the R2 S3 endpoint).
38+
# Variables:
39+
# HUB_BASE_URL — public origin, default https://hub.amd-gaia.ai
40+
# (set only to override).
41+
# npm trusted publisher for @amd-gaia/agent-email registered against THIS
42+
# workflow file name: release_agent_email.yml (repo amd/gaia). The OIDC subject
43+
# is tied to the exact filename — renaming this file breaks publish.
3344

3445
name: Release Agent (email)
3546

@@ -55,6 +66,9 @@ env:
5566
PKG_DIR: hub/agents/npm/agent-email
5667
MANIFEST: hub/agents/python/email/gaia-agent.yaml
5768
FREEZE_DIST: hub/agents/python/email/packaging/dist
69+
# R2 bucket + key prefix (layout mirrors the GitHub hub/agents/<lang>/<id> tree).
70+
R2_BUCKET: gaia-hub
71+
HUB_PREFIX: agents/email
5872

5973
jobs:
6074
# ── Stage 1: native freeze on every platform ───────────────────────
@@ -144,7 +158,7 @@ jobs:
144158
staging/${{ matrix.platform }}.meta.json
145159
if-no-files-found: error
146160

147-
# ── Stage 2: publish to R2 + npm (single atomic step) ──────────────
161+
# ── Stage 2: upload to R2 + publish to npm (single atomic step) ─────
148162
publish:
149163
name: Publish to R2 + npm
150164
runs-on: ubuntu-latest
@@ -169,40 +183,50 @@ jobs:
169183
- name: Resolve + validate release version
170184
id: ver
171185
shell: bash
186+
# User-controlled inputs go through env (never inlined into the script)
187+
# so a value can't inject shell — GitHub Actions script-injection
188+
# hardening. The tag path uses GITHUB_REF_NAME (also env), and the
189+
# version is still triple-checked against package.json + manifest below.
172190
env:
173191
EVENT_NAME: ${{ github.event_name }}
174-
# Routed through env (never inlined into the script) so an input value
175-
# cannot inject shell — GitHub Actions script-injection hardening.
176192
INPUT_VERSION: ${{ github.event.inputs.version }}
193+
HUB_BASE_URL: ${{ vars.HUB_BASE_URL }}
177194
run: |
178195
set -euo pipefail
179196
if [ "$EVENT_NAME" = "workflow_dispatch" ]; then
180197
VERSION="$INPUT_VERSION"
181198
else
182-
# Strip the agent-pkg-email- prefix and an optional leading 'v'.
183199
TAG="${GITHUB_REF_NAME}"
184200
VERSION="${TAG#agent-pkg-email-}"
185201
VERSION="${VERSION#v}"
186202
fi
187203
PKG_VER="$(node -p "require('./${PKG_DIR}/package.json').version")"
188-
MAN_VER="$(python -c "import yaml,sys; print(yaml.safe_load(open('${MANIFEST}'))['version'])")"
204+
MAN_VER="$(python -c "import yaml; print(yaml.safe_load(open('${MANIFEST}'))['version'])")"
189205
echo "release version: ${VERSION} (package.json=${PKG_VER}, manifest=${MAN_VER})"
190206
if [ "$VERSION" != "$PKG_VER" ] || [ "$VERSION" != "$MAN_VER" ]; then
191207
echo "::error::version mismatch — tag/input=${VERSION}, package.json=${PKG_VER}, gaia-agent.yaml=${MAN_VER}. Align all three before tagging."
192208
exit 1
193209
fi
194210
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
211+
echo "base_url=${HUB_BASE_URL:-https://hub.amd-gaia.ai}/${HUB_PREFIX}/${VERSION}" >> "$GITHUB_OUTPUT"
195212
196-
- name: Assert release config present
213+
- name: Assert R2 credentials present
197214
shell: bash
215+
# Secrets go through env (never inlined into the script text) — same as
216+
# the rclone step — so a value with shell metacharacters can't break
217+
# parsing or defeat log masking.
218+
env:
219+
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
220+
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
221+
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
198222
run: |
199223
set -euo pipefail
200-
if [ -z "${{ vars.AGENT_HUB_BASE_URL }}" ]; then
201-
echo "::error::repo variable AGENT_HUB_BASE_URL is not set (Worker origin, e.g. https://hub.example)."
202-
exit 1
203-
fi
204-
if [ -z "${{ secrets.AGENT_HUB_PUBLISH_TOKEN }}" ]; then
205-
echo "::error::repo secret AGENT_HUB_PUBLISH_TOKEN is not set (Bearer publish token)."
224+
missing=""
225+
[ -z "${R2_ACCESS_KEY_ID:-}" ] && missing="$missing R2_ACCESS_KEY_ID"
226+
[ -z "${R2_SECRET_ACCESS_KEY:-}" ] && missing="$missing R2_SECRET_ACCESS_KEY"
227+
[ -z "${R2_ACCOUNT_ID:-}" ] && missing="$missing R2_ACCOUNT_ID"
228+
if [ -n "$missing" ]; then
229+
echo "::error::missing repo secret(s):$missing — needed to upload to the gaia-hub R2 bucket."
206230
exit 1
207231
fi
208232
@@ -212,56 +236,55 @@ jobs:
212236
pattern: email-agent-*
213237
path: artifacts
214238

215-
- name: Install publish deps
216-
run: python -m pip install --upgrade requests pyyaml
239+
- name: Install deps (PyYAML for the lock generator) + rclone
240+
shell: bash
241+
run: |
242+
set -euo pipefail
243+
python -m pip install --upgrade pyyaml
244+
curl -fsSL https://rclone.org/install.sh | sudo bash
245+
rclone version | head -1
217246
218-
- name: Collect binaries
219-
id: collect
247+
- name: Collect binaries + metas
220248
shell: bash
221249
run: |
222250
set -euo pipefail
223251
mkdir -p bins
224-
# Each artifact dir holds the binary + a <platform>.meta.json.
225252
find artifacts -type f ! -name '*.meta.json' -exec cp {} bins/ \;
226253
find artifacts -name '*.meta.json' -exec cp {} bins/ \;
254+
cp "${MANIFEST}" bins/gaia-agent.yaml
227255
echo "=== collected ==="
228256
ls -la bins/
229-
# Build the repeatable --artifact arg list (path=platform from meta).
230-
ARGS=""
231-
for meta in bins/*.meta.json; do
232-
plat="$(python -c "import json,sys; print(json.load(open(sys.argv[1]))[0]['platform'])" "$meta")"
233-
fn="$(python -c "import json,sys; print(json.load(open(sys.argv[1]))[0]['filename'])" "$meta")"
234-
ARGS="$ARGS --artifact bins/${fn}=${plat}"
235-
done
236-
echo "artifact_args=$ARGS" >> "$GITHUB_OUTPUT"
237257
238-
- name: Publish binaries to R2
239-
env:
240-
AGENT_HUB_PUBLISH_TOKEN: ${{ secrets.AGENT_HUB_PUBLISH_TOKEN }}
241-
AGENT_HUB_BASE_URL: ${{ vars.AGENT_HUB_BASE_URL }}
258+
- name: Upload binaries to R2 (rclone)
242259
shell: bash
260+
env:
261+
# rclone reads the remote 'R2' entirely from env — nothing on disk.
262+
RCLONE_CONFIG_R2_TYPE: s3
263+
RCLONE_CONFIG_R2_PROVIDER: Cloudflare
264+
RCLONE_CONFIG_R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
265+
RCLONE_CONFIG_R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
266+
RCLONE_CONFIG_R2_ENDPOINT: https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com
243267
run: |
244268
set -euo pipefail
245-
# artifact_args is generated by the previous step from the build job's
246-
# own meta files (platform keys + filenames) — intentionally word-split.
247-
python hub/agents/python/email/packaging/publish_to_r2.py \
248-
--base-url "$AGENT_HUB_BASE_URL" \
249-
--manifest "${MANIFEST}" \
250-
${{ steps.collect.outputs.artifact_args }} \
251-
--summary-out published.json
269+
DEST="R2:${R2_BUCKET}/${HUB_PREFIX}/${{ steps.ver.outputs.version }}"
270+
echo "uploading binaries + manifest -> ${DEST}/"
271+
# Upload only the binaries + manifest (not the *.meta.json sidecars).
272+
for f in bins/email-agent-* bins/gaia-agent.yaml; do
273+
[ -f "$f" ] || continue
274+
rclone copyto "$f" "${DEST}/$(basename "$f")" --s3-no-check-bucket
275+
done
276+
echo "=== R2 listing ==="
277+
rclone lsl "${DEST}/" --s3-no-check-bucket
252278
253279
- name: Regenerate binaries.lock.json with real hashes
254280
shell: bash
255-
env:
256-
AGENT_HUB_BASE_URL: ${{ vars.AGENT_HUB_BASE_URL }}
257-
REL_VERSION: ${{ steps.ver.outputs.version }}
258281
run: |
259282
set -euo pipefail
260283
python hub/agents/python/email/packaging/gen_binaries_lock.py \
261-
--base-url "$AGENT_HUB_BASE_URL" \
262-
--version "$REL_VERSION" \
284+
--base-url "${{ steps.ver.outputs.base_url }}" \
285+
--version "${{ steps.ver.outputs.version }}" \
263286
--lock "${PKG_DIR}/binaries.lock.json" \
264-
$(for m in bins/*.meta.json; do echo --summary "$m"; done)
287+
$(for m in bins/*.meta.json; do echo --meta "$m"; done)
265288
echo "=== regenerated lock ==="
266289
cat "${PKG_DIR}/binaries.lock.json"
267290
@@ -271,17 +294,39 @@ jobs:
271294
npm ci
272295
npm run build
273296
297+
- name: Verify every published object via the real fetch CLI
298+
working-directory: ${{ env.PKG_DIR }}
299+
shell: bash
300+
run: |
301+
set -euo pipefail
302+
# Atomicity gate: the public object's bytes must hash to the lock value
303+
# for EVERY platform before we publish the package that points at them.
304+
# Bounded retry: a just-uploaded object can briefly 404 / serve a stale
305+
# negative cache at the Cloudflare edge (hub.amd-gaia.ai) before it
306+
# propagates — that edge is separate from R2's read-after-write store.
307+
# Explicit, caller-requested retry that still fails loudly once exhausted.
308+
for plat in win32-x64 darwin-arm64 darwin-x64 linux-x64; do
309+
echo "--- verifying $plat ---"
310+
attempt=1; max=5
311+
until node dist/cli.js fetch --out "/tmp/verify-$plat" --platform "$plat"; do
312+
if [ "$attempt" -ge "$max" ]; then
313+
echo "::error::fetch-verify failed for $plat after ${max} attempts — the published object never became fetchable/valid at the public origin."
314+
exit 1
315+
fi
316+
echo "attempt ${attempt}/${max} failed; object may not have propagated to the edge yet — retrying in 10s."
317+
attempt=$((attempt + 1)); sleep 10
318+
done
319+
done
320+
274321
- name: Upgrade npm (trusted publishing support)
275322
run: npm install -g npm@latest
276323

277324
- name: Publish to npm (OIDC trusted publishing, provenance)
278325
working-directory: ${{ env.PKG_DIR }}
279326
shell: bash
280-
env:
281-
REL_VERSION: ${{ steps.ver.outputs.version }}
282327
run: |
283328
set -euo pipefail
284-
VERSION="$REL_VERSION"
329+
VERSION="${{ steps.ver.outputs.version }}"
285330
if npm view "@amd-gaia/agent-email@${VERSION}" version >/dev/null 2>&1; then
286331
echo "@amd-gaia/agent-email@${VERSION} already on npm — skipping (idempotent)."
287332
exit 0

docs/plans/package-publishing.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ We adopt **option (b)** from the design discussion:
2929
> desktop installers). The paused `publish_agents.yml` (PyPI wheels, #1179) is
3030
> **left untouched** — the PyPI channel stays paused; the new publisher does
3131
> **npm + binary** first. Agent UI is **promoted to a registry package** so it
32-
> releases on its own cadence. R2 backend is **rclone → `assets.amd-gaia.ai`**.
32+
> releases on its own cadence. R2 backend is **rclone → `hub.amd-gaia.ai`**.
3333
3434
## Core principle: version-as-signal + idempotent publish
3535

@@ -185,10 +185,10 @@ interact with:
185185

186186
## Open items to resolve during implementation
187187

188-
- **R2 backend** — confirm rclone → `assets.amd-gaia.ai` (bucket `amd-gaia`) as
188+
- **R2 backend** — confirm rclone → `hub.amd-gaia.ai` (bucket `gaia-hub`) as
189189
the binary origin; the `binary` channel's upload + the lock `baseUrl` depend on
190190
it. (Repo secrets `R2_ACCESS_KEY_ID` / `R2_SECRET_ACCESS_KEY` /
191-
`R2_ACCOUNT_ID`; var `ASSETS_BASE_URL`.)
191+
`R2_ACCOUNT_ID`; var `HUB_BASE_URL`.)
192192
- **Approval gate**`publish.yml` uses a manual `publish` environment gate. Do
193193
package releases need the same gate, or is the namespaced tag + idempotency
194194
enough? (Recommendation: gate the `npm`/`pypi` publish step only, not the

hub/agents/npm/agent-email/binaries.lock.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
{
22
"schemaVersion": "1.0",
33
"agentVersion": "0.1.0",
4-
"_comment": "Per-platform frozen email-agent binary manifest. baseUrl + sha256 values are PLACEHOLDERS pending real R2 publishing (issue #1648). fetch is intentionally blocked while sha256 is a placeholder so a bad binary can never be trusted. Override baseUrl at fetch time and replace these hashes once artifacts are published. NO binaries are committed to the repo or the npm tarball — this manifest + the fetch CLI pull them at build time.",
5-
"baseUrl": "https://PENDING-R2-BUCKET.example/email-agent/0.1.0",
4+
"baseUrl": "https://hub.amd-gaia.ai/agents/email/0.1.0",
65
"binaries": {
76
"win32-x64": {
87
"filename": "email-agent-win32-x64.exe",
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Manual R2 upload — email agent binaries
2+
3+
How to publish frozen email-agent binaries to the GAIA hub bucket **by hand**
4+
(the "I run rclone myself" path). This produces the exact same objects and lock
5+
as the CI release (`.github/workflows/release_agent_email.yml`) — same bucket,
6+
same `agents/email` prefix, same `hub.amd-gaia.ai` origin — so a hand-upload
7+
and a CI release are interchangeable.
8+
9+
> **One-time rclone setup** (remote name, R2 credentials, endpoint) follows the
10+
> same steps as [`scripts/video-demo/R2-SETUP.md`](../../../../../scripts/video-demo/R2-SETUP.md),
11+
> but targets the dedicated `gaia-hub` bucket (custom domain `hub.amd-gaia.ai`)
12+
> rather than `amd-gaia`/`assets.amd-gaia.ai`. Configure a remote named `gaia`
13+
> against `gaia-hub`. This page assumes that remote exists.
14+
15+
## Layout
16+
17+
Binaries are served by a plain public GET; the SHA-256 in `binaries.lock.json`
18+
is the integrity gate the npm `fetch` CLI enforces (no server-side checksum).
19+
20+
```
21+
R2: gaia-hub/agents/email/<version>/email-agent-<platform>[.exe]
22+
URL: https://hub.amd-gaia.ai/agents/email/<version>/email-agent-<platform>[.exe]
23+
```
24+
25+
Platforms: `win32-x64` (`.exe`), `darwin-arm64`, `darwin-x64`, `linux-x64`.
26+
27+
## One command
28+
29+
Stage the per-platform binaries into a folder named `email-agent-<platform>[.exe]`,
30+
then:
31+
32+
```bash
33+
# from the repo root, with rclone remote 'gaia' configured
34+
hub/agents/python/email/packaging/upload_to_r2.sh 0.1.0 ./staging
35+
```
36+
37+
The script:
38+
39+
1. asserts the version matches `package.json` **and** `gaia-agent.yaml` (fails loudly otherwise),
40+
2. hashes each binary it is about to upload (so the lock can never drift from the bytes),
41+
3. `rclone copyto`s each binary + `gaia-agent.yaml` to `gaia:gaia-hub/agents/email/<version>/`,
42+
4. regenerates `hub/agents/npm/agent-email/binaries.lock.json` with the real hashes.
43+
44+
Absent platforms keep their existing lock entry, so a **Windows-only** hand-upload
45+
won't wipe the mac/linux entries — upload the rest later and rerun.
46+
47+
Env overrides: `R2_REMOTE` (default `gaia`), `R2_BUCKET` (default `gaia-hub`),
48+
`HUB_BASE_URL` (default `https://hub.amd-gaia.ai`).
49+
50+
## Where the binaries come from
51+
52+
PyInstaller does **not** cross-compile, so each platform must be frozen natively
53+
(`freeze.py` — see [`README.md`](README.md)). You can only build `win32-x64` on a
54+
Windows box. For an all-platform release without four machines, let CI build them
55+
and **download its build artifacts**, then rclone them up yourself:
56+
57+
```bash
58+
# download the 4 platform artifacts from a release_agent_email.yml run into ./staging,
59+
# unzip so files are named email-agent-<platform>[.exe], then:
60+
hub/agents/python/email/packaging/upload_to_r2.sh 0.1.0 ./staging
61+
```
62+
63+
This is the clean split: **CI builds, you publish.**
64+
65+
## Verify, then publish npm
66+
67+
```bash
68+
cd hub/agents/npm/agent-email
69+
npm ci && npm run build
70+
node dist/cli.js fetch --out ./verify --platform win32-x64 # --out is required; repeat per platform — downloads + checks SHA-256
71+
npm publish --access public # or let CI do it via OIDC trusted publishing
72+
```
73+
74+
If `fetch` errors with a hash mismatch, the uploaded bytes don't match the lock —
75+
re-run the upload; do **not** hand-edit the lock.
76+
77+
## Manual upload without the script
78+
79+
Equivalent raw commands (the script just wraps these + the hashing + lock regen):
80+
81+
```bash
82+
VER=0.1.0
83+
DEST="gaia:gaia-hub/agents/email/$VER"
84+
# --exclude '*.json' keeps the *.meta.json sidecars out of the public dir.
85+
rclone copy ./staging/ "$DEST/" --s3-no-check-bucket --progress --exclude '*.json'
86+
# Upload the manifest too, so the hand path matches CI byte-for-byte.
87+
rclone copyto hub/agents/python/email/gaia-agent.yaml "$DEST/gaia-agent.yaml" \
88+
--s3-no-check-bucket
89+
python hub/agents/python/email/packaging/gen_binaries_lock.py \
90+
--base-url "https://hub.amd-gaia.ai/agents/email/$VER" \
91+
--version "$VER" --lock hub/agents/npm/agent-email/binaries.lock.json \
92+
--meta staging/<platform>.meta.json # one --meta per platform
93+
```
94+
95+
`--s3-no-check-bucket` is required for Object-Read/Write tokens (no bucket-create
96+
permission) — see [`R2-SETUP.md`](../../../../../scripts/video-demo/R2-SETUP.md).

0 commit comments

Comments
 (0)