Independent verification of the #129 checkpoint (checkpoint/2026-08-13-ci-workflows-129-sdk,
head 7cbd1e85) against main at 2d25598e. Filed from the main-line side; the
branch itself was not touched.
Three of these change what the work should be, so they are listed first.
1. The Gitleaks failure is a false positive — no secret, no rotation
Reproduced with the same pinned gitleaks 8.30.1 the workflow's container uses,
--redact, over 2d25598e..7cbd1e85:
RuleID : generic-api-key
File : scripts/check_sdk_runtime_fixtures.py
StartLine: 238
Entropy : 3.7254806
Match : pub_cache_key": "REDACTED"
Line 238 is a negative self-test sample:
"cache_key": "flutter-cache-3.47.0", "pub_cache_key": "flutter-pub-3.47.0"},
A cache-key literal in a synthetic receipt. generic-api-key fired on the _key
suffix plus the value's entropy. No containment or rotation is needed, and an
allowlist is the wrong fix — lowering the literal's entropy (e.g.
"flutter-pub-cache") keeps the scanner strict.
Related: secret-scan.yml on main now scopes to the checked-out ref by default
(#146), because this finding also failed the scan on unrelated branches.
2. The Flutter pin is correct — do not repin it
A review of this checkpoint flagged stable + 3.47.0 as unresolvable. Checked
against the live official manifest:
current_release.stable = 4cf24164269a5ebf0c16a028a00727d0e77bbb05
-> version 3.47.0, channel stable, dart 3.13.0, release_date 2026-08-12
revision 4cf24164... -> exactly one row: (3.47.0, stable)
sha256 26cd99d3... -> exactly one row: (3.47.0, stable)
All five fields in tests/fixtures/sdk-runtime-spec.yml match one official
immutable stable row. Stable 3.47.0 shipped 2026-08-12; 3.44.9 is the previous
stable. Acting on that finding would break a correct pin. The useful residue
is a validator that resolves the tuple against the manifest so it is never
checked by eye — and it belongs in the scheduled tier, since it is a calendar-
driven external fact.
3. catalog/runner-routing.yml does not exist
Referenced twice in review material. git cat-file -e says absent on both
7cbd1e85 and 2d25598e. The real file is catalog/workflow-routing.yml.
4. Receipts hash the caller's tree, not the called workflow (blocking)
All three reusables do a plain actions/checkout — which in a reusable resolves
the caller's repository — then hash
${{ github.workspace }}/.github/workflows/<callee>.yml as workflow_sha256:
dart-flutter-ci.yml:121,147
kotlin-android-ci.yml:117,241
qt-ci.yml:140,161
No use of job.workflow_* anywhere. An external consumer either fails (no such
file) or hashes its own lookalike, which then travels as callee provenance.
Same-repository fixtures cannot detect this by construction.
Suggested fix, simpler than a second checkout: don't hash bytes at all.
Record job.workflow_repository + job.workflow_sha + job.workflow_file_path.
A commit SHA is already a cryptographic identity for the content, and these come
from the runner context, which the caller cannot forge — strictly stronger than a
digest read out of a caller-controlled workspace, and it needs no token and does
not break on a private callee. (github.* stays bound to the caller in a
reusable, which is exactly why the job.workflow_* properties exist.)
If a byte digest is still wanted, take it from a second read-only path-scoped
checkout at repository: job.workflow_repository, ref: job.workflow_sha.
Needs a true external-caller test plus two negatives: caller without the file,
and caller with a different-bytes lookalike.
5. Evidence steps break the reusables' own advertised API (blocking)
The evidence steps are unconditional and assert one fixture's shape.
Qt — test_command is documented "Empty to skip", and the Test step honours
that, but the evidence step opens TEST_LOG unconditionally. With an empty
test_command the log never exists, so it dies with an unhandled
FileNotFoundError rather than a clean refusal. It also requires qmake and
hard-parses CTest's tests failed out of N / 100% tests passed. All three
"Empty to skip" inputs are now traps.
Android — the workflow says "Works for pure-JVM Kotlin and Android" with
setup_android: false by default, yet the evidence step unconditionally requires
five independent things:
./gradlew --version — a Gradle wrapper
trusted_root(ANDROID_HOME) — unset wherever the image does not preinstall the SDK
digest("gradle/verification-metadata.xml") — absent in most projects, so this fails on any runner
required_tasks hardcoded to a module literally named app
if not apks — pure-JVM produces none
Worth knowing: on main these three reusables have no outputs: and no
evidence step at all — the whole surface is new here and unreleased. So there is
no compatibility to preserve and no migration to write; this is the last moment
to shape the API before consumers pin it.
Suggested split: the reusable emits a generic execution receipt with a mode
discriminator and no fixture-specific hard failures; the canonical assertions
(APK, task graph, locks, verification metadata, CTest) live in the observer and
check_sdk_runtime_fixtures.py.
Test matrix: Android pure-JVM · Android canonical · Android with a different task
graph · Qt canonical CTest · Qt test_command: '' · Qt non-CTest runner · Flutter
with optional steps disabled.
6. The receipt validator accepts malformed digests
_is_sha (64 lowercase hex) is applied to exactly two fields — test_log_sha256
and build_log_sha256. These are checked for truthiness only:
pubspec_lock_sha256
apk_sha256 (a list — ["zzz"] passes)
lock_sha256 (a dict)
verification_metadata_sha256
The negative substitutions cover workflow_sha256, caller_sha, test_count
and some command/JVM fields, but never put a malformed value in the four above.
One shared receipt schema, reused by emitter, observer and negatives, would close
this and #5 together.
7. The root-ownership negative test can false-fail
_sdk_environment._trusted_root accepts info.st_uid not in {0, uid}, so a
root-owned root is always trusted. The "unowned" self-test
(check_sdk_runtime_fixtures.py:371) passes os.getuid() + 1, but the temp
files belong to the executing user. Run as root: files are uid 0, uid is 1,
0 not in {0, 1} is false, no error is raised, the negative case is accepted and
the test reports a false failure.
Extract a pure ownership predicate and test explicit (owner, mode) tuples instead
of building the case from the live filesystem. Do not weaken the production
rule — trusting root-owned toolchain roots is correct.
Verified against 7cbd1e85 and 2d25598e by reading the objects directly, not
the working tree.
Independent verification of the #129 checkpoint (
checkpoint/2026-08-13-ci-workflows-129-sdk,head
7cbd1e85) againstmainat2d25598e. Filed from the main-line side; thebranch itself was not touched.
Three of these change what the work should be, so they are listed first.
1. The Gitleaks failure is a false positive — no secret, no rotation
Reproduced with the same pinned
gitleaks 8.30.1the workflow's container uses,--redact, over2d25598e..7cbd1e85:Line 238 is a negative self-test sample:
A cache-key literal in a synthetic receipt.
generic-api-keyfired on the_keysuffix plus the value's entropy. No containment or rotation is needed, and an
allowlist is the wrong fix — lowering the literal's entropy (e.g.
"flutter-pub-cache") keeps the scanner strict.Related:
secret-scan.ymlonmainnow scopes to the checked-out ref by default(#146), because this finding also failed the scan on unrelated branches.
2. The Flutter pin is correct — do not repin it
A review of this checkpoint flagged
stable+3.47.0as unresolvable. Checkedagainst the live official manifest:
All five fields in
tests/fixtures/sdk-runtime-spec.ymlmatch one officialimmutable stable row. Stable 3.47.0 shipped 2026-08-12; 3.44.9 is the previous
stable. Acting on that finding would break a correct pin. The useful residue
is a validator that resolves the tuple against the manifest so it is never
checked by eye — and it belongs in the
scheduledtier, since it is a calendar-driven external fact.
3.
catalog/runner-routing.ymldoes not existReferenced twice in review material.
git cat-file -esays absent on both7cbd1e85and2d25598e. The real file iscatalog/workflow-routing.yml.4. Receipts hash the caller's tree, not the called workflow (blocking)
All three reusables do a plain
actions/checkout— which in a reusable resolvesthe caller's repository — then hash
${{ github.workspace }}/.github/workflows/<callee>.ymlasworkflow_sha256:dart-flutter-ci.yml:121,147kotlin-android-ci.yml:117,241qt-ci.yml:140,161No use of
job.workflow_*anywhere. An external consumer either fails (no suchfile) or hashes its own lookalike, which then travels as callee provenance.
Same-repository fixtures cannot detect this by construction.
Suggested fix, simpler than a second checkout: don't hash bytes at all.
Record
job.workflow_repository+job.workflow_sha+job.workflow_file_path.A commit SHA is already a cryptographic identity for the content, and these come
from the runner context, which the caller cannot forge — strictly stronger than a
digest read out of a caller-controlled workspace, and it needs no token and does
not break on a private callee. (
github.*stays bound to the caller in areusable, which is exactly why the
job.workflow_*properties exist.)If a byte digest is still wanted, take it from a second read-only path-scoped
checkout at
repository: job.workflow_repository,ref: job.workflow_sha.Needs a true external-caller test plus two negatives: caller without the file,
and caller with a different-bytes lookalike.
5. Evidence steps break the reusables' own advertised API (blocking)
The evidence steps are unconditional and assert one fixture's shape.
Qt —
test_commandis documented "Empty to skip", and the Test step honoursthat, but the evidence step opens
TEST_LOGunconditionally. With an emptytest_commandthe log never exists, so it dies with an unhandledFileNotFoundErrorrather than a clean refusal. It also requiresqmakeandhard-parses CTest's
tests failed out of N/100% tests passed. All three"Empty to skip" inputs are now traps.
Android — the workflow says "Works for pure-JVM Kotlin and Android" with
setup_android: falseby default, yet the evidence step unconditionally requiresfive independent things:
./gradlew --version— a Gradle wrappertrusted_root(ANDROID_HOME)— unset wherever the image does not preinstall the SDKdigest("gradle/verification-metadata.xml")— absent in most projects, so this fails on any runnerrequired_taskshardcoded to a module literally namedappif not apks— pure-JVM produces noneWorth knowing: on
mainthese three reusables have nooutputs:and noevidence step at all — the whole surface is new here and unreleased. So there is
no compatibility to preserve and no migration to write; this is the last moment
to shape the API before consumers pin it.
Suggested split: the reusable emits a generic execution receipt with a mode
discriminator and no fixture-specific hard failures; the canonical assertions
(APK, task graph, locks, verification metadata, CTest) live in the observer and
check_sdk_runtime_fixtures.py.Test matrix: Android pure-JVM · Android canonical · Android with a different task
graph · Qt canonical CTest · Qt
test_command: ''· Qt non-CTest runner · Flutterwith optional steps disabled.
6. The receipt validator accepts malformed digests
_is_sha(64 lowercase hex) is applied to exactly two fields —test_log_sha256and
build_log_sha256. These are checked for truthiness only:pubspec_lock_sha256apk_sha256(a list —["zzz"]passes)lock_sha256(a dict)verification_metadata_sha256The negative substitutions cover
workflow_sha256,caller_sha,test_countand some command/JVM fields, but never put a malformed value in the four above.
One shared receipt schema, reused by emitter, observer and negatives, would close
this and #5 together.
7. The root-ownership negative test can false-fail
_sdk_environment._trusted_rootacceptsinfo.st_uid not in {0, uid}, so aroot-owned root is always trusted. The "unowned" self-test
(
check_sdk_runtime_fixtures.py:371) passesos.getuid() + 1, but the tempfiles belong to the executing user. Run as root: files are uid 0,
uidis 1,0 not in {0, 1}is false, no error is raised, the negative case is accepted andthe test reports a false failure.
Extract a pure ownership predicate and test explicit (owner, mode) tuples instead
of building the case from the live filesystem. Do not weaken the production
rule — trusting root-owned toolchain roots is correct.
Verified against
7cbd1e85and2d25598eby reading the objects directly, notthe working tree.