No reviewers
Labels
No labels
blocked
blocker:ci-red
blocker:conflict
blocker:drill-pending
blocker:unrequested
bug
claimed
documentation
enhancement
epic
merge-next
needs-triage
ready
release
scope:apply
scope:capture
scope:coolify-api
scope:fleet
scope:manifest
scope:secrets
stale
state:addressing
state:bots-reviewing
state:building
state:needs-human
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference: heavy-duty/cast#16
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/clone-auth-instances-capture"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes #13, closes #14, closes #15. Three commits, in that order — each stands alone, and the later two lean on the earlier ones (
captureclones a private manifest, and takes--instance).#13 — clones authenticate, and never prompt
resolveCheckoutshelled out to a baregit cloneand relied entirely on the ambient credential helper. With none configured, git falls through to its interactive username/password prompt — which GitHub no longer accepts — and the error talks about the repository rather than about cast's missing credentials. Being logged intoghdoesn't help:gh auth loginalone does not wire git's helper (that'sgh auth setup-git, a separate act most people never run). Not routable around for prod, where--pathis refused and the clone is the only path.cast now resolves credentials itself:
ghborrowed as a per-invocation helper (no mutation of your global git config), thenGITHUB_TOKEN/GH_TOKEN, then the ambient helper.GIT_TERMINAL_PROMPT=0on every path. The token is never in the clone URL orhttp.extraheader— the helper reads it from the environment at run time, so what lands in argv is the literal text$CAST_GIT_TOKEN.Verified against a live private clone, not just unit tests — this box turned out to be the repro condition (gh logged in, no generic helper):
GITHUB_TOKENonly, no gh → clones ✓terminal prompts disabledand an actionable message ✓One thing worth knowing, since it's load-bearing and I checked it empirically rather than assuming: the empty
credential.helper=reset clears URL-scoped helpers (credential.https://github.com.helper— whatgh auth setup-gitwrites), not just generic ones. So cast's chosen credential is genuinely the one used.#14 — pick the Coolify by name, don't edit
.coolify.envThe connection target was implicit in a file's current contents. Retargeting meant hand-editing a live credential file and putting it back — and the failure mode of getting that wrong is running
applyagainst production.<state>/.coolify/<name>.env+--instance <name>on every verb that reaches Coolify.environments.yamlcan bind one per environment (instance: prod-cp), so--envselects the right control plane with no flag and no file edit. An explicit--instancestill wins.--instancerefuses, naming the ones that exist — same spirit as #12/D-237. Falling back to the default is how a diff meant for a legacy box gets run against prod.COOLIFY_READ_ONLY=trueon an instance makesapply/smoke/server addrefuse it before their first call, even though the token would permit the writes. The guard is the declaration, not the token's scope.With no flag and no binding, behavior is byte-for-byte what it was. The CLI tests spawn cast against stub Coolifys that record what they were asked, so "which instance did it actually talk to" is answered from the wire rather than from cast's console output.
#15 —
cast captureThe adoption path cast never had. Reads the required secret names from the manifest's own env templates, reads live values off the instance, and forces every name into a disposition:
generated_secrets→ literalpending-coolify-generated, never the live value$CAST_CAPTURE_<NAME>The mapping is deliberately not mechanical — that's the design. A
DATABASE_URLcopied off the source points at the source box's Postgres: confidently wrong, entirely plausible, and the target's real URL doesn't exist until Coolify creates the resource.generated_secretsis a manifest property rather than a flag you have to remember, because the manifest is what knowsDATABASE_URLcomes from a database it declares. An entry no template refers to is a hard error — a guard standing over nothing still reads like a guard, and the likeliest cause is a typo whose real name then gets captured instead of placeheld.Hygiene, each covered by a test asserting on real values: the plan prints names and provenance, never values; an override's value comes from the environment, never argv (
ps); plaintext is piped toageon stdin (never a temp file, stdout, or shell history); an existing store isn't overwritten without--force. It inherits diff's absent-target refusal, the team assert, and the--path/--env prodban. Last gate is a typed confirmation of the environment's name — there is no--yes.The end-to-end test decrypts the store cast wrote and asserts on its contents, so "exactly the names the manifest requires, no more and no fewer" is checked against real ciphertext.
Two things for the reviewer
capturefor it would put new code on the migration's critical path, which is the trade the issue argued against.capturecan't be used onincubatorprod until.infra/manifest.yamldeclaresgenerated_secrets(DATABASE_URL_PROD,REDIS_URL_PROD,UMAMI_DATABASE_URL) andenvironments.yamlgainsage_recipient. Both live in other repos, so they're not in this PR. Without them capture refuses (no recipient) rather than doing anything wrong — but it's worth knowing it isn't drop-in yet.Checks
npm run check(biome),npm run build(tsc),npm test— 151 passing, 16 files (was 118/14). Installer scripts passbash -n.🤖 Generated with Claude Code