From ae40e4ec112767bf6798beca0e6a43e63ecd5a22 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 8 Nov 2026 23:47:18 -0500 Subject: [PATCH 1/3] ci: publish the fork image to GHCR (RIG-8568) Co-authored-by: Matt Wilkinson --- .github/workflows/rigel-image.yml | 172 ++++++++++++++++++++++++++++++ 1 file changed, 172 insertions(+) create mode 100644 .github/workflows/rigel-image.yml diff --git a/.github/workflows/rigel-image.yml b/.github/workflows/rigel-image.yml new file mode 100644 index 000000000000..6e962833e51d --- /dev/null +++ b/.github/workflows/rigel-image.yml @@ -0,0 +1,172 @@ +# Upstream docker.yml requires Blacksmith runners and publishes to Docker Hub. +# This workflow publishes the fork on GitHub-hosted runners. +name: Rigel image + +on: + push: + tags: ['v*-rigel.*'] + workflow_dispatch: + inputs: + ref: + description: Tag or commit to publish + required: true + type: string + +permissions: + contents: read + +concurrency: + group: rigel-image-${{ inputs.ref || github.ref }} + cancel-in-progress: false + +env: + IMAGE: ghcr.io/rigelbuild/hermes-agent + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + runner: ubuntu-24.04 + - arch: arm64 + runner: ubuntu-24.04-arm + runs-on: ${{ matrix.runner }} + timeout-minutes: 60 + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1 + with: + ref: ${{ inputs.ref || github.ref }} + fetch-depth: 0 + - name: Resolve commit + id: commit + run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: '3.14' + - name: Write install stamp + run: | + display="$(python3 -m scripts.releases.distance)" + identity=(--commit "$(git rev-parse HEAD)") + if [ -n "$display" ]; then + base="${display%%+*}" + development="${display#*+}" + if [ "$development" = "$display" ]; then distance=0; else distance="${development%%.*}"; fi + identity+=(--base-version "$base" --display-version "$display" --distance "$distance") + fi + python3 scripts/write_install_stamp.py --output install-stamp.json \ + --distribution docker --update-mechanism external --source ci "${identity[@]}" + - name: Reject profile exports in build context + run: python3 scripts/ci/check_profile_archive_boundary.py + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - name: Build image for testing + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + platforms: linux/${{ matrix.arch }} + load: true + tags: hermes-image:test + labels: | + org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent + org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} + cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} + cache-to: type=gha,mode=max,scope=rigel-image-${{ matrix.arch }} + - name: Park install stamp + if: matrix.arch == 'amd64' + run: mv install-stamp.json "$RUNNER_TEMP/install-stamp.json" + - name: Set up test dependencies + if: matrix.arch == 'amd64' + uses: ./.github/actions/setup-pm + with: + extras: '[]' + test-environment: 'true' + prune-python-cache: true + - name: Restore install stamp + if: matrix.arch == 'amd64' + run: mv "$RUNNER_TEMP/install-stamp.json" install-stamp.json + - name: Run docker integration tests + if: matrix.arch == 'amd64' + env: + HERMES_TEST_IMAGE: hermes-image:test + OPENROUTER_API_KEY: '' + OPENAI_API_KEY: '' + NOUS_API_KEY: '' + run: HERMES_TEST_WORKERS=$(nproc) scripts/run_tests.sh tests/docker/ + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Push image by digest + id: push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + context: . + platforms: linux/${{ matrix.arch }} + outputs: type=image,name=ghcr.io/rigelbuild/hermes-agent,push-by-digest=true,name-canonical=true,push=true + labels: | + org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent + org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} + cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} + - name: Save digest + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: | + mkdir -p "$RUNNER_TEMP/digests" + touch "$RUNNER_TEMP/digests/${DIGEST#sha256:}" + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: digest-${{ matrix.arch }} + path: ${{ runner.temp }}/digests/* + if-no-files-found: error + + manifest: + needs: build + runs-on: ubuntu-24.04 + permissions: + contents: write + packages: write + steps: + - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1 + with: + ref: ${{ inputs.ref || github.ref }} + fetch-depth: 0 + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + path: ${{ runner.temp }}/digests + pattern: digest-* + merge-multiple: true + - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Publish manifest + id: manifest + env: + REF: ${{ inputs.ref || github.ref_name }} + run: | + set -euo pipefail + sha="$(git rev-parse HEAD)" + ref="${REF#refs/tags/}" + images=() + for digest in "$RUNNER_TEMP"/digests/*; do + images+=("$IMAGE@sha256:${digest##*/}") + done + docker buildx imagetools create -t "$IMAGE:$ref" -t "$IMAGE:sha-$sha" "${images[@]}" + digest="$(docker buildx imagetools inspect "$IMAGE:sha-$sha" --format '{{json .Manifest.Digest}}' | tr -d '"')" + printf '%s@%s\n' "$IMAGE" "$digest" | tee image-digest.txt >> "$GITHUB_STEP_SUMMARY" + echo "ref=$ref" >> "$GITHUB_OUTPUT" + - name: Attach digest to release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + REF: ${{ steps.manifest.outputs.ref }} + run: | + if gh release view "$REF" --json tagName >/dev/null 2>&1; then + gh release upload "$REF" image-digest.txt --clobber + fi From c1fc7eba2c641ecf7b05d36d273952e96ad4ba78 Mon Sep 17 00:00:00 2001 From: mintaka Date: Sun, 8 Nov 2026 23:57:28 -0500 Subject: [PATCH 2/3] ci: test both arches and push only the tested image (RIG-8568) Co-authored-by: Matt Wilkinson --- .github/workflows/rigel-image.yml | 67 +++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 17 deletions(-) diff --git a/.github/workflows/rigel-image.yml b/.github/workflows/rigel-image.yml index 6e962833e51d..4934cf2f2a00 100644 --- a/.github/workflows/rigel-image.yml +++ b/.github/workflows/rigel-image.yml @@ -8,7 +8,7 @@ on: workflow_dispatch: inputs: ref: - description: Tag or commit to publish + description: Rigel release tag to publish required: true type: string @@ -37,14 +37,32 @@ jobs: permissions: contents: read packages: write + outputs: + sha: ${{ steps.commit.outputs.sha }} steps: - - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1 + - name: Validate tag + id: tag + env: + REF: ${{ inputs.ref || github.ref_name }} + run: | + if [[ ! "$REF" =~ ^v[0-9][^/]*-rigel\.[^/]+$ ]]; then + echo "::error::Ref must be a Rigel release tag" + exit 1 + fi + echo "ref=$REF" >> "$GITHUB_OUTPUT" + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: - ref: ${{ inputs.ref || github.ref }} + ref: refs/tags/${{ steps.tag.outputs.ref }} fetch-depth: 0 - name: Resolve commit id: commit - run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + env: + REF: ${{ steps.tag.outputs.ref }} + run: | + sha="$(git rev-parse "refs/tags/$REF^{commit}")" + test "$sha" = "$(git rev-parse HEAD)" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + echo "epoch=$(git log -1 --format=%ct "$sha")" >> "$GITHUB_OUTPUT" - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: '3.14' @@ -73,23 +91,20 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} + build-args: SOURCE_DATE_EPOCH=${{ steps.commit.outputs.epoch }} cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=rigel-image-${{ matrix.arch }} - name: Park install stamp - if: matrix.arch == 'amd64' run: mv install-stamp.json "$RUNNER_TEMP/install-stamp.json" - name: Set up test dependencies - if: matrix.arch == 'amd64' uses: ./.github/actions/setup-pm with: extras: '[]' test-environment: 'true' prune-python-cache: true - name: Restore install stamp - if: matrix.arch == 'amd64' run: mv "$RUNNER_TEMP/install-stamp.json" install-stamp.json - name: Run docker integration tests - if: matrix.arch == 'amd64' env: HERMES_TEST_IMAGE: hermes-image:test OPENROUTER_API_KEY: '' @@ -111,7 +126,22 @@ jobs: labels: | org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} + build-args: SOURCE_DATE_EPOCH=${{ steps.commit.outputs.epoch }} cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} + - name: Verify pushed image matches tested image + env: + DIGEST: ${{ steps.push.outputs.digest }} + ARCH: ${{ matrix.arch }} + run: | + set -euo pipefail + manifest="$(docker buildx imagetools inspect "$IMAGE@$DIGEST" --raw)" + platform_digest="$(jq -r --arg arch "$ARCH" '.manifests[]? | select(.platform.os == "linux" and .platform.architecture == $arch) | .digest' <<< "$manifest")" + if [ -n "$platform_digest" ]; then + manifest="$(docker buildx imagetools inspect "$IMAGE@$platform_digest" --raw)" + fi + pushed="$(jq -er '.config.digest' <<< "$manifest")" + tested="$(docker image inspect hermes-image:test --format '{{.Id}}')" + test "$pushed" = "$tested" - name: Save digest env: DIGEST: ${{ steps.push.outputs.digest }} @@ -131,10 +161,6 @@ jobs: contents: write packages: write steps: - - uses: actions/checkout@f548e57e544e1ff5a4c46bf1e1b8685f8e4a348a # v7.0.1 - with: - ref: ${{ inputs.ref || github.ref }} - fetch-depth: 0 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: ${{ runner.temp }}/digests @@ -150,23 +176,30 @@ jobs: id: manifest env: REF: ${{ inputs.ref || github.ref_name }} + SHA: ${{ needs.build.outputs.sha }} run: | set -euo pipefail - sha="$(git rev-parse HEAD)" ref="${REF#refs/tags/}" images=() for digest in "$RUNNER_TEMP"/digests/*; do images+=("$IMAGE@sha256:${digest##*/}") done - docker buildx imagetools create -t "$IMAGE:$ref" -t "$IMAGE:sha-$sha" "${images[@]}" - digest="$(docker buildx imagetools inspect "$IMAGE:sha-$sha" --format '{{json .Manifest.Digest}}' | tr -d '"')" + docker buildx imagetools create -t "$IMAGE:$ref" -t "$IMAGE:sha-$SHA" "${images[@]}" + digest="$(docker buildx imagetools inspect "$IMAGE:sha-$SHA" --format '{{json .Manifest.Digest}}' | tr -d '"')" printf '%s@%s\n' "$IMAGE" "$digest" | tee image-digest.txt >> "$GITHUB_STEP_SUMMARY" echo "ref=$ref" >> "$GITHUB_OUTPUT" - name: Attach digest to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} REF: ${{ steps.manifest.outputs.ref }} + REPO: ${{ github.repository }} run: | - if gh release view "$REF" --json tagName >/dev/null 2>&1; then - gh release upload "$REF" image-digest.txt --clobber + set -euo pipefail + if response="$(gh api -i "repos/$REPO/releases/tags/$REF" 2>&1)"; then + gh release upload "$REF" image-digest.txt --clobber -R "$REPO" + elif [[ "$response" == HTTP/*' 404 '* ]]; then + echo "::warning::No GitHub release exists for $REF" + else + printf '%s\n' "$response" >&2 + exit 1 fi From edc652555578e660ec0ad6e9a464fad21db0e837 Mon Sep 17 00:00:00 2001 From: mintaka Date: Mon, 9 Nov 2026 00:25:14 -0500 Subject: [PATCH 3/3] ci: test the pushed digest and validate tag syntax (RIG-8568) Co-authored-by: Matt Wilkinson --- .github/workflows/rigel-image.yml | 51 +++++++++---------------------- 1 file changed, 15 insertions(+), 36 deletions(-) diff --git a/.github/workflows/rigel-image.yml b/.github/workflows/rigel-image.yml index 4934cf2f2a00..1db394f31d1e 100644 --- a/.github/workflows/rigel-image.yml +++ b/.github/workflows/rigel-image.yml @@ -45,7 +45,7 @@ jobs: env: REF: ${{ inputs.ref || github.ref_name }} run: | - if [[ ! "$REF" =~ ^v[0-9][^/]*-rigel\.[^/]+$ ]]; then + if [[ ! "$REF" =~ ^v[0-9][^/]*-rigel\.[^/]+$ || ! "$REF" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]]; then echo "::error::Ref must be a Rigel release tag" exit 1 fi @@ -81,19 +81,28 @@ jobs: - name: Reject profile exports in build context run: python3 scripts/ci/check_profile_archive_boundary.py - uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 - - name: Build image for testing + - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + - name: Build and push image by digest + id: push uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 with: context: . platforms: linux/${{ matrix.arch }} - load: true - tags: hermes-image:test + outputs: type=image,name=ghcr.io/rigelbuild/hermes-agent,push-by-digest=true,name-canonical=true,push=true labels: | org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} build-args: SOURCE_DATE_EPOCH=${{ steps.commit.outputs.epoch }} cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} cache-to: type=gha,mode=max,scope=rigel-image-${{ matrix.arch }} + - name: Pull image for tests + env: + DIGEST: ${{ steps.push.outputs.digest }} + run: docker pull "$IMAGE@$DIGEST" - name: Park install stamp run: mv install-stamp.json "$RUNNER_TEMP/install-stamp.json" - name: Set up test dependencies @@ -106,42 +115,11 @@ jobs: run: mv "$RUNNER_TEMP/install-stamp.json" install-stamp.json - name: Run docker integration tests env: - HERMES_TEST_IMAGE: hermes-image:test + HERMES_TEST_IMAGE: ${{ env.IMAGE }}@${{ steps.push.outputs.digest }} OPENROUTER_API_KEY: '' OPENAI_API_KEY: '' NOUS_API_KEY: '' run: HERMES_TEST_WORKERS=$(nproc) scripts/run_tests.sh tests/docker/ - - uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Push image by digest - id: push - uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 - with: - context: . - platforms: linux/${{ matrix.arch }} - outputs: type=image,name=ghcr.io/rigelbuild/hermes-agent,push-by-digest=true,name-canonical=true,push=true - labels: | - org.opencontainers.image.source=https://github.com/RigelBuild/hermes-agent - org.opencontainers.image.revision=${{ steps.commit.outputs.sha }} - build-args: SOURCE_DATE_EPOCH=${{ steps.commit.outputs.epoch }} - cache-from: type=gha,scope=rigel-image-${{ matrix.arch }} - - name: Verify pushed image matches tested image - env: - DIGEST: ${{ steps.push.outputs.digest }} - ARCH: ${{ matrix.arch }} - run: | - set -euo pipefail - manifest="$(docker buildx imagetools inspect "$IMAGE@$DIGEST" --raw)" - platform_digest="$(jq -r --arg arch "$ARCH" '.manifests[]? | select(.platform.os == "linux" and .platform.architecture == $arch) | .digest' <<< "$manifest")" - if [ -n "$platform_digest" ]; then - manifest="$(docker buildx imagetools inspect "$IMAGE@$platform_digest" --raw)" - fi - pushed="$(jq -er '.config.digest' <<< "$manifest")" - tested="$(docker image inspect hermes-image:test --format '{{.Id}}')" - test "$pushed" = "$tested" - name: Save digest env: DIGEST: ${{ steps.push.outputs.digest }} @@ -197,6 +175,7 @@ jobs: set -euo pipefail if response="$(gh api -i "repos/$REPO/releases/tags/$REF" 2>&1)"; then gh release upload "$REF" image-digest.txt --clobber -R "$REPO" + # The tag can be pushed before its GitHub release exists. elif [[ "$response" == HTTP/*' 404 '* ]]; then echo "::warning::No GitHub release exists for $REF" else