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 only — minimal 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.yml — rust-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 githubon aminimalproject is rejected (minimal has no CI surface) with a clear error. --ci-component-sourceoverrides 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-disttag 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-distworkflow; 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-sourceNFC-normalised + validated viavalidate.rs(new source char-class: host/owner/path form; reject traversal, control chars, length cap), mirroring GTB'sValidateCIComponentSource.- The generated repo path interpolated into
renovate-self/release-plzinputs is the already-validated project name/path — no new untrusted surface. .gitlab-ci.ymland.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-componentsmarker region + manifestci: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-ciopt-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(defaultgitlab); 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-sourcevalidation (accept good, reject traversal/ control/over-length); component-marker-region render with the version constant; manifestci:round-trip;minimal+CI flags → error. - E2E (
assert_cmd):generate project foo(cli) emits a.gitlab-ci.ymlwhose component pins resolve and whose YAML parses;--no-ciomits it;--ci-component-source <x>rewrites the include base; regenerate refreshes theci-componentsregion but preserves a user-addedbefore_scriptanchor outside it; Phase B:--ci-platform githubemits parseable.github/workflows/ci.yml. - Generated
.gitlab-ci.ymlis 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 namesgitlab-cias a suppressible component (§6) and.gitlab-ci.ymlas 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 thecredentials(keychain→libdbus) feature, and a machine-id seed fortelemetry. 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_scriptsetup (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).