Skip to content

Authoring cicd components

This guide is for anyone adding or changing a component in phpboyscout/cicd. It distils the conventions and intent behind the components into one place; the authoritative decision records are the dated specs in docs/development/specs/.

What the components are

phpboyscout/cicd is a monorepo of reusable GitLab CI/CD components, released together under one tag stream. Each component is a single templates/<name>.yml file. A consumer references it by URL and pins a tag:

include:
  - component: gitlab.com/phpboyscout/cicd/<name>@vX.Y.Z

Three goals shape every component:

  • Reusable — one component, many consumers; behaviour is driven by inputs, never by editing the template.
  • Caller-agnostic — a component knows nothing about a specific project, AWS account, branch, or token name. Consumer-specific facts arrive as inputs.
  • Third-party-friendly — the components are MIT-licensed and usable outside phpboyscout. Defaults assume the common case; anything a different consumer would need is an overridable input.

Components run inside registry.gitlab.com/phpboyscout/images/infra-tools. Tool versions are pinned in that image, not here.

Anatomy of a component

A component is one file in single-file form — an input spec, a --- separator, then the jobs:

# templates/<name>.yml
spec:
  component: [version]        # always — see below
  inputs:
    image_version: { type: string, default: "vX.Y.Z" }
    stage:         { type: string, default: "<stage>" }
    # ... component-specific inputs
---
<job-name>:
  stage: $[[ inputs.stage ]]
  image: registry.gitlab.com/phpboyscout/images/infra-tools:$[[ inputs.image_version ]]
  script:
    - echo "<name> $[[ component.version ]] (image $[[ inputs.image_version ]])"
    # ...

spec.component: [version] is mandatory: it lets a job echo the running component version as its first action, so a CI trace shows exactly which release ran.

Conventions

Inputs

  • image_version and stage are always inputs. Default them sensibly, never hardcode — the consumer's image pin and stage layout win.
  • No global keywords (stages:, default:, variables:) at the top level of a template. Declare everything per-job.
  • Path-list inputs are strings, not arrays (e.g. "a/* b/*"). The job word-splits them in shell; GitLab component arrays do not iterate.
  • A default serves the common case. If a value is consumer-specific, it is an input — not a hardcoded constant.

Token inputs

Where a component authenticates to GitLab — state-backend access, the jobs-artifacts API, the package registry — it must not hardcode a credential or name a consumer's CI variable. Instead:

  • Expose a string input for the token, defaulting to $CI_JOB_TOKEN — GitLab's predefined job token.
  • The consumer overrides the input with whatever credential they hold, under whatever name they chose:
inputs:
  plan_token: $MY_ARTIFACT_PAT     # consumer's variable, any name
  • Document, in the input's description, when the default is insufficient and what scope an override needs (e.g. "on GitLab Free, override with a PAT carrying job-artifact read").
  • A token value reaches the runner only through a CI variable; the consumer marks that variable Masked to keep it out of job logs.

This is what makes a token-using component reusable: a consumer on a tier and topology where the job token suffices configures nothing; one who needs a scoped PAT passes it, without inheriting phpboyscout's token names or conventions. See specs/2026-05-19-token-inputs-v0.5.md (D1).

rules: — match the trigger to where the job can run

A component's rules: must only let the job run where it can actually succeed:

  • Gate components (tofu-lint, tofu-security, …) carry an explicit unconditional rules: [{ when: on_success }] — a rule-less job is skipped in merge-request pipelines, so the gate would miss the MR (v0.4 spec).
  • OIDC jobs (tofu-plan, tofu-apply) run only where the cloud IAM trust policy accepts the pipeline's OIDC subject — MR and default branch for a plan, release tags for a ref-mode apply (v0.3 spec D3). A job that runs where assume-role will fail is a bug.

Interpolation: config-time vs runtime

  • $[[ inputs.x ]] is resolved when the pipeline config is assembled; $VAR is resolved by the runner at job time. A token input works because $[[ inputs.token ]] interpolates to the literal $VAR, which the runner then expands.
  • You cannot conditionally interpolate into needs: or rules:. When a component has modes needing different needs: / rules:, put each mode's wiring in a hidden job and select it with extends: ".<name>--$[[ inputs.mode ]]" (v0.3 spec D1).
  • A boolean input does not interpolate into rules: when:. Use a string input with options: instead (v0.2 spec, apply_when).

Cloud auth — no static credentials

A component that touches a cloud provider authenticates via OIDC (id_tokens:AssumeRoleWithWebIdentity). Never take an access key as an input or a variable.

Workflow

  1. Spec first. No template change without a spec it implements — an addendum to an existing spec, or a new docs/development/specs/<YYYY-MM-DD>-<slug>.md. The spec is the decision record and carries status: draft | approved | implemented in frontmatter. Implement only an approved spec.
  2. Write the template per the conventions above.
  3. Self-test. Add tests/<name>/fixture/ — the minimal artefact the component operates on — and tests/<name>/.gitlab-ci.yml, a child pipeline including the component from $CI_SERVER_FQDN/$CI_PROJECT_PATH/<name>@$CI_COMMIT_SHA. Add a trigger job to the root .gitlab-ci.yml. A component without a self-test does not merge.
  4. CHANGELOG — an entry under [Unreleased].
  5. Review and release — MR to develop; once green, merge develop → main, promote the CHANGELOG entry, tag vX.Y.Z.

Versioning

Semantic versioning, with a pre-1.0 caveat: while the major is 0, a minor bump may change input shape. Consumers pin @vX.Y.Z.

  • New component, or new input → minor — even when the input has a behaviour-preserving default; the input surface grew.
  • Bug fix with no input change → patch.

Decision records

Spec Decides
2026-05-15-cicd-v0.1.md Gate components; single-file form; the infra-tools image; the core input rules.
2026-05-16-tofu-plan-apply-v0.2.md tofu-plan / tofu-apply; OIDC auth, no static credentials; the GitLab HTTP state backend; apply_when.
2026-05-16-tofu-apply-plan-sources-v0.3.md plan_source (job / ref); the hidden-job extends: pattern; mode-coupled rules:; tag_pattern.
2026-05-18-gate-component-rules-v0.4.md Gate jobs carry an explicit rules: so they run in merge-request pipelines.
2026-05-19-token-inputs-v0.5.md Token-requiring inputs default to $CI_JOB_TOKEN; the consumer overrides.