From 24a2b70a5a71dd90e720cd1460a345a02ef65949 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 2 Sep 2026 14:59:35 -0400 Subject: [PATCH] ci: a boolean input never equals the string 'true' MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The refresh lever did not work, and the way it did not work is the point. Run 5270 dispatched with refresh=true. Its log: expression '(github.event_name == 'schedule' || github.event.inputs.refresh == 'true') && 'true' || 'false'' evaluated to '%!t(string=false)' trigger: event=workflow_dispatch IS_REFRESH='false' BUILD_REF='refs/heads/dev' trigger: raw inputs refresh='true' force_build='false' The input arrived as true and the comparison still said false. `type: boolean` delivers a real boolean, and GitHub expression semantics cast operands to numbers when their types differ — so `true == 'true'` compares 1 against NaN. My comment on the previous commit asserted the opposite, that Forgejo delivers inputs as strings, and asserted it without checking. The run went GREEN with every step skipped, because a refresh that evaluates false is indistinguishable from an ordinary push. A lever that silently does nothing is worse than no lever: it would have been trusted. Normalised through format(), which is representation-independent — a boolean true and a string 'true' both render 'true'. That is also why force_build was never bitten: it passes its raw value into an env var and compares in the shell, where everything is a string already. format() buys the same thing at expression level, which is where a step `if:` needs the answer. The diagnostic from the previous commit stays. It is what turned this from a guess into a measurement, and it is the only thing that would catch the same class of failure next time. Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01TTjbZZ6JirCMSaJzQV1RhA --- .forgejo/workflows/build.yml | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/.forgejo/workflows/build.yml b/.forgejo/workflows/build.yml index cb3a60b..85bd033 100644 --- a/.forgejo/workflows/build.yml +++ b/.forgejo/workflows/build.yml @@ -89,12 +89,30 @@ on: # makes the milestone-362 gate verifiable at all: a gate has to be watched # rejecting something before anyone can believe it is wired up. # -# Note this is a STRING comparison, not a boolean. Forgejo delivers -# workflow_dispatch inputs as strings, so `inputs.refresh` is 'true'/'false' -# and `&&` on it would treat the string 'false' as truthy. +# The input is normalised through `format()` before it is compared, and that +# is not defensive styling — the direct comparison is WRONG and fails silently. +# +# `type: boolean` delivers a real boolean, and GitHub expression semantics cast +# operands to numbers when their types differ: `true == 'true'` compares 1 +# against NaN and is FALSE. Measured on run 5270, whose own log says it — +# +# expression '(github.event_name == 'schedule' +# || github.event.inputs.refresh == 'true') && 'true' || 'false'' +# evaluated to '%!t(string=false)' +# trigger: raw inputs refresh='true' +# +# — the input arrived as `true` and the expression still said false. The run +# then went green with every step skipped, because a refresh that evaluates +# false behaves exactly like an ordinary push. That is the whole hazard: the +# failure has no symptom. +# +# `force_build` never hit this because it never compares in an expression. It +# passes the raw value into an env var and tests it in the shell, where +# everything is already a string. `format('{0}', x)` buys the same thing here, +# where a step-level `if:` needs the answer before any shell runs. env: - IS_REFRESH: ${{ (github.event_name == 'schedule' || github.event.inputs.refresh == 'true') && 'true' || 'false' }} - BUILD_REF: ${{ (github.event_name == 'schedule' || github.event.inputs.refresh == 'true') && 'main' || github.ref }} + IS_REFRESH: ${{ (github.event_name == 'schedule' || format('{0}', github.event.inputs.refresh) == 'true') && 'true' || 'false' }} + BUILD_REF: ${{ (github.event_name == 'schedule' || format('{0}', github.event.inputs.refresh) == 'true') && 'main' || github.ref }} # Requires repo secret RELEASE_TOKEN — a Forgejo PAT with scopes: # - write:package, read:package (for docker push to git.fabledsword.com)