Skip to content

Commit c35b098

Browse files
committed
ci(half-state-patrol): declared opt-in for a board with no anchor issue
The composite action gains one input, `anchor-optional`, default `false`. Declared `true`, an empty `anchor-issue` gives a ::notice::, exit 0, and the findings in the run summary only; the anchor PATCH is skipped through the resolve step's `configured` output. Left at the default, the empty-anchor ::error:: and exit 1 are unchanged. A non-numeric anchor fails in both modes, and a value other than true or false is refused on every event. objectstack's own caller keeps the default; its prose names the input for sibling installs. Claude-Session: https://claude.ai/code/session_01TdiauJaVCHuj45EzZGUxHh Co-authored-by: Claude <noreply@anthropic.com>
1 parent 41dcf11 commit c35b098

2 files changed

Lines changed: 81 additions & 7 deletions

File tree

‎.github/actions/half-state-patrol/action.yml‎

Lines changed: 75 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -121,12 +121,25 @@
121121
# board identifier that can be typed is a board identifier that can be typed
122122
# wrong, and a patrol sweeping the repo it was installed FROM would file a fully
123123
# green report about the wrong board.
124+
#
125+
# ## An empty anchor: loud by default, summary-only by declaration (#20793)
126+
#
127+
# An empty `anchor-issue` cannot tell "this board has no anchor" from "this
128+
# caller lost its variable", and only the second is a lapse, so by default it
129+
# fails the run. A caller whose board has no anchor BY DECISION says so by name
130+
# (`anchor-optional: true`) and gets the other reading: a `::notice::`, a
131+
# passing run, and the findings in the run summary only -- which notifies
132+
# nobody, the accepted trade of that configuration. ⛔ The opt-in never widens:
133+
# a non-numeric anchor fails in both modes, and so does a sweep that could not
134+
# run.
124135

