Spec: phpboyscout/cicd v0.5 — configurable token inputs¶
- Repository:
gitlab.com/phpboyscout/cicd - Released as:
v0.5.0(minor — two new inputs across two components; one default reproduces v0.4 behaviour, the other fixes a broken feature). - Driver:
phpboyscout/infraPhase E ran the first realref-modetofu-applyand it failed at plan retrieval —CI_JOB_TOKENcannot authenticate the GitLab jobs-artifacts API on the Free tier. Fixing it cleanly means letting the consumer supply the token; generalising that is the convention below.
Summary¶
Two cicd components authenticate to GitLab with a token:
tofu-planandtofu-applyauthenticate the GitLab-managed HTTP state backend (TF_HTTP_PASSWORD);tofu-applyinrefmode authenticates the jobs-artifacts API to download the banked plan.
Both hardcoded $CI_JOB_TOKEN. For the state backend that is
correct — a job token carries terraform_state permission for its own
project. For the jobs-artifacts API it is wrong on GitLab Free:
job-token auth for that endpoint is a Premium/Ultimate feature (the
plan-sources spec D2 assumed otherwise), so every ref-mode apply
returned 401 at plan retrieval.
v0.5.0 makes both token uses configurable inputs and fixes the
ref-mode fetch.
Decisions¶
D1 — token inputs default to $CI_JOB_TOKEN; the consumer overrides¶
Wherever a component needs a token, it exposes a string input
defaulting to $CI_JOB_TOKEN — GitLab's predefined job-token
variable. The component never references a consumer's token variable
by name and never implies a naming convention; the consumer overrides
the input with whatever credential they hold:
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
plan_token: $MY_ARTIFACT_PAT # consumer's variable, any name
This keeps the components reusable by third parties. A consumer whose tier and topology let the job token suffice configures nothing; one who needs a scoped PAT — a different GitLab tier, a cross-project backend, stricter least-privilege — passes it, without inheriting phpboyscout's token names or conventions.
The default is a string ("$CI_JOB_TOKEN"); GitLab expands it as a
CI variable at job runtime, exactly as it expands a consumer's
override. A token value reaches the runner only through a CI variable,
so a consumer marks their PAT variable Masked to keep it out of
job logs.
D2 — state_token: GitLab state-backend auth (tofu-plan, tofu-apply)¶
tofu-plan and tofu-apply set TF_HTTP_PASSWORD for the
GitLab-managed HTTP state backend. New input state_token (string,
default $CI_JOB_TOKEN) replaces the hardcoded value:
The default is behaviour-preserving — CI_JOB_TOKEN carries
terraform_state permission for the job's own project, which is where
every phpboyscout stack's state lives. A consumer whose state is in a
different project, or an external HTTP backend, overrides state_token
with a suitable token. No phpboyscout consumer needs to.
D3 — plan_token: ref-mode plan retrieval, and the Free-tier fix¶
tofu-apply ref mode downloads the banked plan from
GET /projects/:id/jobs/artifacts/:ref/download. v0.3 authenticated
that with --header "JOB-TOKEN: $CI_JOB_TOKEN" and assumed (the
plan-sources spec D2) a job token authorises same-project artifact
download. It does not on GitLab Free — job-token auth for that
endpoint is Premium/Ultimate only; on Free the call returns 401.
v0.3's ref-mode self-test exercised only the failure path, deferring
the success path to infra Phase E — so this surfaced the first time
a real ref-mode apply ran.
New input plan_token (string, default $CI_JOB_TOKEN, per D1).
The ref-mode fetch authenticates with
--header "PRIVATE-TOKEN: $plan_token". The default keeps ref mode
working out of the box on Premium/Ultimate; a GitLab Free consumer
overrides plan_token with a personal or group access token carrying
job-artifact read — for a fine-grained token, the Job Artifact
resource with Read. job mode never reads the input.
The fetch is also no longer silent. v0.3's curl --silent --fail
collapsed every outcome — 401, 404, redirect, network — into one
opaque "no banked plan artifact" message; the Phase E failure could
not be diagnosed from the job log. v0.5.0 drops --fail, captures
%{http_code}, and on any non-200 prints the status code and the
(bounded) response body before exit 1. --retry 3 rides out
transient artifacts-CDN errors; --location still follows the 302 the
API issues to its CDN.
D4 — self-test¶
state_token— thetofu-plan/tofu-applyself-tests run against a GitLab HTTP backend fixture. Leavingstate_tokenunset exercises the$CI_JOB_TOKENdefault — i.e. v0.4 behaviour — so no self-test change is needed.plan_token— theref-mode self-test (tests/tofu-apply/ref.gitlab-ci.yml) passes a dummyplan_token, so the jobs-artifacts fetch returns a deterministic non-200 (401, the token is rejected) →exit 1, tolerated byallow_failure.exit_codes. This proves the input plumbing and the non-200 diagnostic path on every self-test pipeline, regardless of tier. Theref-mode success path keeps its real exercise inphpboyscout/infra(Phase E), as the plan-sources spec D6 set out.
D5 — versioning¶
v0.5.0 — minor. Two new inputs (state_token on two components,
plan_token on one). state_token's default reproduces v0.4
behaviour byte-for-byte. plan_token's default makes a Premium
consumer's ref-mode apply work unchanged and a Free consumer's fail
loudly and clearly until overridden — ref mode had no working
consumer before now, so there is no migration cost; job-mode
consumers are untouched. Minor rather than patch because of the new
inputs. The pre-1.0 caveat from v0.1 still applies.
This spec amends the plan-sources spec
(2026-05-16-tofu-apply-plan-sources-v0.3.md): D2's CI_JOB_TOKEN
claim is corrected by a note pointing here.
Risk register¶
| Risk | Mitigation |
|---|---|
A GitLab Free consumer leaves plan_token at its $CI_JOB_TOKEN default → ref-mode apply 401s |
The fetch prints HTTP 401 + the response body and a checklist naming plan_token; the input description and D3 document the Free-tier override. Loud, not silent. |
state_token left at default for a cross-project / external backend |
tofu init fails loudly at state auth; the default is correct for the same-project GitLab backend every phpboyscout stack uses. |
| Token value leaked into a job log | Tokens reach the runner only via CI variables; GitLab masks variables marked Masked. D1 notes the consumer must mark their PAT variable Masked. |
plan_token PAT over-scoped |
Job-artifact read on one project is the minimum; a fine-grained Job Artifact: Read token is read-only and narrow. Documented in D3. |
Implementation plan¶
- Spec lands — this file, status
approved. templates/tofu-plan.yml— addstate_token;TF_HTTP_PASSWORDreads it (D2).templates/tofu-apply.yml— addstate_token(D2) andplan_token(D3); theref-mode fetch usesPRIVATE-TOKENand reports the HTTP status on a non-200 (D3).tests/tofu-apply/ref.gitlab-ci.yml— pass a dummyplan_token(D4).- Plan-sources spec — add the D2 correction note.
- CHANGELOG
[0.5.0]; mergedevelop → main, tagv0.5.0, cut the Release. - Consumer (
phpboyscout/infraPhase E) bumpstofu-plan/tofu-applyto@v0.5.0and setsplan_tokento its artifact-read PAT.
Follow-ups¶
- The optional component-side pipeline-status check for
refmode (plan-sources spec OQ1) remains deferred.