From 7219c2b9731799d671b519e0e24df5743c03154a Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:39:00 -0500 Subject: [PATCH 1/4] fix(feature-ideation): source tooling from the reusable's own commit tooling_ref defaulted to the floating `v1` tag, which predates scripts/feature-ideation-requirements.txt. Every feature-ideation/v1-* channel now carries the pip step that installs from that file, so every fleet run failed at "Install Python jsonschema" (#839, canary blocker #825). Default tooling_ref to empty and check out `inputs.tooling_ref || github.job_workflow_sha` in both jobs, the same fix auto-rebase took in #465. Add tooling-ref.bats so the default, the checkout refs, the files the reusable reads, and the callers cannot drift back into skew. Fixes #839 Co-Authored-By: Claude Opus 5.5 --- .../workflows/feature-ideation-reusable.yml | 20 ++++--- .../feature-ideation/tooling-ref.bats | 60 +++++++++++++++++++ 2 files changed, 73 insertions(+), 7 deletions(-) create mode 100644 test/workflows/feature-ideation/tooling-ref.bats diff --git a/.github/workflows/feature-ideation-reusable.yml b/.github/workflows/feature-ideation-reusable.yml index 0e939f9c..91bc7060 100644 --- a/.github/workflows/feature-ideation-reusable.yml +++ b/.github/workflows/feature-ideation-reusable.yml @@ -105,16 +105,20 @@ on: tooling_ref: description: | Ref of combsmith/.github to source the feature-ideation scripts from. - Defaults to `v1` to align with the @v1 pin convention used by caller stubs - (see standards/ci-standards.md tier model). When the v1 tag is moved - forward to a new release, callers automatically pick up the matching - script + schema versions in lockstep with the workflow file. + Leave empty (the default) to source them from THIS reusable's own + commit (`github.job_workflow_sha`), so the scripts, schema and + requirements file always match the channel the caller pinned + (e.g. `@feature-ideation/v1-stable`), with no skew. (Previously + defaulted to `v1`, which predates + scripts/feature-ideation-requirements.txt and made every fleet run + fail at "Install Python jsonschema" — see issue #839; same fix as + auto-rebase #465.) Override only for: - Testing a fork or PR branch end-to-end (`tooling_ref: my-feature-branch`) - Bleeding-edge testing against the latest main (`tooling_ref: main`) required: false - default: 'v1' + default: '' type: string sources_file: description: | @@ -162,7 +166,8 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: combsmith/.github - ref: ${{ inputs.tooling_ref }} + # This reusable's own commit unless overridden — see `tooling_ref` (#839). + ref: ${{ inputs.tooling_ref || github.job_workflow_sha }} path: .feature-ideation-tooling fetch-depth: 1 @@ -219,7 +224,8 @@ jobs: uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: repository: combsmith/.github - ref: ${{ inputs.tooling_ref }} + # This reusable's own commit unless overridden — see `tooling_ref` (#839). + ref: ${{ inputs.tooling_ref || github.job_workflow_sha }} path: .feature-ideation-tooling fetch-depth: 1 diff --git a/test/workflows/feature-ideation/tooling-ref.bats b/test/workflows/feature-ideation/tooling-ref.bats new file mode 100644 index 00000000..a4bf4365 --- /dev/null +++ b/test/workflows/feature-ideation/tooling-ref.bats @@ -0,0 +1,60 @@ +#!/usr/bin/env bats +# Regression guard for issue #839: the tooling checkout must track the +# reusable's own commit, never a floating tag. +# +# The reusable checks out combsmith/.github into .feature-ideation-tooling and +# runs scripts from it. When `tooling_ref` defaulted to `v1`, the channel tags +# (feature-ideation/v1-*) advanced the reusable to a version whose pip step +# needs scripts/feature-ideation-requirements.txt, while the tooling checkout +# stayed on the older `v1` tag that lacks it — every fleet run failed. The +# reusable and its tooling must come from the same commit +# (`github.job_workflow_sha`), the same pattern auto-rebase adopted in #465. + +load 'helpers/setup' + +REUSABLE="${TT_REPO_ROOT}/.github/workflows/feature-ideation-reusable.yml" +STUB="${TT_REPO_ROOT}/standards/workflows/feature-ideation.yml" +SELF_CALLER="${TT_REPO_ROOT}/.github/workflows/feature-ideation.yml" + +EXPECTED_REF='${{ inputs.tooling_ref || github.job_workflow_sha }}' + +@test "reusable: tooling_ref defaults to empty (no floating tag)" { + run yq -r '.on.workflow_call.inputs.tooling_ref.default' "$REUSABLE" + [ "$status" -eq 0 ] + [ "$output" = "" ] +} + +@test "reusable: every tooling checkout falls back to the reusable's own commit" { + run yq -r '.jobs[].steps[] | select(.with.path == ".feature-ideation-tooling") | .with.ref' "$REUSABLE" + [ "$status" -eq 0 ] + [ -n "$output" ] + while IFS= read -r ref; do + [ "$ref" = "$EXPECTED_REF" ] || { echo "tooling checkout ref drifted: $ref"; return 1; } + done <<< "$output" +} + +@test "reusable: every tooling checkout targets combsmith/.github" { + run yq -r '.jobs[].steps[] | select(.with.path == ".feature-ideation-tooling") | .with.repository' "$REUSABLE" + [ "$status" -eq 0 ] + [ -n "$output" ] + ! echo "$output" | grep -vxF 'combsmith/.github' +} + +@test "reusable: every .feature-ideation-tooling path it reads exists in this commit" { + # The tooling checkout is this same commit, so each referenced file must be + # here — a step that reads a file the commit lacks is the #839 failure mode. + run grep -oE '\.feature-ideation-tooling/[A-Za-z0-9._/-]+\.(sh|py|txt|json)' "$REUSABLE" + [ "$status" -eq 0 ] + [ -n "$output" ] + while IFS= read -r p; do + [ -f "${TT_REPO_ROOT}/${p#.feature-ideation-tooling/}" ] || { echo "missing: $p"; return 1; } + done <<< "$(echo "$output" | sort -u)" +} + +@test "callers: no caller pins tooling_ref to a ref (that would reintroduce skew)" { + for f in "$STUB" "$SELF_CALLER"; do + run yq -r '[.jobs[] | select(.with != null) | .with | has("tooling_ref")] | any' "$f" + [ "$status" -eq 0 ] + [ "$output" = "false" ] || { echo "$f forwards tooling_ref"; return 1; } + done +} From 71d6c2ff4c5a83c83de754121699a402d0bc9b11 Mon Sep 17 00:00:00 2001 From: Don Petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 21 Oct 2026 17:44:31 -0500 Subject: [PATCH 2/4] fix(feature-ideation): use job.workflow_sha, not github.job_workflow_sha github.job_workflow_sha evaluates empty at step level, so the checkout fell back to the default branch (main) instead of the reusable's commit. A probe run (reusable pinned at a different sha than its caller) showed job.workflow_sha is the reusable's pinned commit. Switch both tooling checkouts to it, guard against the empty context in tooling-ref.bats, and teach CI's actionlint 1.7.7 that job.workflow_sha exists. Refs #839 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yml | 3 ++- .github/workflows/feature-ideation-reusable.yml | 11 ++++++----- test/workflows/feature-ideation/tooling-ref.bats | 14 +++++++++++--- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa634fd4..d3ec5df3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -81,7 +81,8 @@ jobs: rm actionlint.tar.gz # Lint our own workflow files (fail on errors, ignore shellcheck style hints) # `job_workflow_sha` is a valid reusable-workflow context property; actionlint 1.7.7 schema omits it - ./actionlint -ignore 'SC2129' -ignore '"job_workflow_sha" is not defined' .github/workflows/*.yml + # `job.workflow_sha` likewise (the reusable's own commit; #839) — actionlint's job schema omits it + ./actionlint -ignore 'SC2129' -ignore '"job_workflow_sha" is not defined' -ignore 'property "workflow_sha" is not defined in object type' .github/workflows/*.yml # Template workflows use placeholder expressions — informational only ./actionlint -ignore 'context' standards/workflows/*.yml || true diff --git a/.github/workflows/feature-ideation-reusable.yml b/.github/workflows/feature-ideation-reusable.yml index 91bc7060..e1629ad4 100644 --- a/.github/workflows/feature-ideation-reusable.yml +++ b/.github/workflows/feature-ideation-reusable.yml @@ -106,13 +106,14 @@ on: description: | Ref of combsmith/.github to source the feature-ideation scripts from. Leave empty (the default) to source them from THIS reusable's own - commit (`github.job_workflow_sha`), so the scripts, schema and + commit (`job.workflow_sha`), so the scripts, schema and requirements file always match the channel the caller pinned (e.g. `@feature-ideation/v1-stable`), with no skew. (Previously defaulted to `v1`, which predates scripts/feature-ideation-requirements.txt and made every fleet run - fail at "Install Python jsonschema" — see issue #839; same fix as - auto-rebase #465.) + fail at "Install Python jsonschema" — see issue #839.) + Not `github.job_workflow_sha`: it evaluates empty at step level, + which makes actions/checkout silently fall back to `main`. Override only for: - Testing a fork or PR branch end-to-end (`tooling_ref: my-feature-branch`) @@ -167,7 +168,7 @@ jobs: with: repository: combsmith/.github # This reusable's own commit unless overridden — see `tooling_ref` (#839). - ref: ${{ inputs.tooling_ref || github.job_workflow_sha }} + ref: ${{ inputs.tooling_ref || job.workflow_sha }} path: .feature-ideation-tooling fetch-depth: 1 @@ -225,7 +226,7 @@ jobs: with: repository: combsmith/.github # This reusable's own commit unless overridden — see `tooling_ref` (#839). - ref: ${{ inputs.tooling_ref || github.job_workflow_sha }} + ref: ${{ inputs.tooling_ref || job.workflow_sha }} path: .feature-ideation-tooling fetch-depth: 1 diff --git a/test/workflows/feature-ideation/tooling-ref.bats b/test/workflows/feature-ideation/tooling-ref.bats index a4bf4365..f9341a6e 100644 --- a/test/workflows/feature-ideation/tooling-ref.bats +++ b/test/workflows/feature-ideation/tooling-ref.bats @@ -7,8 +7,11 @@ # (feature-ideation/v1-*) advanced the reusable to a version whose pip step # needs scripts/feature-ideation-requirements.txt, while the tooling checkout # stayed on the older `v1` tag that lacks it — every fleet run failed. The -# reusable and its tooling must come from the same commit -# (`github.job_workflow_sha`), the same pattern auto-rebase adopted in #465. +# reusable and its tooling must come from the same commit (`job.workflow_sha`). +# +# Not `github.job_workflow_sha`: it evaluates empty at step level, so +# actions/checkout silently falls back to the default branch (`main`) — +# observed in a live run, and in the job-context probe for #839. load 'helpers/setup' @@ -16,7 +19,7 @@ REUSABLE="${TT_REPO_ROOT}/.github/workflows/feature-ideation-reusable.yml" STUB="${TT_REPO_ROOT}/standards/workflows/feature-ideation.yml" SELF_CALLER="${TT_REPO_ROOT}/.github/workflows/feature-ideation.yml" -EXPECTED_REF='${{ inputs.tooling_ref || github.job_workflow_sha }}' +EXPECTED_REF='${{ inputs.tooling_ref || job.workflow_sha }}' @test "reusable: tooling_ref defaults to empty (no floating tag)" { run yq -r '.on.workflow_call.inputs.tooling_ref.default' "$REUSABLE" @@ -33,6 +36,11 @@ EXPECTED_REF='${{ inputs.tooling_ref || github.job_workflow_sha }}' done <<< "$output" } +@test "reusable: never relies on github.job_workflow_sha (empty at step level)" { + run grep -nE '\$\{\{[^}]*github\.job_workflow_sha' "$REUSABLE" + [ "$status" -eq 1 ] +} + @test "reusable: every tooling checkout targets combsmith/.github" { run yq -r '.jobs[].steps[] | select(.with.path == ".feature-ideation-tooling") | .with.repository' "$REUSABLE" [ "$status" -eq 0 ] From ec70be6edf4855caf509f6632b5262cc7626eb13 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:57:55 +0000 Subject: [PATCH 3/4] fix(reviews): assert tooling checkout ref per job Test-Change-Justification: CodeRabbit review on PR #1368 (issue #839) - aggregate nonempty check let one job lose its tooling checkout unnoticed; the assertion is strengthened, not weakened. Co-Authored-By: Claude Sonnet 5.5 --- test/workflows/feature-ideation/tooling-ref.bats | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/test/workflows/feature-ideation/tooling-ref.bats b/test/workflows/feature-ideation/tooling-ref.bats index f9341a6e..569b6a2c 100644 --- a/test/workflows/feature-ideation/tooling-ref.bats +++ b/test/workflows/feature-ideation/tooling-ref.bats @@ -28,12 +28,14 @@ EXPECTED_REF='${{ inputs.tooling_ref || job.workflow_sha }}' } @test "reusable: every tooling checkout falls back to the reusable's own commit" { - run yq -r '.jobs[].steps[] | select(.with.path == ".feature-ideation-tooling") | .with.ref' "$REUSABLE" - [ "$status" -eq 0 ] - [ -n "$output" ] - while IFS= read -r ref; do - [ "$ref" = "$EXPECTED_REF" ] || { echo "tooling checkout ref drifted: $ref"; return 1; } - done <<< "$output" + for job in gather-signals analyze; do + run yq -r ".jobs.\"${job}\".steps[] | select(.with.path == \".feature-ideation-tooling\") | .with.ref" "$REUSABLE" + [ "$status" -eq 0 ] + [ -n "$output" ] || { echo "$job has no tooling checkout"; return 1; } + while IFS= read -r ref; do + [ "$ref" = "$EXPECTED_REF" ] || { echo "$job tooling checkout ref drifted: $ref"; return 1; } + done <<< "$output" + done } @test "reusable: never relies on github.job_workflow_sha (empty at step level)" { From f43d22035e3d7ef898e9e0c74ad3537da100b7f7 Mon Sep 17 00:00:00 2001 From: don-petry <36422719+don-petry@users.noreply.github.com> Date: Thu, 22 Oct 2026 00:18:53 +0000 Subject: [PATCH 4/4] fix(bot): strengthen tooling-ref assertions to validate each job's checkout separately Add explicit check for nonempty refs and improve error messages to identify which job and which assertion failed. This prevents regression where a checkout could have an empty or missing ref and still pass. --- test/workflows/feature-ideation/tooling-ref.bats | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/workflows/feature-ideation/tooling-ref.bats b/test/workflows/feature-ideation/tooling-ref.bats index 569b6a2c..83ab7ef6 100644 --- a/test/workflows/feature-ideation/tooling-ref.bats +++ b/test/workflows/feature-ideation/tooling-ref.bats @@ -31,9 +31,10 @@ EXPECTED_REF='${{ inputs.tooling_ref || job.workflow_sha }}' for job in gather-signals analyze; do run yq -r ".jobs.\"${job}\".steps[] | select(.with.path == \".feature-ideation-tooling\") | .with.ref" "$REUSABLE" [ "$status" -eq 0 ] - [ -n "$output" ] || { echo "$job has no tooling checkout"; return 1; } + [ -n "$output" ] || { echo "$job: no tooling checkout step"; return 1; } while IFS= read -r ref; do - [ "$ref" = "$EXPECTED_REF" ] || { echo "$job tooling checkout ref drifted: $ref"; return 1; } + [ -n "$ref" ] || { echo "$job: tooling checkout ref is empty"; return 1; } + [ "$ref" = "$EXPECTED_REF" ] || { echo "$job: tooling checkout ref drifted to $ref (expected $EXPECTED_REF)"; return 1; } done <<< "$output" done }