125136
name: Half-state patrol
126137
description: >-
127138
Sweep the calling repository's board for half-states and rewrite that
128-
repository's pinned anchor issue with the result. Report-only: findings never
129-
fail the run, but a sweep that could not run does.
139+
repository's pinned anchor issue with the result -- or, for a caller that
140+
declares `anchor-optional` and passes no anchor, deliver it to the run summary
141+
only. Report-only: findings never fail the run, but a sweep that could not run
142+
does.
130143
131144
inputs:
132145
github-token:
@@ -142,9 +155,24 @@ inputs:
142155
body this patrol owns end-to-end. An issue number is only ever meaningful
143156
in the repo it was minted in, so there is no default and no fallback: left
144157
empty, the run says so loudly and fails rather than guessing a number and
145-
rewriting an unrelated card four times a day.
158+
rewriting an unrelated card four times a day -- unless the caller has
159+
declared `anchor-optional`, below.
146160
required: false
147161
default: ''
162+
anchor-optional:
163+
description: >-
164+
`true` declares that an EMPTY `anchor-issue` is this caller's supported
165+
configuration: that one case becomes a `::notice::` and a passing run,
166+
with the findings delivered to the run summary only. It exists for one
167+
consumer, objectui, whose maintainer declined an anchor there (#20793,
168+
decision A). The default, `false`, stays LOUD -- an empty anchor fails the
169+
run -- because without the declaration an empty value reads the same as a
170+
caller that lost its variable, and objectstack's own caller must go red
171+
when that happens. It never widens: a non-numeric `anchor-issue` fails
172+
either way, and a value other than true or false is refused on every
173+
run, pull_request included.
174+
required: false
175+
default: 'false'
148176
closed-floor:
149177
description: >-
150178
Optional `YYYY-MM-DD` floor for the sweeper's closed-card pass, passed
@@ -169,6 +197,7 @@ runs:
169197
env:
170198
ACTION_PATH: ${{ github.action_path }}
171199
GH_TOKEN_IN: ${{ inputs.github-token }}
200+
ANCHOR_OPTIONAL_IN: ${{ inputs.anchor-optional }}
172201
run: |
173202
set -euo pipefail
174203
@@ -201,6 +230,24 @@ runs:
201230
exit 1
202231
fi
203232
233+
# `anchor-optional` is judged HERE, on every event, although only the
234+
# anchor steps read it: a pull_request run skips those steps, so a
235+
# misspelt opt-in judged there would pass the PR that adds it and first
236+
# fail on the scheduled run after. The accepted spellings are the YAML
237+
# 1.2 core-schema booleans `core.getBooleanInput` takes; anything else
238+
# is refused rather than read as `false`, because a misspelt `true` read
239+
# that way goes red with the no-anchor error, which names the wrong
240+
# remedy.
241+
case "$ANCHOR_OPTIONAL_IN" in
242+
true|True|TRUE) anchor_optional=true ;;
243+
false|False|FALSE) anchor_optional=false ;;
244+
*)
245+
echo "::error::The 'anchor-optional' input is '$ANCHOR_OPTIONAL_IN'. It takes true or false: true when this repository's board has no anchor issue by decision, false (the default) otherwise."
246+
exit 1
247+
;;
248+
esac
249+
echo "anchor_optional=$anchor_optional" >> "$GITHUB_OUTPUT"
250+
204251
# The runtime floor travels WITH the sweeper, read from the .nvmrc of
205252
# the tree this action shipped from. Deliberately not a literal typed
206253
# here: a second hand-written Node pin is the exact drift
@@ -304,24 +351,41 @@ runs:
304351
# findings (the same "land the truth, then raise the alarm" order the final
305352
# step keeps), and skipped on a pull_request run, which never writes an
306353
# anchor at all.
354+
#
355+
# The one exception to "loudly" is DECLARED, never inferred: a caller that
356+
# passed `anchor-optional: true` gets a `::notice::` and exit 0 on an empty
357+
# anchor, and this step publishes `configured` so the write below runs
358+
# exactly when delivery was asked for. ⛔ Do not re-derive emptiness in any
359+
# other step: a second spelling of it would drift, and the drift that
360+
# matters is a run that reports "no anchor" and then PATCHes issue 0.
361+
id: anchor
307362
if: github.event_name != 'pull_request'
308363
shell: bash
309364
working-directory: ${{ github.workspace }}
310365
env:
311366
ANCHOR_ISSUE: ${{ inputs.anchor-issue }}
367+
ANCHOR_OPTIONAL: ${{ steps.sources.outputs.anchor_optional }}
312368
run: |
369+
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ] && [ "$ANCHOR_OPTIONAL" = true ]; then
370+
echo "configured=false" >> "$GITHUB_OUTPUT"
371+
echo "::notice::No anchor issue configured for ${{ github.repository }}, which is a supported configuration for this caller: it declares 'anchor-optional: true' on the half-state-patrol action. The sweep RAN and its rendered body is in this run's summary; nothing was written to any issue, and the missing anchor does not fail this run. To deliver findings to a pinned issue instead, open a \`tracking\`-labeled anchor issue in this repo and pass its number as the 'anchor-issue' input."
372+
exit 0
373+
fi
313374
if [ -z "${ANCHOR_ISSUE//[[:space:]]/}" ]; then
314375
echo "::error::No anchor issue configured for ${{ github.repository }}. The sweep RAN (see the run summary) but has nowhere to land. Open a \`tracking\`-labeled anchor issue in this repo and pass its number as the 'anchor-issue' input of the half-state-patrol action."
315376
exit 1
316377
fi
317378
case "$ANCHOR_ISSUE" in
318379
*[!0-9]*|'') echo "::error::The 'anchor-issue' input is '$ANCHOR_ISSUE', which is not an issue number."; exit 1 ;;
319380
esac
381+
echo "configured=true" >> "$GITHUB_OUTPUT"
320382
echo "anchor: #$ANCHOR_ISSUE in ${{ github.repository }}"
321383
322384
- name: Update the pinned anchor issue
323-
# A pull_request run proves the sweep; it must not touch the board.
324-
if: github.event_name != 'pull_request'
385+
# A pull_request run proves the sweep; it must not touch the board. And a
386+
# declared anchor-less install has no board write to attempt: the guard
387+
# reads the resolve step's OUTPUT rather than testing the input again.
388+
if: github.event_name != 'pull_request' && steps.anchor.outputs.configured == 'true'
325389
uses: actions/github-script@v9
326390
env:
327391
SWEEP_EXIT: ${{ steps.sweep.outputs.exit_code }}
@@ -401,6 +465,12 @@ runs:
401465
if [ "${{ github.event_name }}" = "pull_request" ]; then
402466
echo "_Anchor write skipped: a pull_request run proves the sweep without touching the board._"
403467
echo
468+
elif [ "${{ steps.anchor.outputs.configured }}" = "false" ]; then
469+
# ⛔ Only on the EXPLICIT not-configured reading. An empty anchor
470+
# without the opt-in, or a non-numeric one, sets no output and fails
471+
# the run, and must not be described here as "nothing was asked for".
472+
echo "_No anchor issue configured (\`anchor-optional\` is on and \`anchor-issue\` is empty): this summary is the ONLY delivery of the findings below — it notifies nobody, by accepted trade. Pass a \`tracking\` issue's number as \`anchor-issue\` to have them land on a pinned card instead._"
473+
echo
404474
fi
405475
echo '<details><summary>Rendered anchor body</summary>'
406476
echo

‎.github/workflows/half-state-patrol.yml‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ name: Half-State Patrol
116116
#
117117
# plus one `tracking`-labeled anchor issue opened in that repo, whose number is
118118
# what `anchor-issue` carries. ⛔ A sha, never `@main`: `@main` is the drift this
119-
# change removes, re-entered from the other side.
119+
# change removes, re-entered from the other side. A board with no anchor BY
120+
# DECISION adds `anchor-optional: true` instead of opening one, and its findings
121+
# then reach run summaries only; the action's header says why that is an opt-in
122+
# (#20793), and this caller keeps the default.
120123
#
121124
# ⚠️ The paragraph this replaces was a LIST of files to copy, and it is worth
122125
# recording why a list is not the fix. It said TWO files until 2026-09-03 while
@@ -279,7 +282,8 @@ jobs:
279282
#
280283
# Resolution: the repository variable `HALF_STATE_ANCHOR_ISSUE` if
281284
# set, else this repo's own pinned number, else EMPTY — and empty
282-
# makes the action refuse to write rather than guess. The literal is
285+
# makes the action refuse to write rather than guess, because this
286+
# caller leaves `anchor-optional` at its loud default. The literal is
283287
# guarded by the repository name even though this file no longer
284288
# travels: the cheapest way to adopt the patrol is still to copy this
285289
# caller, and an unguarded fallback is what would let such a copy

0 commit comments

Comments
 (0)