The token-input convention¶
Several components authenticate to GitLab itself — the state backend
(tofu-plan,
tofu-apply), the jobs-artifacts API
(tofu-apply in ref mode), the package registry
(tofu-module-publish), the
GitLab API for opening MRs and cutting tags
(renovate-self,
releaser-pleaser,
release-plz). Every one of them
follows the same rule: the component exposes a string input for the
token, defaulting to $CI_JOB_TOKEN, and never references a consumer's
variable by name.
The problem this solves¶
A reusable component can't know what a given consumer calls their
credential. If tofu-apply hardcoded $MY_PROJECT_PLAN_TOKEN, it would
only work for the one project that happened to name its variable that. The
alternative — every consumer copy-pastes and edits the component — defeats
the point of having a shared component at all.
The fix is to make the token an input, defaulting to
GitLab's predefined $CI_JOB_TOKEN,
which every job gets automatically with no configuration:
include:
- component: gitlab.com/phpboyscout/cicd/[email protected]
inputs:
plan_token: $MY_ARTIFACT_PAT # consumer's variable, any name
A consumer whose tier and topology let the job token suffice configures nothing. A consumer who needs a scoped personal access token — a different GitLab tier, a cross-project backend, stricter least-privilege — passes it, under whatever name they've already chosen, without inheriting phpboyscout's naming conventions.
Where the default is enough — and where it isn't¶
$CI_JOB_TOKEN carries real permissions, and they're narrower than they
look:
- State-backend auth (
state_token) —CI_JOB_TOKENcarriesterraform_statepermission for the job's own project by default. Every phpboyscout stack stores its state in its own project, so the default just works. - Package-registry publish (
tofu-module-publish'stoken) —CI_JOB_TOKENcarries write access to its own project's package registry. This works even on GitLab Free, so publishing a module needs no personal access token at all. - The jobs-artifacts API (
tofu-apply'splan_tokeninrefmode) — this is where the default quietly stops being enough. Job-token auth for that specific endpoint is a Premium/Ultimate-only feature; on Free it returns a401. This surfaced for real the first timephpboyscout/infraran aref-mode apply — the component had assumed otherwise (2026-05-16-tofu-apply-plan-sources-v0.3.mdD2), and2026-05-19-token-inputs-v0.5.mdD3 corrected it: a Free-tier consumer must overrideplan_tokenwith a personal or group access token carrying job-artifact read.
The lesson generalises: document, in the input's description, exactly
when the default stops being sufficient and what scope an override needs
— don't make a consumer discover a silent 401 the hard way.
The self-referencing collision¶
One sharp edge, hit by renovate-self:
setting a job variable to the same name as the group variable it's meant
to read —
— renders, for the default input value, the self-referencing job variable
RENOVATE_TOKEN: "$RENOVATE_TOKEN". GitLab resolves a job variable that
references a group variable of the same name to a broken value, not the
group's real value — so Renovate authenticates with junk and GitLab returns
401 throttle_unauthenticated_api, for every consumer on the default token.
The fix is to alias the token to a non-colliding runtime variable name
(RENOVATE_TOKEN_RUNTIME) and hand it to the tool via explicit shell
expansion rather than another same-named variable assignment. release-plz
uses the identical _RUNTIME alias pattern for the same reason.
The rule that stays constant¶
Regardless of which endpoint or which default, three things never change:
- The component never hardcodes a credential.
- The component never names a consumer's CI variable — the input, not a convention, is the contract.
- A token reaches the runner only through a CI/CD variable; the consumer marks a PAT override Masked, keeping it out of job logs.
See also¶
- Reference:
tofu-apply,tofu-module-publish,renovate-self,releaser-pleaser,release-plz - Contributing: token inputs
- Spec:
2026-05-19-token-inputs-v0.5