From fdb839ca3716e717a77947fd1bbf43acc30f529c Mon Sep 17 00:00:00 2001 From: Asgeir Frimannsson Date: Sun, 13 Sep 2026 17:24:28 +0200 Subject: [PATCH 1/3] fix: run the reporting steps after a failing check A failed gate (exit 3) or a check that did not run (exit 4) skipped every later composite step, because a step's if: implies success(). The change detection, sticky PR comment and status steps now also run after a check verdict: status is failed, has-changes is set, and pr-comment posts the gate result or a did-not-run line naming the cause. The check step's exit code still fails the job. A kapi up that fails, or a check that errors, still reports nothing. The did_not_run_cause output is renamed did-not-run-cause to match the action's other outputs. It has not been released. The test fixture moves from the retired content: key to collections:, and the self-tests pin kapi 1.2.0-rc29, because 1.2.0-rc14 reads no content from the new shape. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VRid8i4qnuNfE7Lio73Uu4 --- .github/workflows/test.yml | 143 +++++++++++++++++++++++++++++++++---- README.md | 12 ++-- action.yml | 41 ++++++++--- test/check-outcomes.sh | 68 ++++++++++++++++-- test/fixture/fixture.kapi | 35 +++------ test/stub/gh | 22 ++++++ 6 files changed, 264 insertions(+), 57 deletions(-) create mode 100755 test/stub/gh diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a2b98ec..6de471e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,12 +2,13 @@ name: Test # Keyless self-tests against the fixture project in test/fixture — plan mode # and check need no provider or server credentials. The CLI version is pinned -# to the first release that ships `kapi up` / `check --ship`; bump -# deliberately. +# to a release that reads the fixture's `collections:` recipe (1.2.0-rc17 or +# later); bump deliberately. # -# The check-outcome jobs put a kapi stub (test/stub/kapi) on PATH instead, so -# each exit code kapi check can end with, and each did-not-run cause, is -# exercised whatever the pinned release can produce. +# The check-outcome and reporting jobs put stubs (test/stub) on PATH instead: +# a kapi that exits with a chosen code, and a gh that records the sticky PR +# comment rather than posting it. Every exit code kapi check can end with, and +# each did-not-run cause, is exercised whatever the pinned release can produce. on: push: @@ -19,7 +20,7 @@ on: workflow_dispatch: env: - KAPI_VERSION: "1.2.0-rc14" + KAPI_VERSION: "1.2.0-rc29" jobs: test-plan: @@ -30,7 +31,7 @@ jobs: - uses: neokapi/setup-kapi@v1 with: - version: "1.2.0-rc14" + version: ${{ env.KAPI_VERSION }} plugins: "" - name: Run plan @@ -57,7 +58,7 @@ jobs: - uses: neokapi/setup-kapi@v1 with: - version: "1.2.0-rc14" + version: ${{ env.KAPI_VERSION }} plugins: "" # The fixture is deliberately untranslated: the ship gate must FAIL the @@ -77,11 +78,13 @@ jobs: STEP_OUTCOME: ${{ steps.gate.outcome }} GATE: ${{ steps.gate.outputs.gate }} RESULT: ${{ steps.gate.outputs.result }} + STATUS: ${{ steps.gate.outputs.status }} run: | - echo "step outcome: ${STEP_OUTCOME}, gate: ${GATE}, result: ${RESULT}" + echo "step outcome: ${STEP_OUTCOME}, gate: ${GATE}, result: ${RESULT}, status: ${STATUS}" test "${STEP_OUTCOME}" = "failure" test "${GATE}" = "fail" test "${RESULT}" = "failed" + test "${STATUS}" = "failed" test-check-file: name: "check (file-mode pass)" @@ -91,7 +94,7 @@ jobs: - uses: neokapi/setup-kapi@v1 with: - version: "1.2.0-rc14" + version: ${{ env.KAPI_VERSION }} plugins: "" - name: Gate the fixture source file @@ -209,7 +212,7 @@ jobs: # expect STEP OUTCOME RESULT GATE CAUSE expect() { local got want - got="$(jq -r --arg id "$1" '.[$id] | [.outcome, (.outputs.result // ""), (.outputs.gate // ""), (.outputs.did_not_run_cause // "")] | join(" | ")' <<< "$STEPS")" + got="$(jq -r --arg id "$1" '.[$id] | [.outcome, (.outputs.result // ""), (.outputs.gate // ""), (.outputs["did-not-run-cause"] // "")] | join(" | ")' <<< "$STEPS")" want="$2 | $3 | $4 | $5" if [ "$got" = "$want" ]; then echo "ok $1: $got" @@ -228,14 +231,126 @@ jobs: expect error failure error "" "" exit "$status" + test-check-reporting: + name: "reporting after a failing check (kapi and gh stubs)" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Put the stubs on PATH + run: echo "${GITHUB_WORKSPACE}/test/stub" >> "$GITHUB_PATH" + + # Each run asks for the sticky PR comment, and the gh stub records the + # body instead of posting it. The action comments only on pull_request + # events, so on a push or a schedule the verify step expects no comment. + - name: "exit 0 with pr-comment" + id: passed + continue-on-error: true + uses: ./ + env: + KAPI_STUB_OUTPUT: test/check-output/passed.txt + KAPI_STUB_EXIT: "0" + GH_STUB_LOG: ${{ runner.temp }}/gh-passed + with: + command: check + pr-comment: "true" + + - name: "exit 3 with pr-comment" + id: failed + continue-on-error: true + uses: ./ + env: + KAPI_STUB_OUTPUT: test/check-output/failed.txt + KAPI_STUB_EXIT: "3" + GH_STUB_LOG: ${{ runner.temp }}/gh-failed + with: + command: check + args: "--ship" + pr-comment: "true" + + - name: "exit 4 with pr-comment" + id: did_not_run + continue-on-error: true + uses: ./ + env: + KAPI_STUB_OUTPUT: test/check-output/checker_invalid.txt + KAPI_STUB_EXIT: "4" + GH_STUB_LOG: ${{ runner.temp }}/gh-did_not_run + with: + command: check + pr-comment: "true" + + - name: Verify the reports, and that the check still failed + env: + STEPS: ${{ toJSON(steps) }} + EVENT_NAME: ${{ github.event_name }} + GH_LOGS: ${{ runner.temp }} + run: | + status=0 + expect() { + if [ "$2" = "$3" ]; then + echo "ok $1: $2" + else + echo "::error::$1: got '$2', want '$3'" + status=1 + fi + } + # value STEP FILTER reads one field of a step from the steps context. + value() { + jq -r --arg id "$1" ".[\$id] | $2 // \"\"" <<< "$STEPS" + } + # comment STEP TEXT: on a pull_request event the body the action + # posted must contain TEXT; on any other event it must post nothing. + comment() { + local body="${GH_LOGS}/gh-$1/body.md" + if [ "$EVENT_NAME" != "pull_request" ]; then + if [ -e "$body" ]; then + echo "::error::$1: a comment was posted on a ${EVENT_NAME} event" + status=1 + else + echo "ok $1: no comment on a ${EVENT_NAME} event" + fi + elif [ ! -f "$body" ]; then + echo "::error::$1: no comment was posted" + status=1 + elif grep -qF -- "$2" "$body"; then + echo "ok $1: the comment contains: $2" + else + echo "::error::$1: the comment lacks: $2" + sed 's/^/ > /' "$body" + status=1 + fi + } + + # The action step itself fails on exit 3 and exit 4, which is what + # fails a job that does not set continue-on-error. + expect "passed outcome" "$(value passed .outcome)" success + expect "failed outcome" "$(value failed .outcome)" failure + expect "did_not_run outcome" "$(value did_not_run .outcome)" failure + + # The reporting steps still ran after the failing check. + expect "passed status" "$(value passed .outputs.status)" no-changes + expect "failed status" "$(value failed .outputs.status)" failed + expect "did_not_run status" "$(value did_not_run .outputs.status)" failed + expect "passed has-changes" "$(value passed '.outputs["has-changes"]')" false + expect "failed has-changes" "$(value failed '.outputs["has-changes"]')" false + expect "did_not_run has-changes" "$(value did_not_run '.outputs["has-changes"]')" false + expect "did_not_run did-not-run-cause" "$(value did_not_run '.outputs["did-not-run-cause"]')" checker_invalid + + comment passed "✅ **Gate: pass**" + comment failed "❌ **Gate: unmet**" + comment did_not_run "❌ **Did not run** (\`checker_invalid\`): a checker failed its canary, so this run's result cannot be trusted." + exit "$status" + test-check-annotations: name: "check outcomes: exit codes, outputs and annotations (kapi stub)" runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - # Runs the action's own step script once per outcome, including the - # did-not-run causes read from text, JSON, YAML and colored output. + # Runs the action's own step scripts once per outcome, including the + # did-not-run causes read from text, JSON, YAML and colored output, and + # the sticky PR comment each outcome renders. - name: Run the check-outcome harness run: | yq --version @@ -249,7 +364,7 @@ jobs: - uses: neokapi/setup-kapi@v1 with: - version: "1.2.0-rc14" + version: ${{ env.KAPI_VERSION }} plugins: "" - name: Pseudo-translate the fixture diff --git a/README.md b/README.md index cb4bd08..6d2e580 100644 --- a/README.md +++ b/README.md @@ -181,11 +181,13 @@ jobs: Ordinary builds never fail on target-language drift — a locale that is behind is pending work, not an error. `check --ship` is the explicit, opt-in enforcement point. +A failing gate still reports: `status` is `failed`, `has-changes` is set, and with `pr-comment` the sticky comment shows the result. The step keeps the gate's exit code, so the job fails. + #### When a check does not run -A check can also end without a verdict. kapi exits `4` when the check did not run: it checked no content, or it could not show that its checkers are able to fail. The Action fails the step and reports this as its own result: `result` is `did_not_run`, `gate` stays empty, and `did_not_run_cause` carries the cause kapi named. The step's error annotation and the job summary state the cause in words. +A check can also end without a verdict. kapi exits `4` when the check did not run: it checked no content, or it could not show that its checkers are able to fail. The Action fails the step and reports this as its own result: `result` is `did_not_run`, `gate` stays empty, `status` is `failed`, and `did-not-run-cause` carries the cause kapi named. The step's error annotation, the job summary and, with `pr-comment`, the sticky PR comment state the cause in words. -| `did_not_run_cause` | What it means | +| `did-not-run-cause` | What it means | |---|---| | `checker_invalid` | A checker failed its canary, so the run's result cannot be trusted. Read it as neither a pass nor a gate failure, and fix or report the checker. | | `nothing_to_check` | There was nothing in scope to check. | @@ -232,7 +234,7 @@ Server-connected projects don't need this — the project state lives on the ser | `project` | | Path to the `kapi.yaml` recipe (`-p` flag) | | `plan` | `false` | With `command: up`: dry run — pending work, TM leverage, token estimate; no writes, no provider calls | | `fail-on-parked` | `false` | With `command: up`, fail the workflow when the run parks instead of reporting partial progress | -| `pr-comment` | `false` | Sticky report comment on pull-request events | +| `pr-comment` | `false` | Sticky report comment on pull-request events, including for a failed gate or a check that did not run | | `token` | `${{ github.token }}` | Token for the sticky PR comment | | `paths` | | Space-separated paths to scan for changes (whole working tree if empty) | @@ -240,13 +242,13 @@ Server-connected projects don't need this — the project state lives on the ser | Output | Description | |---|---| -| `status` | `success`, `no-changes`, or `failed` | +| `status` | `success`, `no-changes`, or `failed` (with `command: check`: a failed gate or a check that did not run) | | `outcome` | With `command: up`: `converged` or `parked` (a failed run fails the step, so it never reaches an output) | | `passes` | With `command: up`: how many reconciliation passes the run took | | `parked-locales` | With `command: up`: comma-separated locales still short of their gate | | `gate` | With `command: check`: `pass` or `fail` (empty when the check did not run or errored) | | `result` | With `command: check`: `passed` (exit 0), `failed` (exit 3), `did_not_run` (exit 4), or `error` (any other exit code) | -| `did_not_run_cause` | With `command: check`, when `result` is `did_not_run`: `checker_invalid`, `nothing_to_check`, `content_not_checked`, or `unknown` | +| `did-not-run-cause` | With `command: check`, when `result` is `did_not_run`: `checker_invalid`, `nothing_to_check`, `content_not_checked`, or `unknown` | | `plan-missing` / `plan-tm-exact` / `plan-ai-remaining` / `plan-token-estimate` | With `plan: true`: the plan totals | | `has-changes` | Whether the run left changes in the working tree for your delivery step | | `changed-files` | Newline-separated paths the run changed | diff --git a/action.yml b/action.yml index 9df978b..15e5d59 100644 --- a/action.yml +++ b/action.yml @@ -35,7 +35,8 @@ inputs: pr-comment: description: >- On pull-request events, post one sticky comment with the report (plan, - `kapi up` outcome, or gate result) that re-runs update in place. + `kapi up` outcome, or check result, including a failed gate or a check + that did not run) that re-runs update in place. Needs `permissions: pull-requests: write`. required: false default: "false" @@ -50,7 +51,7 @@ inputs: outputs: status: - description: "Run result: success, no-changes, or failed" + description: "Run result: success, no-changes, or failed (with `command: check`: a failed gate or a check that did not run)" value: ${{ steps.set-outputs.outputs.status }} outcome: description: "With `command: up`: `converged` or `parked` (empty for other commands)" @@ -67,13 +68,13 @@ outputs: result: description: "With `command: check`: `passed`, `failed`, `did_not_run`, or `error`" value: ${{ steps.run-kapi.outputs.result }} - did_not_run_cause: + did-not-run-cause: description: >- With `command: check`, when `result` is `did_not_run`: `checker_invalid` (a checker failed its canary, so the result cannot be trusted), `nothing_to_check`, `content_not_checked`, or `unknown` when kapi's output named no cause - value: ${{ steps.run-kapi.outputs.did_not_run_cause }} + value: ${{ steps.run-kapi.outputs.did-not-run-cause }} plan-missing: description: "With `plan: true`: pending units across all locales" value: ${{ steps.run-kapi.outputs.plan-missing }} @@ -226,7 +227,8 @@ runs: esac { echo "result=did_not_run" - echo "did_not_run_cause=${CAUSE}" + echo "did-not-run-cause=${CAUSE}" + echo "did-not-run-summary=${SUMMARY}. ${ADVICE}" } >> "$GITHUB_OUTPUT" ICON="⚠️" if [ "$CAUSE" = "checker_invalid" ]; then @@ -330,9 +332,15 @@ runs: fi fi + # The reporting steps below also run after the check step failed with a + # verdict (a failed gate, or a check that did not run), because reporting + # that outcome is what they are for. The job still fails: the check step + # already did, and a later step that succeeds does not undo it. A run that + # failed any other way reports nothing, so a delivery step is never handed + # the partial work of a broken `kapi up`. - name: Check for changes id: check-changes - if: inputs.plan != 'true' + if: ${{ !cancelled() && inputs.plan != 'true' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: PATHS: ${{ inputs.paths }} @@ -371,7 +379,8 @@ runs: fi - name: Sticky PR comment - if: inputs.pr-comment == 'true' && github.event_name == 'pull_request' + id: pr-comment + if: ${{ !cancelled() && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: GH_TOKEN: ${{ inputs.token }} @@ -382,6 +391,9 @@ runs: PASSES: ${{ steps.run-kapi.outputs.passes }} PARKED: ${{ steps.run-kapi.outputs.parked-locales }} GATE: ${{ steps.run-kapi.outputs.gate }} + RESULT: ${{ steps.run-kapi.outputs.result }} + CAUSE: ${{ steps.run-kapi.outputs.did-not-run-cause }} + DID_NOT_RUN_SUMMARY: ${{ steps.run-kapi.outputs.did-not-run-summary }} PLAN_MISSING: ${{ steps.run-kapi.outputs.plan-missing }} PLAN_TM: ${{ steps.run-kapi.outputs.plan-tm-exact }} PLAN_AI: ${{ steps.run-kapi.outputs.plan-ai-remaining }} @@ -413,6 +425,15 @@ runs: ## kapi check ${HEAD}" + elif [ "$RESULT" = "did_not_run" ]; then + ICON="⚠️" + if [ "$CAUSE" = "checker_invalid" ]; then + ICON="❌" + fi + BODY="${MARKER} + ## kapi check + + ${ICON} **Did not run** (\`${CAUSE}\`): ${DID_NOT_RUN_SUMMARY} See the [run log](${RUN_URL}) for the reasons." elif [ -n "$OUTCOME" ]; then BODY="${MARKER} ## kapi up report @@ -440,12 +461,16 @@ runs: - name: Set outputs id: set-outputs + if: ${{ !cancelled() && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: HAS_CHANGES: ${{ steps.check-changes.outputs.has_changes }} + RESULT: ${{ steps.run-kapi.outputs.result }} run: | set -euo pipefail - if [ "$HAS_CHANGES" = "true" ]; then + if [ "$RESULT" = "failed" ] || [ "$RESULT" = "did_not_run" ]; then + echo "status=failed" >> "$GITHUB_OUTPUT" + elif [ "$HAS_CHANGES" = "true" ]; then echo "status=success" >> "$GITHUB_OUTPUT" else echo "status=no-changes" >> "$GITHUB_OUTPUT" diff --git a/test/check-outcomes.sh b/test/check-outcomes.sh index 0401dfe..8d46a5f 100755 --- a/test/check-outcomes.sh +++ b/test/check-outcomes.sh @@ -1,8 +1,10 @@ #!/usr/bin/env bash # Runs the action's "Run kapi" step with `command: check` against the kapi stub # in test/stub, once per check outcome, and asserts the step's exit code, its -# outputs and its error annotation. The step script is read out of action.yml, -# so the code under test is the code the action ships. Needs yq (v4) and jq. +# outputs and its error annotation. It then runs the "Sticky PR comment" step +# with the outputs a case produced, against the gh stub, and asserts the body +# it would post. The step scripts are read out of action.yml, so the code under +# test is the code the action ships. Needs yq (v4) and jq. set -euo pipefail here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -15,6 +17,11 @@ if ! grep -q 'kapi-check-output.txt' "$work/step.sh"; then echo "could not read the run-kapi step out of action.yml" >&2 exit 1 fi +yq '.runs.steps[] | select(.id == "pr-comment") | .run' "$root/action.yml" > "$work/comment.sh" +if ! grep -q 'kapi-report' "$work/comment.sh"; then + echo "could not read the pr-comment step out of action.yml" >&2 + exit 1 +fi failures=0 @@ -62,8 +69,8 @@ run_case() { [ "$got" = "$want_result" ] || fail "$name" "result: got '$got', want '$want_result'" got="$(output_value gate "$dir/output")" [ "$got" = "$want_gate" ] || fail "$name" "gate: got '$got', want '$want_gate'" - got="$(output_value did_not_run_cause "$dir/output")" - [ "$got" = "$want_cause" ] || fail "$name" "did_not_run_cause: got '$got', want '$want_cause'" + got="$(output_value did-not-run-cause "$dir/output")" + [ "$got" = "$want_cause" ] || fail "$name" "did-not-run-cause: got '$got', want '$want_cause'" local error_line error_line="$(grep '^::error::' "$dir/stdout" || true)" @@ -90,6 +97,51 @@ run_case() { fi } +# comment_case NAME WANT_BODY runs the sticky PR comment step with the outputs +# case NAME wrote, and asserts the posted body contains WANT_BODY. An empty +# WANT_BODY means the step must post nothing. +comment_case() { + local name="$1" want="$2" + local dir="$work/cases/$name" + local out="$dir/output" + + local rc=0 + env \ + PATH="$here/stub:$PATH" GH_STUB_LOG="$dir/gh" \ + GH_TOKEN=unused REPO=neokapi/kapi-action PR_NUMBER=1 \ + RUN_URL="https://github.com/neokapi/kapi-action/actions/runs/1" \ + MODE="$(output_value mode "$out")" OUTCOME="$(output_value outcome "$out")" \ + PASSES="" PARKED="" \ + GATE="$(output_value gate "$out")" RESULT="$(output_value result "$out")" \ + CAUSE="$(output_value did-not-run-cause "$out")" \ + DID_NOT_RUN_SUMMARY="$(output_value did-not-run-summary "$out")" \ + PLAN_MISSING="" PLAN_TM="" PLAN_AI="" PLAN_TOKENS="" \ + RUNNER_TEMP="$dir/tmp" \ + bash "$work/comment.sh" > "$dir/comment.stdout" 2>&1 || rc=$? + + local before="$failures" + [ "$rc" = 0 ] || fail "$name comment" "exit code: got '$rc', want '0'" + if [ -z "$want" ]; then + [ ! -e "$dir/gh/body.md" ] || fail "$name comment" "posted a comment with nothing to report" + elif [ ! -f "$dir/gh/body.md" ]; then + fail "$name comment" "posted no comment" + else + case "$(cat "$dir/gh/body.md")" in + *"$want"*) ;; + *) fail "$name comment" "body lacks '$want'" ;; + esac + fi + + if [ "$failures" = "$before" ]; then + echo "ok $name comment" + else + sed 's/^/ | /' "$dir/comment.stdout" + if [ -f "$dir/gh/body.md" ]; then + sed 's/^/ > /' "$dir/gh/body.md" + fi + fi +} + # name args output exit rc result gate cause annotation run_case passed "content/en.json" passed.txt 0 0 passed pass "" "" run_case failed "--ship" failed.txt 3 3 failed fail "" "kapi gate unmet (exit 3)" @@ -107,6 +159,14 @@ run_case no-cause "content/en.json" "" run_case operational-error "content/en.json" "" 1 1 error "" "" "kapi check failed operationally (exit 1)" run_case usage-error "--no-such-flag" "" 2 2 error "" "" "kapi check failed operationally (exit 2)" +comment_case passed "✅ **Gate: pass**" +comment_case failed "❌ **Gate: unmet**" +comment_case checker_invalid-text "❌ **Did not run** (\`checker_invalid\`): a checker failed its canary, so this run's result cannot be trusted." +comment_case nothing_to_check-json "⚠️ **Did not run** (\`nothing_to_check\`): there was nothing in scope to check." +comment_case content_not_checked-ship "⚠️ **Did not run** (\`content_not_checked\`): content in scope was not checked." +comment_case no-cause "(\`unknown\`): kapi's output named no cause." +comment_case operational-error "" + if [ "$failures" -gt 0 ]; then echo "$failures assertion(s) failed" exit 1 diff --git a/test/fixture/fixture.kapi b/test/fixture/fixture.kapi index cb0b4e5..f3ec426 100644 --- a/test/fixture/fixture.kapi +++ b/test/fixture/fixture.kapi @@ -6,29 +6,12 @@ defaults: - fr - de -# Define content and flows. Each bare content entry maps a source glob to a -# target; kapi tools read the source content and edit, check, or translate it. -# The {lang} placeholder in a target fans output out per language. Runtime block -# state lives in .kapi/cache/blocks.db. -# -# content: -# - path: "src/locales/en/*.json" -# format: json -# target: "src/locales/{lang}/*.json" -# -# flows: -# # Monolingual: check source content against brand voice and terminology. -# brand-check: -# steps: -# - tool: brand-vocab-check -# - tool: brand-voice-check -# # Multilingual: translate the source into each target language. -# translate: -# steps: -# - tool: translate -# -# Tip: 'kapi init --framework ' pre-fills content for a known stack. -content: - - path: "content/en.json" - target: "content/{lang}.json" -flows: {} +# One source file whose French and German targets are deliberately absent, so +# the plan has pending work and the ship gate fails. A collection groups the +# content kapi reads; each entry maps a source path to its target, and {lang} +# fans the target out per language. +collections: + - name: app + content: + - path: "content/en.json" + target: "content/{lang}.json" diff --git a/test/stub/gh b/test/stub/gh new file mode 100755 index 0000000..7da0297 --- /dev/null +++ b/test/stub/gh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# A stand-in for the GitHub CLI, for testing the action's sticky PR comment +# without calling the GitHub API. It appends each call's arguments to +# $GH_STUB_LOG/calls and writes the body of a POST or PATCH to +# $GH_STUB_LOG/body.md. Listing comments returns none, so the action posts a +# new comment rather than editing one. +set -euo pipefail + +log="${GH_STUB_LOG:?GH_STUB_LOG must name a directory}" +mkdir -p "$log" + +args=() +prev="" +for arg in "$@"; do + if [ "$prev" = "-f" ] && [ "${arg#body=}" != "$arg" ]; then + printf '%s\n' "${arg#body=}" > "$log/body.md" + arg="body=" + fi + args+=("$arg") + prev="$arg" +done +printf '%s\n' "${args[*]}" >> "$log/calls" From f1515509f06e7c70e09cc599fff9953f4d9419fc Mon Sep 17 00:00:00 2001 From: Asgeir Frimannsson Date: Sun, 13 Sep 2026 17:25:20 +0200 Subject: [PATCH 2/3] test: revert the reporting conditions to prove the tests fail Temporary, reverted in the next commit. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VRid8i4qnuNfE7Lio73Uu4 --- action.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index 15e5d59..c4c7b64 100644 --- a/action.yml +++ b/action.yml @@ -340,7 +340,7 @@ runs: # the partial work of a broken `kapi up`. - name: Check for changes id: check-changes - if: ${{ !cancelled() && inputs.plan != 'true' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} + if: inputs.plan != 'true' shell: bash env: PATHS: ${{ inputs.paths }} @@ -380,7 +380,7 @@ runs: - name: Sticky PR comment id: pr-comment - if: ${{ !cancelled() && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} + if: inputs.pr-comment == 'true' && github.event_name == 'pull_request' shell: bash env: GH_TOKEN: ${{ inputs.token }} @@ -461,7 +461,6 @@ runs: - name: Set outputs id: set-outputs - if: ${{ !cancelled() && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: HAS_CHANGES: ${{ steps.check-changes.outputs.has_changes }} From 318309adfd32ef16cb642d702b9e65d1ab448a15 Mon Sep 17 00:00:00 2001 From: Asgeir Frimannsson Date: Sun, 13 Sep 2026 17:26:11 +0200 Subject: [PATCH 3/3] test: restore the reporting conditions after the must-fail proof Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01VRid8i4qnuNfE7Lio73Uu4 --- action.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index c4c7b64..15e5d59 100644 --- a/action.yml +++ b/action.yml @@ -340,7 +340,7 @@ runs: # the partial work of a broken `kapi up`. - name: Check for changes id: check-changes - if: inputs.plan != 'true' + if: ${{ !cancelled() && inputs.plan != 'true' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: PATHS: ${{ inputs.paths }} @@ -380,7 +380,7 @@ runs: - name: Sticky PR comment id: pr-comment - if: inputs.pr-comment == 'true' && github.event_name == 'pull_request' + if: ${{ !cancelled() && inputs.pr-comment == 'true' && github.event_name == 'pull_request' && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: GH_TOKEN: ${{ inputs.token }} @@ -461,6 +461,7 @@ runs: - name: Set outputs id: set-outputs + if: ${{ !cancelled() && (success() || steps.run-kapi.outputs.result == 'failed' || steps.run-kapi.outputs.result == 'did_not_run') }} shell: bash env: HAS_CHANGES: ${{ steps.check-changes.outputs.has_changes }}