Skip to content

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/infra Phase E ran the first real ref-mode tofu-apply and it failed at plan retrieval — CI_JOB_TOKEN cannot 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-plan and tofu-apply authenticate the GitLab-managed HTTP state backend (TF_HTTP_PASSWORD);
  • tofu-apply in ref mode 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:

variables:
  TF_HTTP_USERNAME: gitlab-ci-token
  TF_HTTP_PASSWORD: $[[ inputs.state_token ]]

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 — the tofu-plan / tofu-apply self-tests run against a GitLab HTTP backend fixture. Leaving state_token unset exercises the $CI_JOB_TOKEN default — i.e. v0.4 behaviour — so no self-test change is needed.
  • plan_token — the ref-mode self-test (tests/tofu-apply/ref.gitlab-ci.yml) passes a dummy plan_token, so the jobs-artifacts fetch returns a deterministic non-200 (401, the token is rejected) → exit 1, tolerated by allow_failure.exit_codes. This proves the input plumbing and the non-200 diagnostic path on every self-test pipeline, regardless of tier. The ref-mode success path keeps its real exercise in phpboyscout/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

  1. Spec lands — this file, status approved.
  2. templates/tofu-plan.yml — add state_token; TF_HTTP_PASSWORD reads it (D2).
  3. templates/tofu-apply.yml — add state_token (D2) and plan_token (D3); the ref-mode fetch uses PRIVATE-TOKEN and reports the HTTP status on a non-200 (D3).
  4. tests/tofu-apply/ref.gitlab-ci.yml — pass a dummy plan_token (D4).
  5. Plan-sources spec — add the D2 correction note.
  6. CHANGELOG [0.5.0]; merge develop → main, tag v0.5.0, cut the Release.
  7. Consumer (phpboyscout/infra Phase E) bumps tofu-plan / tofu-apply to @v0.5.0 and sets plan_token to its artifact-read PAT.

Follow-ups

  • The optional component-side pipeline-status check for ref mode (plan-sources spec OQ1) remains deferred.