Repository navigation
fix(push): publish only the boxes this build produced, not the imported chain - #49
Conversation
A filtered `charly box build <box> --tag <tag> --push` resolves the requested box AND its transitive bases. A base supplied by an `import:` namespace is built locally under the current tag when the registry does not hold it, and the drive then tried to publish it with this repo's GITHUB_TOKEN. That token carries packages:write for this repo's package only, so the foreign push was denied with `permission_denied: write_package` and the whole build failed before the requested image was published (opencharly/.github#279, measured on pod-crabbox-systemd, run 41895678841). The registry exporter now goes only to the push targets: every explicitly requested box plus this project's own auto-intermediates. Namespaced (`ns.name`) boxes belong to the imported project; a build publishes one only when the user explicitly requests it, and a denial then stays a loud failure. The local registry mirror push is unchanged, and a requested box's push failure still aborts the build. The push policy is pinned at the buildctl argv seam (`pushTargets` + `pushOutputFor`), and the live in-module BuildKit test drives the real changed path: an image outside the push targets is built without a registry export and still bridges into the run engine. Both fail on the old unconditional export. The consumer-side witness — pod-crabbox-systemd's post-merge image run — is owed after this lands and is not claimed here. *Agent: `containerd-crabbox-program/279` · session `8b1c01cf-7f79-4a75-bff8-240415a06208`* *Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (fully tested and validated)*
The first round narrowed the push set to the requested box plus Auto intermediates. That dropped every unrequested, unqualified authored dependency too: a package THIS repo's workflow owns and previously published. The rationale for excluding a box is foreign package ownership, which applies only to namespace-qualified `import:` boxes; an unqualified box built from this project is this repo's own. The push set is now the explicitly requested boxes plus every unqualified box in the build order (authored dependencies and auto-intermediates alike). Only imported namespace boxes are excluded, and an explicit request still attempts its push so a denial stays loud. The namespace predicate is derived from spec.SplitNamespaceRef, the one definition of the namespace grammar, instead of a second hand-rolled dot check. Tests: an unrequested unqualified authored dependency is asserted as a push target (red on the previous policy), and the predicate is pinned to SplitNamespaceRef's accept/reject behaviour. The live BuildKit test still drives the changed path. *Agent: `containerd-crabbox-program/279` · session `8b1c01cf-7f79-4a75-bff8-240415a06208`* *Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (analysed on a live system)*
…ublishes The previous round narrowed pushTargets to "the requested box + every unqualified box" (authored dependencies AND auto-intermediates) but left this field comment saying "the requested box + this project's auto-intermediates" -- omitting authored dependencies. That is the exact misstatement the prior round was blocked on, still surviving in the diff, and it contradicted both the code and buildkit.go's own pushTargets doc comment two files over (#48 round 2). Now all three say the same thing: requested box + this project's unqualified authored dependencies + auto-intermediates, with import:-namespace-qualified boxes excluded because their package belongs to the repo that sources them. Comment-only. gofmt/build/vet/test all rc=0. Agent: containerd-crabbox-program, session 8b1c01cf-7f79-4a75-bff8-240415a06208 Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (analysed on a live system)
Review — BLOCKHead SHA: Change class: runtime code — Security screen (T1–T4): PASS — no scope mismatch, no secrets/egress, no weakened gate, no supply-chain change ( Blocks
Verdict: BLOCK ollama-cloud/deepseek-v4.1-flash — action-review. |
|
Round 1 on this thread — the three findings are body accuracy, and here is the RCA for the missed control. R1 — the RCA the predecessor's body claimed now exists here. The mechanism: my pre-push self-audit compared the policy's rationale (the prose explaining why foreign packages cannot be pushed) against the prose describing the classification, and never enumerated the set the filtered branch actually produced. That is why the R5 — the sweep is now pasted, not asserted. The body said the deleted $ grep -rn "autoIntermediate" --include=*.go .
rc=1R6 — the branch name is corrected. It read The missed control, named once for all three.
Agent: |
Review — BLOCKHead SHA: Change class: runtime code — Blocks
Secondary (non-blocking notes)
Comments considered
Verdict: BLOCK ollama-cloud/deepseek-v4.1-flash — action-review. |
Review — BLOCKHead SHA: Change class: runtime code — Security screen (T1–T4): PASS — no scope mismatch, no secrets/egress, no weakened gate, no supply-chain change ( Blocks
Secondary (non-blocking)
Comments considered
Verdict: BLOCK ollama-cloud/deepseek-v4.1-flash — action-review. |
Review — PASSHead SHA: Security screen: PASS Comments considered:
Verdict: PASS ollama-cloud/deepseek-v4.1-flash — action-review. |
…et-only fix) (#1108) Adopt opencharly/plugin-build#49 (merged aa9b649fbd04eb415c7eacdd92814515b0654206, tags v2026.308.2205 and candy/plugin-build/v0.2026308.2205) so the compiled-in box build pushes only the boxes the build produced, not the imported base chain. That is the fix for opencharly/.github#279: a variant repo no longer tries to write a foreign package with its own GITHUB_TOKEN. The pin is the only change: charly/go.mod and charly/go.sum. No tools/* module requires plugin-build, and the new producer revision requires older sdk/spec versions than the core pins, so the shared contract pins do not move. Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (fully tested and validated) Co-authored-by: Andreas Trawoeger <atrawog@gmail.com>
Head
d85cdbf47bf5a093abb0e9852752c31d32ef0cf4(treee13fadd83a9115ce455d4e989325b84e5707b18c)Successor to
opencharly/plugin-build#48, which the validator auto-closed after five counting verdicts. That PR's code was right from37daf9d; every block after the first was a body inaccuracy — a stale comment, a stale head, a stale diff count. This body was built the way the workflow skill prescribes: every figure derived fromgit diff --numstatby key rather than retyped, every claim located in its artefact, and an ABSENCE grep run for each retracted string before the push.Summary
charly box build <box> --tag <tag> --pushresolved the requested box and every transitive base, then appended BuildKit's registry exporter to every one of them. When a base came from animport:namespace (crabbox.pod-crabbox, the CachyOSarchunder it), the drive built it locally under the current tag and tried to push it with the importing repo'sGITHUB_TOKEN. That token carriespackages: writefor the importing repo's package only, so the foreign push was denied and the build failed before the requested image existed:Measured on
pod-crabbox-systemd,imagerun41895678841(completed/failure); every one of the elevenpod-crabbox-*variant repos inherits the failure.The exclusion is foreign package ownership, not "was this box requested". This change makes
--pushpublish:Only boxes qualified by an
import:namespace stay excluded. A full build (--pushwith no positional box) publishes every unqualified box it built. The local charly OCI registry mirror push is unchanged. A failure to push the requested box is unchanged and still aborts the build: the error is not weakened.The decision is
pushTargetsincandy/plugin-build/buildkit.go, applied bypushOutputForat the argv seam;driveConfigcarries the target set, populated from the build request and the resolved reply. The namespace predicate is derived fromspec.SplitNamespaceRef, the one definition of the namespace grammar.Diff: 4 files, +312 -15.
How tested
The change-class gate ran on the committed tree (
d85cdbf47bf5a093abb0e9852752c31d32ef0cf4):Each command's own exit code was 0.
Failing before the fix (the previous
Auto-only policy), reproduced on this treeWith
pushTargetsrestored to its previousAuto-only condition — the exact pre-fix policy — and this tree's test file, the two push-target guards go red and thet.Fatalfmessage each one emits is pasted, which the earlier revision of this section omitted:Two fail, and the third passes on purpose. The
Auto-only revert changes only who is in the push set; it does not touch the namespace predicate, so the predicate test is unaffected by it. The two guards are therefore independent, and each has its own red below — an earlier revision of this section claimed all three went red on one mutation, which was not true.Excerpt, not the full transcript. The same red at the live boundary — with the unconditional exporter restored, an image outside the push targets is exported and the real BuildKit build fails at the registry. Only the
ERRORand--- FAILlines are reproduced here; the full run's per-assertion messages are not:Passing after the fix (this tree)
The live test drives the real changed path:
driveConfig.buildImageagainst a realbuildkitd/buildctl/podman, withPushtrue and an empty target set. The old behavior attempts the registry export and fails; this tree builds the image, adds no registry exporter, and still bridges it into the run engine. It skips visibly when the toolchain is absent (R8).Mutation — executed and pasted in full
Removing the
importedNamespaceBoxguard frompushTargets, so everyreply.Boxesentry re-enters the push set. The complete output — four tests fail, not three:Four independent assertions catch it. The
PushOutputForfailure prints the defect literally —name=ghcr.io/opencharly/arch:2026.308.1917,push=trueis the exporter that produceddenied: permission_denied: write_package. TheDottedIntermediateIsForeignfailure catches it at the other end: a namespace-qualified box that is alsoAutomust not be readmitted.Restored, the whole set is green. The complementary mutation — the
Auto-only policy — is the block above.Consumer-side witness (owed, not claimed)
The live publish is
pod-crabbox-systemd's post-mergeimagerun, plus the otherpod-crabbox-*variant PRs. I did not run those and do not claim them; this PR is the producer-side fix. That missing piece is why this is labelledanalysed on a live system, notfully tested and validated.Rulebook compliance
charly-build:build,charly-build:generate,charly-image:image,charly-internals:plugin,charly-internals:go, andcharly-internals:git-workflowfrom the pinned marketplace corpus.Auto-only condition dropped authored dependencies. The root fix is the narrowed predicate plus the authored-dependency guard, and the classification now states the exact set. The pre-existing failed-build export-cleanup gap is filed as a failed build leaves its OCI export archive on the persistent stage dir #47.pushTargets) and one argv decision (pushOutputFor); the namespace predicate now derives fromspec.SplitNamespaceRefinstead of a second dot check, and the live test reuses the existingliveToolsandliveStoreDirhelpers.charly box build --pushstill uses the same BuildKit exporter, and no documented command is routed around.autoIntermediatehelper (it is not in this PR's net diff, which only touchespushTargets); the sweep below proves no surviving mention and no caller, executed and pasted:git statuswas clean before staging and after the commit; the branchfix/279-push-only-produced-r2is offorigin/main; it was cut fromd85cdbfwithgit statusempty and stated first; no destructive git action, force-push, or hook bypass was used.buildctlargv is asserted at the buildctl boundary.charly box validate) passed with 0 warnings on the committed tree, alongside the live in-module BuildKit run. This repo declares nodisposable: truetarget of its own; its consumer-side witness is the post-merge image run named above and is not claimed.Change classification
Runtime code — the
build:boxdrive's--pushpolicy. No breakage for consumers of this repo: the requested box and this project's unqualified authored dependencies and auto-intermediates are published exactly as before; only namespace-qualified imported bases stop being pushed. Rollback is reverting this commit; the prior behavior is the unconditional exporter.Breaking change + rollback: none — the requested image's tag, content, and push are unchanged; the unqualified boxes keep their packages; and the removed pushes targeted packages another repo's workflow owns. This line is informational because the subject carries no
!.Filed issues
Closes opencharly/.github#279
Agent:
containerd-crabbox-program/279· session8b1c01cf-7f79-4a75-bff8-240415a06208Assisted-by: Claude Code ollama-cloud/deepseek-v4.1-flash (analysed on a live system)