The rehearsal hardcoded `cargo build -p clawmates-bundler`, so it died with "cargo: command not found" on the release runner — gw-04 builds Rust inside a container and has no toolchain of its own. The release job had already built the bundler two steps earlier, so it was also redundant work. CLAWMATES_BUNDLER now short-circuits that build when it points at an executable, falling back to cargo otherwise, so running the script by hand is unchanged. Everything before this step already passed on the runner: images built, SBOMs generated, bundle assembled and signed, and "bundle OK: 94 artifacts verified offline" inside a --network none container. Co-Authored-By: Claude Opus 5 <[email protected]>
200 lines
9.5 KiB
YAML
200 lines
9.5 KiB
YAML
# Release: build the images both deploy targets share, assemble the SIGNED
|
|
# air-gapped bundle, verify it offline, rehearse the customer's install, and
|
|
# attach everything to the Gitea release for the tag.
|
|
#
|
|
# Moved from .github/workflows/ and rewritten for this forge. The old copy could
|
|
# never have run: `runs-on: ubuntu-latest` matches no runner here, and
|
|
# `softprops/action-gh-release` talks to GitHub's API, not Gitea's.
|
|
#
|
|
# The signing key is a repo secret (BUNDLE_SIGNING_KEY, hex ed25519 from
|
|
# `clawmates-bundler keygen`). The matching PUBLIC key is published out of band
|
|
# so customers can verify a bundle before `docker load`.
|
|
name: release
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
workflow_dispatch:
|
|
|
|
jobs:
|
|
bundle:
|
|
runs-on: gw04
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Version from tag
|
|
run: |
|
|
# workflow_dispatch has no tag; fall back to the short sha so a manual
|
|
# run produces a clearly-not-a-release version rather than an empty one.
|
|
if [ "${GITHUB_REF_TYPE:-}" = "tag" ]; then
|
|
echo "VERSION=${GITHUB_REF_NAME#v}" >> "$GITHUB_ENV"
|
|
else
|
|
echo "VERSION=0.0.0-$(git rev-parse --short HEAD)" >> "$GITHUB_ENV"
|
|
fi
|
|
|
|
- name: Build images
|
|
run: |
|
|
set -eu
|
|
docker build -t "clawmates/server:$VERSION" -f images/server.Dockerfile .
|
|
docker build -t "clawmates/frontend:$VERSION" -f images/frontend.Dockerfile .
|
|
docker build -t "clawmates/broker:$VERSION" -f images/broker.Dockerfile .
|
|
docker build -t "clawmates/agent-base:$VERSION" images/agent-base
|
|
docker build -t "clawmates/agent-browser:$VERSION" images/agent-browser
|
|
docker pull -q postgres:16-alpine
|
|
docker pull -q tecnativa/docker-socket-proxy:0.3
|
|
|
|
# syft goes in the workspace, NOT /usr/local/bin. The host executor runs
|
|
# as root on the production gateway; a release should not leave binaries
|
|
# behind on it.
|
|
- name: SBOMs for every shipped image
|
|
run: |
|
|
set -eu
|
|
mkdir -p dist/sboms .tools
|
|
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh \
|
|
| sh -s -- -b .tools
|
|
for image in server frontend broker agent-base agent-browser; do
|
|
./.tools/syft "clawmates/$image:$VERSION" -o spdx-json \
|
|
> "dist/sboms/$image.spdx.json"
|
|
done
|
|
|
|
- name: Save image tarballs
|
|
run: |
|
|
set -eu
|
|
mkdir -p dist/images
|
|
docker save "clawmates/server:$VERSION" -o dist/images/server.tar
|
|
docker save "clawmates/frontend:$VERSION" -o dist/images/frontend.tar
|
|
docker save "clawmates/broker:$VERSION" -o dist/images/broker.tar
|
|
docker save "clawmates/agent-base:$VERSION" -o dist/images/agent-base.tar
|
|
docker save "clawmates/agent-browser:$VERSION" -o dist/images/agent-browser.tar
|
|
docker save tecnativa/docker-socket-proxy:0.3 -o dist/images/socket-proxy.tar
|
|
docker save postgres:16-alpine -o dist/images/postgres.tar
|
|
du -sh dist/images
|
|
|
|
# gw-04 has no cargo, so the bundler builds in a container — same pattern
|
|
# and same cache volumes as deploy.yml. The forge credential is here
|
|
# because cargo resolves the whole workspace, which includes cm-brain's
|
|
# private clawhdf5 git dependency.
|
|
- name: Build bundler
|
|
run: |
|
|
docker run --rm \
|
|
-v "$PWD":/w -w /w \
|
|
-v cm-ci-cargo-registry:/usr/local/cargo/registry \
|
|
-v cm-ci-cargo-git:/usr/local/cargo/git \
|
|
-v cm-ci-target:/w/target \
|
|
-e SQLX_OFFLINE=true -e CARGO_NET_GIT_FETCH_WITH_CLI=true \
|
|
-e FORGE_TOKEN='${{ secrets.FORGE_TOKEN }}' \
|
|
rust:1.96-slim \
|
|
sh -c 'set -e
|
|
apt-get update -qq
|
|
apt-get install -y -qq pkg-config libssl-dev cmake git >/dev/null
|
|
git config --global url."https://oauth2:[email protected]/".insteadOf "https://git.redclaw.dev/"
|
|
cargo build --release -p clawmates-bundler
|
|
# Copy the binary OUT of the target volume and into the workspace.
|
|
# /w/target is a named docker volume, so anything left there is
|
|
# invisible to later steps running on the host — which is exactly
|
|
# how this failed the first time (exit 127, No such file).
|
|
mkdir -p /w/.tools
|
|
cp target/release/clawmates-bundler /w/.tools/clawmates-bundler'
|
|
test -x .tools/clawmates-bundler || { echo "bundler did not land in the workspace"; exit 1; }
|
|
|
|
- name: Assemble and sign the bundle
|
|
env:
|
|
BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }}
|
|
run: |
|
|
set -eu
|
|
test -n "$BUNDLE_SIGNING_KEY" || { echo "BUNDLE_SIGNING_KEY is empty"; exit 1; }
|
|
umask 077
|
|
printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key
|
|
BUNDLER=.tools/clawmates-bundler
|
|
ARTIFACTS=""
|
|
for tar in dist/images/*.tar; do
|
|
ARTIFACTS="$ARTIFACTS $tar=images/$(basename "$tar")"
|
|
done
|
|
for migration in migrations/*.sql; do
|
|
ARTIFACTS="$ARTIFACTS $migration=migrations/$(basename "$migration")"
|
|
done
|
|
# shellcheck disable=SC2086
|
|
"$BUNDLER" assemble dist/bundle "$VERSION" /tmp/release.key \
|
|
deploy/compose/docker-compose.yml=compose/docker-compose.yml \
|
|
deploy/compose/clawmates.toml=compose/clawmates.toml \
|
|
deploy/compose/.env.example=compose/.env.example \
|
|
deploy/e2e/scenarios.toml=compose/scenarios.toml \
|
|
images/seccomp/agent-profile.json=seccomp/agent-profile.json \
|
|
deploy/airgapped/install.sh=install.sh \
|
|
"$BUNDLER"=bin/clawmates-bundler \
|
|
dist/sboms/server.spdx.json=sboms/server.spdx.json \
|
|
dist/sboms/frontend.spdx.json=sboms/frontend.spdx.json \
|
|
dist/sboms/agent-base.spdx.json=sboms/agent-base.spdx.json \
|
|
dist/sboms/agent-browser.spdx.json=sboms/agent-browser.spdx.json \
|
|
$ARTIFACTS
|
|
chmod +x dist/bundle/bin/clawmates-bundler dist/bundle/install.sh
|
|
rm -f /tmp/release.key
|
|
|
|
- name: Verify the bundle offline (public key only)
|
|
env:
|
|
BUNDLE_SIGNING_KEY: ${{ secrets.BUNDLE_SIGNING_KEY }}
|
|
run: |
|
|
set -eu
|
|
umask 077
|
|
printf '%s' "$BUNDLE_SIGNING_KEY" > /tmp/release.key
|
|
.tools/clawmates-bundler pubkey /tmp/release.key dist/release.pub
|
|
rm -f /tmp/release.key
|
|
# The customer's exact procedure: the public half only, inside a
|
|
# NETWORK-DISABLED container, proving verification needs no internet.
|
|
docker run --rm --network none \
|
|
-v "$PWD/dist:/dist:ro" \
|
|
ubuntu:24.04 \
|
|
/dist/bundle/bin/clawmates-bundler verify /dist/bundle /dist/release.pub
|
|
|
|
- name: Tarball
|
|
run: tar -C dist -czf "clawmates-bundle-$VERSION.tgz" bundle
|
|
|
|
- name: Clean-room install rehearsal
|
|
run: |
|
|
set -eu
|
|
docker tag "clawmates/server:$VERSION" clawmates/server:latest
|
|
docker tag "clawmates/frontend:$VERSION" clawmates/frontend:latest
|
|
docker tag "clawmates/broker:$VERSION" clawmates/broker:latest
|
|
# Absolute path: the script cds around, and it needs the binary we
|
|
# already built rather than a cargo this host does not have.
|
|
CLAWMATES_BUNDLER="$PWD/.tools/clawmates-bundler" ./scripts/rehearse-install.sh
|
|
|
|
# Gitea's release API, not softprops/action-gh-release (GitHub-only).
|
|
# Create-or-reuse, so a re-run of the same tag updates instead of 409ing.
|
|
- name: Attach to the Gitea release
|
|
env:
|
|
FORGE_TOKEN: ${{ secrets.FORGE_TOKEN }}
|
|
run: |
|
|
set -eu
|
|
API="https://git.redclaw.dev/api/v1/repos/$GITHUB_REPOSITORY/releases"
|
|
TAG="${GITHUB_REF_NAME}"
|
|
id=$(curl -sS -H "Authorization: token $FORGE_TOKEN" "$API/tags/$TAG" \
|
|
| sed -n 's/.*"id":[ ]*\([0-9]\+\).*/\1/p' | head -1)
|
|
if [ -z "$id" ]; then
|
|
id=$(curl -sS -X POST -H "Authorization: token $FORGE_TOKEN" \
|
|
-H 'content-type: application/json' \
|
|
-d "{\"tag_name\":\"$TAG\",\"name\":\"$TAG\",\"body\":\"Air-gapped bundle for $TAG. Verify with the published public key before docker load.\"}" \
|
|
"$API" | sed -n 's/.*"id":[ ]*\([0-9]\+\).*/\1/p' | head -1)
|
|
fi
|
|
test -n "$id" || { echo "could not create or find the release for $TAG"; exit 1; }
|
|
for f in "clawmates-bundle-$VERSION.tgz" dist/release.pub; do
|
|
code=$(curl -sS -o /dev/null -w '%{http_code}' -X POST \
|
|
-H "Authorization: token $FORGE_TOKEN" \
|
|
-F "attachment=@$f" \
|
|
"$API/$id/assets?name=$(basename "$f")")
|
|
echo " attached $(basename "$f") (HTTP $code)"
|
|
case "$code" in 20*) ;; *) echo "attach failed"; exit 1 ;; esac
|
|
done
|
|
|
|
# Release artifacts are GBs of image tarballs on the production gateway.
|
|
# Never `docker image prune -a` here: clawmates/agent-*:dev exist in no
|
|
# registry and are the source of the microVM rootfs files.
|
|
- name: Reclaim disk
|
|
if: always()
|
|
run: |
|
|
rm -rf dist .tools "clawmates-bundle-$VERSION.tgz" || true
|
|
for i in server frontend broker agent-base agent-browser; do
|
|
docker rmi "clawmates/$i:$VERSION" 2>/dev/null || true
|
|
done
|
|
df -h / | awk 'NR==2{print " disk free: "$4}'
|