From 077df8720fa2ef14c075e0ecf2fd700991581a91 Mon Sep 17 00:00:00 2001 From: UnArbosFive Date: Wed, 5 Aug 2026 00:31:47 +0200 Subject: [PATCH] fix: advance latest node image from main --- .github/scripts/resolve-node-image.sh | 30 ++++++++++ .github/scripts/test-resolve-node-image.sh | 56 +++++++++++++++++++ .../check-node-image-publication.yml | 20 +++++++ .github/workflows/docker.yml | 50 ++++++++--------- 4 files changed, 130 insertions(+), 26 deletions(-) create mode 100755 .github/scripts/resolve-node-image.sh create mode 100755 .github/scripts/test-resolve-node-image.sh create mode 100644 .github/workflows/check-node-image-publication.yml diff --git a/.github/scripts/resolve-node-image.sh b/.github/scripts/resolve-node-image.sh new file mode 100755 index 0000000000..41a4358590 --- /dev/null +++ b/.github/scripts/resolve-node-image.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash + +set -euo pipefail + +output_file="${1:-${GITHUB_ENV:-}}" +: "${output_file:?pass an output file or set GITHUB_ENV}" +: "${INPUT_TAG:?INPUT_TAG is required}" +: "${SOURCE_REF:?SOURCE_REF is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY is required}" + +# Docker tags cannot contain '/', so sanitize manual refs such as feat/x. +tag="${INPUT_TAG//[^a-zA-Z0-9._-]/-}" +[[ -n "$tag" ]] || { echo "Docker tag is empty" >&2; exit 1; } + +# Main is the only production publication path. A successful main build +# updates :main and :latest together; release and network tags cannot race it +# and move :latest backward. +if [[ "$tag" == main && "$SOURCE_REF" == refs/heads/main ]]; then + latest_tag=true +else + latest_tag=false +fi + +image_repository=$(printf '%s' "$GITHUB_REPOSITORY" | tr '[:upper:]' '[:lower:]') + +{ + echo "tag=$tag" + echo "latest_tag=$latest_tag" + echo "image=ghcr.io/$image_repository" +} >> "$output_file" diff --git a/.github/scripts/test-resolve-node-image.sh b/.github/scripts/test-resolve-node-image.sh new file mode 100755 index 0000000000..c4017adc87 --- /dev/null +++ b/.github/scripts/test-resolve-node-image.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash + +set -euo pipefail + +script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +resolver="$script_dir/resolve-node-image.sh" +tmp=$(mktemp -d) +trap 'rm -rf "$tmp"' EXIT + +run_case() { + local name="$1" + local input_tag="$2" + local source_ref="$3" + local expected_tag="$4" + local expected_latest="$5" + local output="$tmp/$name" + + GITHUB_REPOSITORY=RaoFoundation/subtensor \ + INPUT_TAG="$input_tag" \ + SOURCE_REF="$source_ref" \ + "$resolver" "$output" + + grep -qxF "tag=$expected_tag" "$output" + grep -qxF "latest_tag=$expected_latest" "$output" + grep -qxF "image=ghcr.io/raofoundation/subtensor" "$output" +} + +run_case main main refs/heads/main main true +run_case stale-main main refs/tags/v448 main false +run_case testnet testnet refs/heads/testnet testnet false +run_case release v448 refs/tags/v448 v448 false +run_case feature feature/example refs/heads/feature/example feature-example false + +workflow="$script_dir/../workflows/docker.yml" +grep -qF 'branches: [main, devnet, testnet]' "$workflow" +grep -qF 'run: ./.github/scripts/resolve-node-image.sh' "$workflow" +grep -qF "env.latest_tag == 'true'" "$workflow" +grep -qF "cancel-in-progress: \${{ github.ref != 'refs/heads/main' }}" "$workflow" +grep -qF 'current_main=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main"' "$workflow" + +publish_job=$(sed -n '/^ publish:/,$p' "$workflow") +checkout_line=$(grep -nF 'ref: ${{ needs.setup.outputs.sha }}' <<<"$publish_job" | head -n 1 | cut -d: -f1) +resolver_line=$(grep -nF 'run: ./.github/scripts/resolve-node-image.sh' <<<"$publish_job" | cut -d: -f1) +[[ "$checkout_line" -lt "$resolver_line" ]] || { + echo "publish job must check out the pinned source before running its resolver" >&2 + exit 1 +} + +head_check_line=$(grep -nF 'name: Verify current main revision' <<<"$publish_job" | cut -d: -f1) +push_line=$(grep -nF 'name: Build and push' <<<"$publish_job" | cut -d: -f1) +[[ "$head_check_line" -lt "$push_line" ]] || { + echo "publish job must reject stale main revisions before pushing" >&2 + exit 1 +} + +echo "node image tag policy checks passed" diff --git a/.github/workflows/check-node-image-publication.yml b/.github/workflows/check-node-image-publication.yml new file mode 100644 index 0000000000..af91fbab8a --- /dev/null +++ b/.github/workflows/check-node-image-publication.yml @@ -0,0 +1,20 @@ +name: Validate Node Image Publication + +on: + pull_request: + paths: + - ".github/scripts/resolve-node-image.sh" + - ".github/scripts/test-resolve-node-image.sh" + - ".github/workflows/docker.yml" + - ".github/workflows/check-node-image-publication.yml" + +permissions: + contents: read + +jobs: + policy: + name: main updates latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: ./.github/scripts/test-resolve-node-image.sh diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index ed59a983eb..f893857023 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -1,11 +1,9 @@ name: Publish Docker Image # Node images publish whenever main or a network mirror moves, on runtime -# releases, and on demand. Main advances the development-facing :main tag. -# Releases cut by watch-mainnet-release.yml use the default GITHUB_TOKEN, which -# never emits `release: published`; the watcher therefore dispatches this -# workflow directly with the release tag. Release-version tags (vN) move -# :latest; branch tags do not. +# releases, and on demand. Main advances both :main and :latest so every +# successful merge publishes the default production image without waiting for +# a runtime release. Release-version and other branch tags never move :latest. on: release: @@ -21,7 +19,9 @@ on: concurrency: group: docker-${{ github.ref }} - cancel-in-progress: true + # Main publications run serially so an older push cannot finish after a + # newer one and roll :latest backward. Other refs keep the fast-cancel path. + cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} permissions: contents: read @@ -122,30 +122,16 @@ jobs: runs-on: [self-hosted, fireactions-turbo-8] timeout-minutes: 30 steps: - - name: Determine tag and image name - env: - INPUT_TAG: ${{ github.event.inputs.tag || github.ref_name }} - run: | - # Docker tags cannot contain '/', so derive the tag by replacing any - # disallowed characters — otherwise a ref like `feat/x` fails tag - # validation at push time. - tag="${INPUT_TAG//[^a-zA-Z0-9._-]/-}" - echo "tag=$tag" >> $GITHUB_ENV - # Move :latest for release events and for dispatched release-version - # tags (the watcher dispatches with tag=vN because its GITHUB_TOKEN - # release cannot fire the `release` trigger). - if [[ "${{ github.event_name }}" == "release" || "$tag" =~ ^v[0-9]+$ ]]; then - echo "latest_tag=true" >> $GITHUB_ENV - else - echo "latest_tag=false" >> $GITHUB_ENV - fi - # Docker requires lowercase image names; github.repository is RaoFoundation/subtensor - echo "image=ghcr.io/${GITHUB_REPOSITORY,,}" >> $GITHUB_ENV - - uses: actions/checkout@v4 with: ref: ${{ needs.setup.outputs.sha }} + - name: Determine tag and image name + env: + INPUT_TAG: ${{ github.event.inputs.tag || github.ref_name }} + SOURCE_REF: ${{ github.ref }} + run: ./.github/scripts/resolve-node-image.sh + - name: Download AMD64 production binary uses: actions/download-artifact@v5 with: @@ -171,6 +157,18 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Verify current main revision + if: env.latest_tag == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILT_SHA: ${{ needs.setup.outputs.sha }} + run: | + current_main=$(gh api "repos/$GITHUB_REPOSITORY/git/ref/heads/main" --jq '.object.sha') + [ "$BUILT_SHA" = "$current_main" ] || { + echo "Refusing to move :latest from stale main revision $BUILT_SHA; current main is $current_main" >&2 + exit 1 + } + - name: Build and push uses: docker/build-push-action@v6 with: