Skip to content

Scaffolder CI-pipeline scaffolding

Status: APPROVED — GTB-parity gap from the v0.22.0 audit (Phase 2, §A). RTB's presets ship no CI file at all; GTB scaffolds a full GitLab pipeline from phpboyscout/cicd components. Product decisions resolved 2026-06-26 (§7). Delivery is phased (§6): Phase A = GitLab, Phase B = GitHub Actions.

Resolutions (binding, §7): - Default-on, with --no-ci to opt out (R-1). - cli preset onlyminimal stays bare (R-2). - Full pipeline mirroring RTB's own .gitlab-ci.yml (gates + pages + release + renovate) (R-3). - Both platforms supported via --ci-platform <gitlab|github> (default gitlab); GitHub Actions is Phase B (R-4).

1. Motivation

A scaffolded RTB tool is not ship-ready: it has no lint/test/security gate, no release automation, no docs deploy. RTB's own repo runs a proven pipeline assembled from phpboyscout/cicd Rust components (.gitlab-ci.ymlrust-lint, rust-test, rust-security, rust-docs, zensical-pages, release-plz, renovate-self). This spec scaffolds that same shape into generated cli-preset projects, so a new tool is gated + releasable from commit one. GTB does the equivalent for Go (internal/generator/assets/skeleton-gitlab/.gitlab-ci.yml + --ci-component-source).

2. Surface

rtb generate project <name> [--no-ci]
                            [--ci-platform <gitlab|github>]   # default gitlab
                            [--ci-component-source <base>]    # gitlab only; default gitlab.com/phpboyscout/cicd
  • cli preset only. --no-ci / --ci-platform github on a minimal project is rejected (minimal has no CI surface) with a clear error.
  • --ci-component-source overrides the cicd include base (mirrors GTB); validated (§5).
  • Default invocation (rtb generate project foo) → cli preset with .gitlab-ci.yml.

3. What gets emitted

Phase A — GitLab (.gitlab-ci.yml)

A minijinja template crates/rtb-cli-bin/templates/cli/.gitlab-ci.yml.j2, modelled on RTB's own pipeline:

  • Gate (MR-only): rust-lint, rust-test (coverage + integration + cross-os inputs), rust-security, rust-docs.
  • Release/deploy: release-plz (default branch), zensical-pages (docs site), cargo-dist tag job placeholder.
  • Schedule: renovate-self (repositories = the generated repo path).
  • Component pins via {{ ci_component_source }}/<name>@{{ cicd_version }}.
  • The include: list lives in a marker region so version bumps + regenerate are non-destructive over user edits:
# rtb:ci-components-begin
include:
  - component: gitlab.com/phpboyscout/cicd/[email protected]
  # …
# rtb:ci-components-end

Everything outside the region (project-specific before_script anchors — e.g. the dbus/machine-id setup RTB itself adds) is preserved verbatim across regenerate, exactly like other generated files.

Phase B — GitHub Actions (.github/workflows/)

No cicd-equivalent component library exists on GitHub, so these are hand-rolled but stage-for-stage equivalent:

  • ci.yml (on PR): fmt-check + clippy (dtolnay/rust-toolchain), cargo nextest, cargo llvm-cov, cargo-deny, cargo doc.
  • release.yml (on tag / default branch): release-plz/action + cargo-dist workflow; Pages deploy for the docs site.
  • Renovate via a committed renovate.json (the cicd preset) — no scheduled job needed (GitHub-hosted Renovate app or Actions).
  • Action versions pinned to a baked constant set (§4), Renovate/Dependabot carries them forward in the generated repo.

4. Version pinning

Bake a CICD_COMPONENT_VERSION constant (and a GitHub action-pin set) into rtb-cli-bin, seeded to RTB's current known-good (v0.18.1). The scaffolder renders that constant into the pins; Renovate in the generated repo (the cicd preset extended in renovate.json) keeps them current thereafter. The constant is bumped deliberately as RTB upgrades its own pins — never floated. Mirrors GTB's {{ .CICDComponentVersion }}.

5. Security / validation

  • --ci-component-source NFC-normalised + validated via validate.rs (new source char-class: host/owner/path form; reject traversal, control chars, length cap), mirroring GTB's ValidateCIComponentSource.
  • The generated repo path interpolated into renovate-self / release-plz inputs is the already-validated project name/path — no new untrusted surface.
  • .gitlab-ci.yml and .github/workflows/* are protected output paths for the custom-template overlay spec (overlays may not overwrite them); this spec is what creates them. See cross-impact.

6. Phased delivery

  • Phase A (M) — GitLab .gitlab-ci.yml + --no-ci + --ci-component-source + ci-components marker region + manifest ci: block + version constant + regenerate integration. Unblocks the custom-template overlay spec.
  • Phase B (M→L)--ci-platform github + .github/workflows/* + action-pin set.

7. Resolutions (resolved 2026-06-26)

  • [R-1] Default-on, --no-ci opt-out (batteries-included; presets ship none today). Rejected: opt-in --ci.
  • [R-2] cli preset only (minimal stays a bare learning/embedding skeleton — no rtb dep, no release story).
  • [R-3] Full pipeline mirroring RTB's own (gates + zensical-pages + release-plz + renovate-self). Rejected: gates-only-with-flags (a real downstream wants the whole thing; flags can trim later if asked).
  • [R-4] Both platforms via --ci-platform (default gitlab); GitHub Actions is Phase B (no cicd component library there → larger, hand- rolled). Default stays GitLab (RTB's infra + release-plz home).

8. Manifest schema

Add to Manifest:

#[serde(default, skip_serializing_if = "Option::is_none")]
pub ci: Option<CiConfig>,   // platform, component_source, cicd_version pin

regenerate reads it to re-emit/refresh the pipeline (refreshing only the # rtb:ci-components-* region; user setup anchors preserved). Additive + skip_serializing_if to preserve deny_unknown_fields round-tripping.

9. Testing (TDD)

  • Unit: --ci-component-source validation (accept good, reject traversal/ control/over-length); component-marker-region render with the version constant; manifest ci: round-trip; minimal+CI flags → error.
  • E2E (assert_cmd): generate project foo (cli) emits a .gitlab-ci.yml whose component pins resolve and whose YAML parses; --no-ci omits it; --ci-component-source <x> rewrites the include base; regenerate refreshes the ci-components region but preserves a user-added before_script anchor outside it; Phase B: --ci-platform github emits parseable .github/workflows/ci.yml.
  • Generated .gitlab-ci.yml is valid YAML (serde_yaml parse assertion); GitHub workflows parse as valid workflow YAML.

10. Cross-impact

  • 2026-06-23-scaffolder-custom-template-overlays.md — UNBLOCKS IT. That spec names gitlab-ci as a suppressible component (§6) and .gitlab-ci.yml as a protected path (§7), but RTB shipped no such file. This spec creates it, resolving the sequencing dependency recorded in the Phase-2 audit. Land Phase A before/with the overlay work.
  • 2026-06-23-scaffolder-feature-toggle.md — cross-link (deferred). RTB's own pipeline adds a dbus apt-setup anchor because it enables the credentials (keychain→libdbus) feature, and a machine-id seed for telemetry. A future refinement can make the scaffolded setup anchors conditional on enabled features (toggle spec). v0.1 emits a feature-agnostic baseline + a commented hint; conditional setup is out of scope here.

11. Out of scope

  • Conditional, feature-aware before_script setup (cross-link to feature-toggle; future).
  • Bitbucket/Gitea/other forges (rtb-vcs supports them as release sources, but CI scaffolding is GitLab + GitHub only for v0.1).
  • Trimming the GitLab pipeline to a gates-only subset via flags (rejected R-3; revisit only on demand).