From 8f1e81da0ae8302924b626d7fdb79398d346c809 Mon Sep 17 00:00:00 2001 From: vash Date: Wed, 3 Jun 2026 12:43:03 -0500 Subject: [PATCH 1/2] ci(build-stable): read manifest from origin/brightfire/ci in release-check check-release-needed.sh ran after 'Prepare stable branch' switched the working tree to upstream's stable/ branch, which does not contain BRIGHTFIRE_PATCHES.md. The script was reading from the filesystem ($PATCHES_FILE) and failing with 'Manifest not found'. Fix: read the manifest directly from origin/brightfire/ci via git show. BRIGHTFIRE_PATCHES.md only ever lives on brightfire/ci anyway. The release-check step no longer depends on working-tree contents. Also pipes git-show through sha256sum (instead of capturing into a shell var) so the trailing newline is preserved byte-for-byte. Verified locally that the resulting sha matches 'sha256sum BRIGHTFIRE_PATCHES.md' on the working-tree copy. Failure: https://github.com/brightfire/openclaw/actions/runs/26901255445/job/79354008660 --- .github/workflows/bf-build-stable.yml | 1 - .../bf/build-stable/check-release-needed.sh | 24 +++++++++++++------ 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.github/workflows/bf-build-stable.yml b/.github/workflows/bf-build-stable.yml index fc8b2ab13d397..239a2c50b50ed 100644 --- a/.github/workflows/bf-build-stable.yml +++ b/.github/workflows/bf-build-stable.yml @@ -202,7 +202,6 @@ jobs: id: release-check env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - PATCHES_FILE: ${{ env.PATCHES_FILE }} FORCE_RELEASE: ${{ inputs.force_release || 'false' }} UPSTREAM_TAG_INPUT: ${{ inputs.upstream_tag || '' }} run: /tmp/bf-build-stable/check-release-needed.sh diff --git a/scripts/bf/build-stable/check-release-needed.sh b/scripts/bf/build-stable/check-release-needed.sh index 3a32684f318e1..c3c8fbcd20199 100755 --- a/scripts/bf/build-stable/check-release-needed.sh +++ b/scripts/bf/build-stable/check-release-needed.sh @@ -26,11 +26,18 @@ # - Latest release has no asset -> needs_release=true (backfill run) # - gh release view fails (other) -> fail loudly (no silent skip) # +# Manifest source: read directly from `origin/brightfire/ci:BRIGHTFIRE_PATCHES.md` +# via `git show`. At this point in the build-stable workflow the working tree +# is the merged stable/ branch, which does not contain the manifest. +# `brightfire/ci` is the only branch that ever has it. +# # Inputs (env): # GITHUB_REPOSITORY — owner/repo (set by GHA runtime) # GITHUB_TOKEN — used implicitly by gh # GITHUB_OUTPUT — path to GitHub Actions output file -# PATCHES_FILE — manifest path (default: BRIGHTFIRE_PATCHES.md) +# MANIFEST_REF — git ref to read manifest from (default: +# origin/brightfire/ci); override only for local +# testing # FORCE_RELEASE — "true" forces needs_release=true (from workflow_dispatch) # UPSTREAM_TAG_INPUT — non-empty value forces needs_release=true (from # workflow_dispatch upstream_tag input) @@ -41,21 +48,24 @@ set -euo pipefail -PATCHES_FILE="${PATCHES_FILE:-BRIGHTFIRE_PATCHES.md}" +MANIFEST_REF="${MANIFEST_REF:-origin/brightfire/ci}" FORCE_RELEASE="${FORCE_RELEASE:-false}" UPSTREAM_TAG_INPUT="${UPSTREAM_TAG_INPUT:-}" -if [ ! -f "$PATCHES_FILE" ]; then - echo "::error::Manifest not found: $PATCHES_FILE" +if [ -z "${GITHUB_OUTPUT:-}" ]; then + echo "::error::GITHUB_OUTPUT not set" exit 2 fi -if [ -z "${GITHUB_OUTPUT:-}" ]; then - echo "::error::GITHUB_OUTPUT not set" +if ! git rev-parse --verify "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" >/dev/null 2>&1; then + echo "::error::BRIGHTFIRE_PATCHES.md not found at $MANIFEST_REF" exit 2 fi -MANIFEST_SHA=$(sha256sum "$PATCHES_FILE" | awk '{print $1}') +# Pipe git-show through sha256sum so the trailing newline is preserved +# byte-for-byte (sha must match `sha256sum BRIGHTFIRE_PATCHES.md` taken +# elsewhere, e.g. by write-build-fingerprint.sh). +MANIFEST_SHA=$(git show "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" | sha256sum | awk '{print $1}') echo "Current manifest sha256: $MANIFEST_SHA" echo "manifest_sha256=$MANIFEST_SHA" >> "$GITHUB_OUTPUT" From 98494622d5602281eea8e1e3e82de7aa67399d1a Mon Sep 17 00:00:00 2001 From: Vash Date: Wed, 3 Jun 2026 12:48:37 -0500 Subject: [PATCH 2/2] ci(build-stable): pin manifest source to staged copy Codex flagged a real race: the workflow stages BRIGHTFIRE_PATCHES.md from origin/brightfire/ci at the top, but check-release-needed.sh re-reads the same moving ref ~30+ min later, after the build/test phase. The intervening 'Fetch all brightfire branches' step (and any concurrent push to brightfire/ci that lands before this run's cancel signal does) advances origin/brightfire/ci, so the gate compares AND fingerprints against a manifest that did NOT drive this build. Fix: stage a frozen copy of the manifest to /tmp at the top of the workflow, record its sha256 to GITHUB_ENV, and pipe both into check-release-needed.sh as PINNED_MANIFEST_PATH / PINNED_MANIFEST_SHA256. The script prefers the pinned source when present, with a documented git-ref fallback for local testing and a warning emitted if the pinned path is set but unreadable. Verified the three branches manually: pinned path + pre-computed sha, pinned path + auto-computed sha, and pinned path missing (warns + falls back to git ref). All emit consistent manifest_sha256 to GITHUB_OUTPUT. Net: 2 files, +70/-23. --- .github/workflows/bf-build-stable.yml | 19 ++++- .../bf/build-stable/check-release-needed.sh | 74 +++++++++++++------ 2 files changed, 70 insertions(+), 23 deletions(-) diff --git a/.github/workflows/bf-build-stable.yml b/.github/workflows/bf-build-stable.yml index 239a2c50b50ed..6adee7d3b565a 100644 --- a/.github/workflows/bf-build-stable.yml +++ b/.github/workflows/bf-build-stable.yml @@ -67,10 +67,20 @@ jobs: token: ${{ steps.app-token.outputs.token }} persist-credentials: true - - name: Fetch patch manifest from brightfire/ci + # Pin the manifest used by this run: stage it to the working tree AND + # save a frozen copy under /tmp + record its sha256 to GITHUB_ENV. Every + # later step (parse, fingerprint gate, fingerprint writer) reads from + # the pinned copy, not origin/brightfire/ci, so concurrent pushes to + # brightfire/ci during build/test can't move the gate out from under us. + - name: Fetch patch manifest from brightfire/ci (pinned) + id: pin-manifest run: | git show origin/brightfire/ci:BRIGHTFIRE_PATCHES.md > BRIGHTFIRE_PATCHES.md - echo "Loaded patch manifest from brightfire/ci" + cp BRIGHTFIRE_PATCHES.md /tmp/BRIGHTFIRE_PATCHES.pinned.md + MANIFEST_SHA=$(sha256sum /tmp/BRIGHTFIRE_PATCHES.pinned.md | awk '{print $1}') + echo "Pinned manifest sha256: $MANIFEST_SHA" + echo "PINNED_MANIFEST_PATH=/tmp/BRIGHTFIRE_PATCHES.pinned.md" >> "$GITHUB_ENV" + echo "PINNED_MANIFEST_SHA256=$MANIFEST_SHA" >> "$GITHUB_ENV" - name: Stage patch parser run: cp scripts/bf/parse-patches.py /tmp/parse-patches.py @@ -204,6 +214,11 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} FORCE_RELEASE: ${{ inputs.force_release || 'false' }} UPSTREAM_TAG_INPUT: ${{ inputs.upstream_tag || '' }} + # Pinned to the manifest staged at the top of the workflow so the + # gate compares against the manifest that actually drove the build, + # not whatever origin/brightfire/ci has advanced to since. + PINNED_MANIFEST_PATH: ${{ env.PINNED_MANIFEST_PATH }} + PINNED_MANIFEST_SHA256: ${{ env.PINNED_MANIFEST_SHA256 }} run: /tmp/bf-build-stable/check-release-needed.sh # Tarball is built unconditionally. `npm pack` is cheap (~10s) and diff --git a/scripts/bf/build-stable/check-release-needed.sh b/scripts/bf/build-stable/check-release-needed.sh index c3c8fbcd20199..a5dbb98fa33cd 100755 --- a/scripts/bf/build-stable/check-release-needed.sh +++ b/scripts/bf/build-stable/check-release-needed.sh @@ -26,46 +26,78 @@ # - Latest release has no asset -> needs_release=true (backfill run) # - gh release view fails (other) -> fail loudly (no silent skip) # -# Manifest source: read directly from `origin/brightfire/ci:BRIGHTFIRE_PATCHES.md` -# via `git show`. At this point in the build-stable workflow the working tree -# is the merged stable/ branch, which does not contain the manifest. -# `brightfire/ci` is the only branch that ever has it. +# Manifest source (preferred): a pinned file path + pre-computed sha256 +# staged by the workflow's "Fetch patch manifest from brightfire/ci (pinned)" +# step. Pinning matters because origin/brightfire/ci can advance between the +# moment the build started and the moment this gate runs — concurrent pushes +# happen during the ~30+ min build/test window, and several intervening +# `git fetch` calls update the remote tracking ref. Reading the moving ref +# here would gate (and fingerprint) on a manifest that did NOT drive this +# build, letting newly-registered patches sneak into a fingerprint that +# claims to represent only the older set. +# +# Fallback: read from a git ref (default origin/brightfire/ci). This keeps +# local invocations working and provides a graceful degradation if the +# workflow ever ran this script without staging the pinned copy. # # Inputs (env): -# GITHUB_REPOSITORY — owner/repo (set by GHA runtime) -# GITHUB_TOKEN — used implicitly by gh -# GITHUB_OUTPUT — path to GitHub Actions output file -# MANIFEST_REF — git ref to read manifest from (default: -# origin/brightfire/ci); override only for local -# testing -# FORCE_RELEASE — "true" forces needs_release=true (from workflow_dispatch) -# UPSTREAM_TAG_INPUT — non-empty value forces needs_release=true (from -# workflow_dispatch upstream_tag input) +# GITHUB_REPOSITORY — owner/repo (set by GHA runtime) +# GITHUB_TOKEN — used implicitly by gh +# GITHUB_OUTPUT — path to GitHub Actions output file +# PINNED_MANIFEST_PATH — path to the staged pinned manifest; preferred +# source when present and readable +# PINNED_MANIFEST_SHA256 — sha256 of the pinned manifest, computed at +# staging time; used directly when set so the +# fingerprint writer and the gate share the +# exact same value byte-for-byte +# MANIFEST_REF — git ref fallback (default: +# origin/brightfire/ci); override only for +# local testing +# FORCE_RELEASE — "true" forces needs_release=true (from +# workflow_dispatch) +# UPSTREAM_TAG_INPUT — non-empty value forces needs_release=true +# (from workflow_dispatch upstream_tag input) # # Outputs (written to $GITHUB_OUTPUT): # needs_release — "true" | "false" -# manifest_sha256 — sha256 of the current manifest +# manifest_sha256 — sha256 of the manifest that drove this build set -euo pipefail MANIFEST_REF="${MANIFEST_REF:-origin/brightfire/ci}" FORCE_RELEASE="${FORCE_RELEASE:-false}" UPSTREAM_TAG_INPUT="${UPSTREAM_TAG_INPUT:-}" +PINNED_MANIFEST_PATH="${PINNED_MANIFEST_PATH:-}" +PINNED_MANIFEST_SHA256="${PINNED_MANIFEST_SHA256:-}" if [ -z "${GITHUB_OUTPUT:-}" ]; then echo "::error::GITHUB_OUTPUT not set" exit 2 fi -if ! git rev-parse --verify "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" >/dev/null 2>&1; then - echo "::error::BRIGHTFIRE_PATCHES.md not found at $MANIFEST_REF" - exit 2 +if [ -n "$PINNED_MANIFEST_PATH" ] && [ -r "$PINNED_MANIFEST_PATH" ]; then + # Preferred path: pinned file + pre-computed sha. + if [ -n "$PINNED_MANIFEST_SHA256" ]; then + MANIFEST_SHA="$PINNED_MANIFEST_SHA256" + else + MANIFEST_SHA=$(sha256sum "$PINNED_MANIFEST_PATH" | awk '{print $1}') + fi + echo "Manifest source: pinned ($PINNED_MANIFEST_PATH)" +else + if [ -n "$PINNED_MANIFEST_PATH" ]; then + echo "::warning::PINNED_MANIFEST_PATH set but not readable ($PINNED_MANIFEST_PATH); falling back to $MANIFEST_REF" + fi + if ! git rev-parse --verify "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" >/dev/null 2>&1; then + echo "::error::BRIGHTFIRE_PATCHES.md not found at $MANIFEST_REF" + exit 2 + fi + # Pipe git-show through sha256sum so the trailing newline is preserved + # byte-for-byte (sha must match `sha256sum BRIGHTFIRE_PATCHES.md` taken + # elsewhere, e.g. by write-build-fingerprint.sh). + MANIFEST_SHA=$(git show "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" | sha256sum | awk '{print $1}') + echo "Manifest source: $MANIFEST_REF (fallback)" fi -# Pipe git-show through sha256sum so the trailing newline is preserved -# byte-for-byte (sha must match `sha256sum BRIGHTFIRE_PATCHES.md` taken -# elsewhere, e.g. by write-build-fingerprint.sh). -MANIFEST_SHA=$(git show "$MANIFEST_REF:BRIGHTFIRE_PATCHES.md" | sha256sum | awk '{print $1}') echo "Current manifest sha256: $MANIFEST_SHA" echo "manifest_sha256=$MANIFEST_SHA" >> "$GITHUB_OUTPUT"