feat(skills): /ci-scaffold 5-phase pipeline
This commit is contained in:
parent
7e2afc366b
commit
cd7a983f98
6 changed files with 596 additions and 0 deletions
89
skills/ci-scaffold/SKILL.md
Normal file
89
skills/ci-scaffold/SKILL.md
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
---
|
||||
name: ci-scaffold
|
||||
description: Hub-and-spoke pipeline that produces a production-grade CI/CD plan and scaffolds the workflow files for a new or existing repo — platform choice (GitHub Actions vs Forgejo Actions), build matrix, OIDC-vs-token secrets posture, release automation, and a security gate — via pure-click decisions across five phases. Emits `.github/workflows/*.yml` or `.forgejo/workflows/*.yml`, a secrets-env scaffold (RULE 0.8), and runs `kei-ci-lint` before handing off. Never writes secret values.
|
||||
argument-hint: <one-line repo description, e.g. "Rust axum service, deploys to AWS via OIDC, crates.io publish on tag">
|
||||
---
|
||||
|
||||
# CI-Scaffold — CI/CD Pipeline Generator (index)
|
||||
|
||||
You are converting "I need CI for repo X" into a reviewable, concrete plan plus generated workflow files: which platform (GH Actions vs Forgejo), what build matrix, how secrets flow (OIDC vs PAT), which release tool, and which security scanners block merge. Every decision is a click; the only typed input is the Phase 1 intake paragraph.
|
||||
|
||||
This skill scaffolds workflow YAML. It does NOT commit on the user's behalf and NEVER writes secret values. After Phase 5 it runs `_primitives/kei-ci-lint.sh` and walks the user through any violations via AskUserQuestion (fix / skip / abort).
|
||||
|
||||
The skill reads four companion blocks heavily — every phase references at least one:
|
||||
|
||||
- `_blocks/ci-github-actions.md` — GH Actions: OIDC, matrix, cache, reusable, least-privilege token.
|
||||
- `_blocks/ci-forgejo-actions.md` — Forgejo (GH-compat) self-hosted runner, Tailscale-only admin.
|
||||
- `_blocks/ci-release-automation.md` — release-please / changesets / cargo-release / goreleaser.
|
||||
- `_blocks/ci-security-gate.md` — gitleaks, cargo-audit, npm/pip-audit, syft SBOM, semgrep, licenses.
|
||||
|
||||
---
|
||||
|
||||
## Pipeline overview (5 phases, ≥5 AskUserQuestion calls)
|
||||
|
||||
| Phase | File | Purpose | AskUserQuestion |
|
||||
|---|---|---|---|
|
||||
| 1 | [phase-1-intake.md](phase-1-intake.md) | Platform / languages / deploy target / release strategy | 4× |
|
||||
| 2 | [phase-2-matrix.md](phase-2-matrix.md) | Build matrix: OS × version × target | 1× |
|
||||
| 3 | [phase-3-workflows.md](phase-3-workflows.md) | Generate `.github/workflows/*.yml` or `.forgejo/workflows/*.yml` | 1× |
|
||||
| 4 | [phase-4-secrets.md](phase-4-secrets.md) | OIDC vs PAT; RULE 0.8 env-var scaffold | 1× |
|
||||
| 5 | [phase-5-verify.md](phase-5-verify.md) | Run `kei-ci-lint`; fix/skip/abort on each finding | 1× per finding (≥0) |
|
||||
|
||||
Minimum AskUserQuestion count across a full session: **8** (4 Phase 1 + 1 each Phases 2–5). Exceeds the ≥5 hub-and-spoke contract. Phase 5 adds one AskUserQuestion PER lint finding — typically 0–3.
|
||||
|
||||
---
|
||||
|
||||
## Variables the pipeline produces
|
||||
|
||||
| Name | Set in | Meaning |
|
||||
|---|---|---|
|
||||
| `REPO` | Phase 1 | Free-text one-liner: stack + deploy target |
|
||||
| `PLATFORM` | Phase 1 | github-actions / forgejo-actions / both |
|
||||
| `LANGS` | Phase 1 | subset of {rust, node, python, go, flutter, swift} |
|
||||
| `DEPLOY` | Phase 1 | none / aws-oidc / gcp-oidc / cloudflare / modal / docker-registry / custom |
|
||||
| `RELEASE` | Phase 1 | release-please / changesets / cargo-release / goreleaser / none |
|
||||
| `MATRIX` | Phase 2 | {os, lang-version, target} tuple list |
|
||||
| `WORKFLOWS` | Phase 3 | list of generated YAML filenames |
|
||||
| `SECRETS` | Phase 4 | env var NAMES + storage path; NEVER values |
|
||||
| `LINT` | Phase 5 | pass / warn-with-overrides / fail |
|
||||
|
||||
---
|
||||
|
||||
## Final report (emit after Phase 5)
|
||||
|
||||
```
|
||||
=== CI-SCAFFOLD REPORT ===
|
||||
Repo: <REPO one-liner>
|
||||
Platform: <PLATFORM>
|
||||
Languages: <LANGS>
|
||||
Deploy: <DEPLOY>
|
||||
Release: <RELEASE tool>
|
||||
Matrix: <os count> × <version count> × <target count> = N cells
|
||||
Workflows: <list of generated file paths>
|
||||
Secrets: <N> env VAR names written to secrets/ci.env scaffold (RULE 0.8)
|
||||
Lint: <kei-ci-lint status> (<N findings, M fixed, K skipped>)
|
||||
Next: review diff → commit → push to feat/<name>-ci branch
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Rules (apply throughout)
|
||||
|
||||
- **Pure-click contract.** Only Phase 1 intake is typed. Every other decision is `AskUserQuestion`.
|
||||
- **RULE 0.8 Secrets SSoT.** Emit env VARIABLE NAMES only (`AWS_ROLE_ARN`, `CARGO_REGISTRY_TOKEN`, ...). NEVER echo a token value. Storage path is `<repo>/secrets/ci.env` per `_blocks/domain-has-secrets.md`.
|
||||
- **RULE 0.4 NO HALLUCINATION.** Every `uses:` value cites a real repo — tags used are those actually published on the action's release page at scaffold time (`actions/checkout@v4`, `actions/cache@v4`, `Swatinem/rust-cache@v2`, etc.). If unsure, prefer pin-by-SHA with a comment; never invent a version.
|
||||
- **RULE 0.1 NO GITHUB PUSH.** If `PLATFORM=forgejo-actions` the skill REFUSES to also emit `.github/workflows/` files. Mixed posture allowed only with explicit user confirmation.
|
||||
- **NO DOWNGRADE.** If a Phase-5 finding blocks, the skill returns 2–3 constructive fixes (not "skip it").
|
||||
- **Fail-closed default.** Unknown stack → no matrix generated until user clicks; missing OIDC role → block deploy job scaffold with a typed TODO.
|
||||
- **Surgical scope.** Writes ONLY under `.github/workflows/` or `.forgejo/workflows/` and prints the `secrets/ci.env` scaffold to chat (never writes `secrets/*.env` itself).
|
||||
|
||||
---
|
||||
|
||||
## References
|
||||
|
||||
- `_blocks/ci-github-actions.md`, `_blocks/ci-forgejo-actions.md`,
|
||||
`_blocks/ci-release-automation.md`, `_blocks/ci-security-gate.md`.
|
||||
- `_blocks/domain-has-secrets.md` — storage path + loading convention.
|
||||
- `_blocks/rule-pre-dev-gate.md` — analogue check before inventing a new workflow.
|
||||
- `_primitives/kei-ci-lint.sh` — workflow YAML validator (R1–R7 rules).
|
||||
- Evidence grade [E2] — mirrors GitHub Actions security hardening guide + Forgejo Actions docs as of 2026-04-21.
|
||||
115
skills/ci-scaffold/phase-1-intake.md
Normal file
115
skills/ci-scaffold/phase-1-intake.md
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
# Phase 1 — Intake (platform, languages, deploy, release)
|
||||
|
||||
One free-text paragraph, then four click batches. This is the only phase that accepts typed input.
|
||||
|
||||
## 1a — Ask for the repo description
|
||||
|
||||
Emit a regular message (NOT AskUserQuestion):
|
||||
|
||||
> Describe the repo in one paragraph: what it builds, what language/stack, where it deploys (if anywhere), how releases are tagged today, and any constraint I should know (monorepo, regulated, RULE 0.1 KeiGit-only, etc.). Reply in one message.
|
||||
|
||||
Store the reply verbatim as `REPO`.
|
||||
|
||||
## 1b — Platform click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "CI platform?",
|
||||
"header": "Platform",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "GitHub Actions", "description": "github.com-hosted runners; OIDC to AWS/GCP/CF; see _blocks/ci-github-actions.md"},
|
||||
{"label": "Forgejo Actions (self-hosted)", "description": "RULE 0.1 — KeiGit patent IP. Self-hosted runner on Tailscale. GH-compat; see _blocks/ci-forgejo-actions.md"},
|
||||
{"label": "Both (mirror main → GH, CI on Forgejo only)", "description": "Rare; explicit sign-off required — RULE 0.1 forbids pushing patent IP to GitHub"},
|
||||
{"label": "Neither / unsure", "description": "Skill defaults to Forgejo (safer for unfiled IP); override later"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `PLATFORM`. If `Both` is selected, emit a one-line confirm: "You understand RULE 0.1 — only non-patent code ever pushes to GitHub?" and wait for a `y` typed reply before proceeding.
|
||||
|
||||
## 1c — Languages click (AskUserQuestion, multi-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which language toolchains must CI build + test?",
|
||||
"header": "Languages",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "Rust", "description": "cargo build/test + Swatinem/rust-cache@v2 + cargo-audit + cargo-deny"},
|
||||
{"label": "Node / TypeScript", "description": "pnpm or npm; actions/setup-node@v4 with cache; npm audit / pnpm audit"},
|
||||
{"label": "Python", "description": "actions/setup-python@v5 + pip cache; pip-audit; hatch/poetry/uv as lock source"},
|
||||
{"label": "Go", "description": "actions/setup-go@v5 + cache; go vet + govulncheck; goreleaser for release"},
|
||||
{"label": "Flutter","description": "subosito/flutter-action@v2; flutter analyze + flutter test before any build"},
|
||||
{"label": "Swift", "description": "SPM on macos-14 runner (GH) or self-hosted mac (Forgejo); codesign outside CI"},
|
||||
{"label": "Docker image only", "description": "No language toolchain in CI; buildx builds the image + SBOM"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `LANGS`. Empty selection → re-ask.
|
||||
|
||||
## 1d — Deploy target click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Where does CI deploy the artefact?",
|
||||
"header": "Deploy",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "None — CI only tests", "description": "Skip all deploy jobs; still run build + security gate"},
|
||||
{"label": "AWS via OIDC", "description": "aws-actions/configure-aws-credentials@v4; role trust policy pinned to repo+ref"},
|
||||
{"label": "GCP via OIDC (WIF)", "description": "google-github-actions/auth@v2 + Workload Identity Federation"},
|
||||
{"label": "Cloudflare (Workers/Pages/R2)","description": "wrangler deploy; CLOUDFLARE_API_TOKEN with scopes from self-sufficiency.md"},
|
||||
{"label": "Modal (GPU)", "description": "modal deploy; cost tiers enforced (see _blocks/deploy-modal.md + RULE api-cost-guard)"},
|
||||
{"label": "Container registry (GHCR / ECR / GAR / Forgejo)", "description": "Build + push image, optionally sign with cosign; SBOM attached"},
|
||||
{"label": "Custom / on-prem via SSH", "description": "appleboy/ssh-action@v1 with an ephemeral key minted per run"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `DEPLOY`.
|
||||
|
||||
## 1e — Release strategy click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Release / versioning tool?",
|
||||
"header": "Release",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "release-please", "description": "Conventional Commits → Release-PR; polyglot; recommended monorepo default"},
|
||||
{"label": "changesets", "description": "JS/TS; per-PR .changeset/*.md; best for npm publishing"},
|
||||
{"label": "cargo-release", "description": "Rust crates.io; sign-tag, cargo publish with trusted-publishing token"},
|
||||
{"label": "goreleaser", "description": "Go; tag push → build matrix + archives + checksums + SBOM"},
|
||||
{"label": "Manual tags / none", "description": "No release automation; CI builds + tests only"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `RELEASE`.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `REPO` non-empty.
|
||||
- `PLATFORM` exactly one label.
|
||||
- `LANGS` has ≥1 entry (or exactly `Docker image only`).
|
||||
- `DEPLOY`, `RELEASE` each exactly one label.
|
||||
- If `PLATFORM = Both`, explicit user `y` confirm captured (RULE 0.1).
|
||||
- If `DEPLOY = AWS via OIDC` (or GCP/WIF) and `PLATFORM = Forgejo`, warn: "Forgejo has no OIDC JWKS — the AWS role must be assumed via a bastion. Continue?" and offer NO DOWNGRADE alternatives before proceeding.
|
||||
83
skills/ci-scaffold/phase-2-matrix.md
Normal file
83
skills/ci-scaffold/phase-2-matrix.md
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
# Phase 2 — Build matrix (OS × version × target)
|
||||
|
||||
Decide how the build fans out. Matrix minimum: OS × primary-language version. Max reasonable: 3 OS × 3 versions × 3 targets = 27 cells — beyond that CI time-to-green kills iteration speed.
|
||||
|
||||
## 2a — Matrix click (AskUserQuestion, multi-select across three axes)
|
||||
|
||||
The question encodes three axes in one screen to keep the click contract tight. Each selection is stored as a set; the cartesian product becomes `MATRIX`.
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Build OS?",
|
||||
"header": "OS",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "ubuntu-24.04", "description": "GH-hosted default; also available as self-hosted label on Forgejo"},
|
||||
{"label": "ubuntu-22.04", "description": "Older glibc; pick if targeting older prod"},
|
||||
{"label": "macos-14", "description": "Apple Silicon (M1); required for Swift/iOS/macOS builds"},
|
||||
{"label": "macos-13", "description": "Intel macOS; x86_64 test matrix on Apple software"},
|
||||
{"label": "windows-2022", "description": "Only when a .exe / MSVC artefact is shipped"},
|
||||
{"label": "self-hosted (Forgejo runner)", "description": "Labels from ci-forgejo-actions.md: self-hosted,linux,x64,docker"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Language / toolchain versions (picks combine with every OS)?",
|
||||
"header": "Versions",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "Rust stable + MSRV 1.80", "description": "rust: [stable, 1.80] — MSRV pin catches accidental newer-feature use"},
|
||||
{"label": "Node 20 LTS + 22 LTS", "description": "node-version: [20, 22] — covers current + upcoming LTS"},
|
||||
{"label": "Python 3.11 + 3.12 + 3.13", "description": "python-version: ['3.11','3.12','3.13'] — matches supported pip-audit range"},
|
||||
{"label": "Go 1.22 + 1.23", "description": "go-version: ['1.22','1.23'] — current + previous minor"},
|
||||
{"label": "Flutter stable", "description": "Single version; pin via flutter-version-file"},
|
||||
{"label": "Swift 6.0 (Xcode 16)", "description": "xcode-select on macos-14; one version per OS cell"},
|
||||
{"label": "Single version only", "description": "Matrix collapses on the version axis; OS axis still fans out"}
|
||||
]
|
||||
},
|
||||
{
|
||||
"question": "Cross-compile targets (Rust / Go only; skip otherwise)?",
|
||||
"header": "Targets",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "host (no cross)", "description": "Default; one per OS"},
|
||||
{"label": "aarch64-unknown-linux-gnu", "description": "ARM64 server deploy; use cross or native ARM runner"},
|
||||
{"label": "wasm32-unknown-unknown", "description": "Browser / edge Worker target"},
|
||||
{"label": "x86_64-pc-windows-gnu", "description": "MinGW Windows from Linux build"},
|
||||
{"label": "aarch64-apple-darwin", "description": "Apple Silicon; native on macos-14, cross on ubuntu"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store the three sets as `MATRIX.os`, `MATRIX.versions`, `MATRIX.targets`. The scaffold uses the cartesian product.
|
||||
|
||||
## 2b — Sanity check (no AskUserQuestion)
|
||||
|
||||
Compute `N = |os| × |versions| × |targets|`. Print inline:
|
||||
|
||||
```
|
||||
Matrix cells: N (os=<...>) × (versions=<...>) × (targets=<...>)
|
||||
Estimated runtime: ~<N × typical-cell-minutes> minutes wall-clock (parallel)
|
||||
```
|
||||
|
||||
If `N > 12`, warn once: "Matrix is wide. Consider dropping one axis or using `strategy.fail-fast: true` for PR-time feedback. Continue?"
|
||||
|
||||
If `MATRIX.os` includes both `macos-*` and `MATRIX.targets` includes only `host`, collapse the targets axis silently — host-on-mac is the only useful combination.
|
||||
|
||||
## 2c — Fail-fast click inferred (inline, no extra AskUserQuestion)
|
||||
|
||||
- PR matrix: `fail-fast: false` (user wants to see ALL failing cells at once).
|
||||
- Scheduled cron + release matrix: `fail-fast: true` (first failure is enough to trigger remediation).
|
||||
|
||||
Emitted in Phase 3 `ci.yml` / `release.yml` accordingly.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `MATRIX.os` has ≥1 entry.
|
||||
- `MATRIX.versions` has ≥1 entry (or "Single version only").
|
||||
- `MATRIX.targets` has ≥1 entry (defaults to `host`).
|
||||
- `N ≤ 27` or explicit user override recorded in the final report.
|
||||
- If `LANGS = {Swift}` then `MATRIX.os` MUST include a `macos-*` entry (fail-closed otherwise).
|
||||
113
skills/ci-scaffold/phase-3-workflows.md
Normal file
113
skills/ci-scaffold/phase-3-workflows.md
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
# Phase 3 — Workflow generation
|
||||
|
||||
Scaffold the YAML files under `.github/workflows/` (if `PLATFORM = GitHub Actions`) or `.forgejo/workflows/` (if `PLATFORM = Forgejo Actions`). Uses `_blocks/ci-github-actions.md` and `_blocks/ci-forgejo-actions.md` as the template source; uses `_blocks/ci-release-automation.md` for the release workflow; uses `_blocks/ci-security-gate.md` for the scanner workflow.
|
||||
|
||||
## 3a — Confirm generation scope (AskUserQuestion, multi-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Which workflow files to generate?",
|
||||
"header": "Workflows",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "ci.yml — build + test + lint (from MATRIX)", "description": "Runs on push + PR; uses fail-fast:false for PRs"},
|
||||
{"label": "security.yml — gitleaks + SCA + semgrep", "description": "From _blocks/ci-security-gate.md; PR trigger + daily cron"},
|
||||
{"label": "release.yml — tag / publish (from RELEASE)", "description": "Only if RELEASE != 'Manual tags / none'"},
|
||||
{"label": "deploy.yml — per DEPLOY target", "description": "Only if DEPLOY != 'None — CI only tests'; guarded by GitHub Environment + reviewers"},
|
||||
{"label": "sbom.yml — syft CycloneDX on release", "description": "Attaches SBOM artefact to release; from ci-security-gate.md"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `WORKFLOWS.selected`. Default-include the first two if the user clicks "nothing selected" — fail-closed.
|
||||
|
||||
## 3b — Scaffold ci.yml
|
||||
|
||||
Platform-specific base directory:
|
||||
|
||||
- `PLATFORM = GitHub Actions` → `.github/workflows/ci.yml`
|
||||
- `PLATFORM = Forgejo Actions` → `.forgejo/workflows/ci.yml`
|
||||
|
||||
Template (filled from `MATRIX`, `LANGS`):
|
||||
|
||||
```yaml
|
||||
name: ci
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
permissions:
|
||||
contents: read # least-privilege top-level (ci-github-actions.md R2)
|
||||
jobs:
|
||||
build-test:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [<MATRIX.os>]
|
||||
# version axis injected per language
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # [VERIFIED: https://github.com/actions/checkout]
|
||||
# language setup steps injected from LANGS
|
||||
- name: Test
|
||||
run: <per-lang test command>
|
||||
```
|
||||
|
||||
Per-language step injection (one `setup-*` + one cache strategy each):
|
||||
|
||||
| Lang | setup action | cache action | test command |
|
||||
|---|---|---|---|
|
||||
| Rust | `actions-rust-lang/setup-rust-toolchain@v1` [VERIFIED: https://github.com/actions-rust-lang/setup-rust-toolchain] | `Swatinem/rust-cache@v2` [VERIFIED: https://github.com/Swatinem/rust-cache] | `cargo test --workspace --locked` |
|
||||
| Node | `actions/setup-node@v4` [VERIFIED: https://github.com/actions/setup-node] | built-in `cache: pnpm` | `pnpm install --frozen-lockfile && pnpm test` |
|
||||
| Python | `actions/setup-python@v5` [VERIFIED: https://github.com/actions/setup-python] | built-in `cache: pip` | `pip install -e .[test] && pytest` |
|
||||
| Go | `actions/setup-go@v5` [VERIFIED: https://github.com/actions/setup-go] | built-in (>=setup-go v4) | `go test ./...` |
|
||||
| Flutter | `subosito/flutter-action@v2` [VERIFIED: https://github.com/subosito/flutter-action] | built-in | `flutter analyze && flutter test` |
|
||||
| Swift | `maxim-lobanov/setup-xcode@v1` [VERIFIED: https://github.com/maxim-lobanov/setup-xcode] | n/a | `swift test` |
|
||||
|
||||
All `uses:` tags above correspond to published versions on the linked repos — RULE 0.4: never invent a tag. If the repo's latest major is unknown at scaffold time, pin by SHA with a `# v<major>` comment.
|
||||
|
||||
## 3c — Scaffold security.yml
|
||||
|
||||
Uses `_blocks/ci-security-gate.md` as the authoritative template. Emits one job per selected scanner:
|
||||
|
||||
- `secrets-scan` — gitleaks (first job, before build)
|
||||
- `sca-<lang>` — one job per `LANGS` entry (`cargo audit`, `pnpm audit`, `pip-audit`, `govulncheck`)
|
||||
- `sast` — semgrep with `p/default p/secrets p/owasp-top-ten`
|
||||
- `licenses` — cargo-deny (Rust) or `license-checker --failOn 'GPL;AGPL;SSPL'` (Node)
|
||||
|
||||
Trigger: `on: { pull_request:, push: { branches: [main] }, schedule: [{ cron: '17 3 * * *' }] }`.
|
||||
|
||||
## 3d — Scaffold release.yml
|
||||
|
||||
Template per `RELEASE`:
|
||||
|
||||
- `release-please` → `googleapis/release-please-action@v4` [VERIFIED: https://github.com/googleapis/release-please-action]
|
||||
- `changesets` → `changesets/action@v1` [VERIFIED: https://github.com/changesets/action]
|
||||
- `cargo-release` → step runs `cargo publish --locked`, token via OIDC trusted-publishing (or `CARGO_REGISTRY_TOKEN` from Phase 4)
|
||||
- `goreleaser` → `goreleaser/goreleaser-action@v6` [VERIFIED: https://github.com/goreleaser/goreleaser-action]
|
||||
|
||||
Permissions set at job level only: `contents: write`, `id-token: write`, `pull-requests: write` (release-please).
|
||||
|
||||
## 3e — Scaffold deploy.yml (per DEPLOY)
|
||||
|
||||
- `aws-oidc` — `aws-actions/configure-aws-credentials@v4` with `role-to-assume: ${{ vars.AWS_ROLE_ARN }}`; environment `production` with required reviewer.
|
||||
- `gcp-oidc` — `google-github-actions/auth@v2` [VERIFIED: https://github.com/google-github-actions/auth] with `workload_identity_provider`.
|
||||
- `cloudflare` — `cloudflare/wrangler-action@v3` [VERIFIED: https://github.com/cloudflare/wrangler-action] with `apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }}`.
|
||||
- `modal` — `pip install modal && modal deploy` with `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET` (Phase 4 registers the env names; RULE api-cost-guard before first run).
|
||||
|
||||
## 3f — Write files, print diff
|
||||
|
||||
Emit each file path + the generated content as a fenced code block. DO NOT commit. Append to chat:
|
||||
|
||||
> Scaffold written. Review, then `git add <paths>` + commit. Phase 5 will run `kei-ci-lint` before you push.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- Every entry in `WORKFLOWS.selected` produced exactly one YAML file at the platform-correct path.
|
||||
- Every `uses:` line has a VERIFIED cite in the surrounding block reference OR is pinned by 40-hex SHA.
|
||||
- No `secrets.*` variable is populated with a literal in the YAML (Phase 4 owns names only).
|
||||
- No workflow has `permissions: write-all` at top level.
|
||||
98
skills/ci-scaffold/phase-4-secrets.md
Normal file
98
skills/ci-scaffold/phase-4-secrets.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Phase 4 — Secrets posture (OIDC vs PAT; RULE 0.8 scaffold)
|
||||
|
||||
Decides how CI obtains credentials. Default bias is OIDC (short-lived, no stored secret); fall back to PAT only when the provider has no OIDC (e.g. Forgejo → AWS, npm trusted-publishing not configured, custom SSH deploy). Every chosen secret is referenced by NAME ONLY per RULE 0.8 — this skill NEVER writes a value.
|
||||
|
||||
## 4a — Posture click (AskUserQuestion, single-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Credential posture for CI?",
|
||||
"header": "Secrets",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "OIDC-first (recommended)",
|
||||
"description": "Cloud roles trust token.actions.githubusercontent.com; no long-lived keys stored. Requires DEPLOY ∈ {aws-oidc, gcp-oidc} and PLATFORM = GitHub Actions."},
|
||||
{"label": "PAT fallback (when OIDC unavailable)",
|
||||
"description": "Long-lived scoped tokens stored in repo secrets. Rotation schedule mandatory (30–90 days). Used for Cloudflare, npm, DockerHub, custom SSH."},
|
||||
{"label": "Hybrid — OIDC where possible, PAT elsewhere",
|
||||
"description": "Most real setups. Skill emits both sections of the scaffold."},
|
||||
{"label": "No secrets (public CI tests only)",
|
||||
"description": "ci.yml + security.yml do not need credentials. deploy.yml / release.yml skipped."}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store as `SECRETS.posture`.
|
||||
|
||||
If `PLATFORM = Forgejo Actions` and the user picked `OIDC-first`, warn: "Forgejo does not serve a JWKS endpoint. Use the bastion pattern from `_blocks/ci-forgejo-actions.md` OR switch to PAT-fallback." Offer both constructive paths (NO DOWNGRADE) and re-ask.
|
||||
|
||||
## 4b — Enumerate required secrets (no AskUserQuestion; derived from DEPLOY + RELEASE)
|
||||
|
||||
Walk the matrix below. For each hit, add to `SECRETS.required`.
|
||||
|
||||
| DEPLOY / RELEASE | OIDC posture | PAT fallback posture |
|
||||
|---|---|---|
|
||||
| `aws-oidc` | `AWS_ROLE_ARN` (repo var, not secret); `AWS_REGION` | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` (last-resort, rotate 30d) |
|
||||
| `gcp-oidc` | `GCP_WORKLOAD_IDENTITY_PROVIDER` + `GCP_SERVICE_ACCOUNT` | `GCP_SA_KEY` JSON (avoid; Google deprecates static keys 2026) |
|
||||
| `cloudflare` | (Workers OIDC preview; most prod still token) | `CLOUDFLARE_API_TOKEN` (scopes per `self-sufficiency.md`); `CLOUDFLARE_ACCOUNT_ID` |
|
||||
| `modal` | n/a (Modal has its own token model) | `MODAL_TOKEN_ID` + `MODAL_TOKEN_SECRET`; cost tier check pre-launch |
|
||||
| `registry (GHCR)` | built-in `GITHUB_TOKEN` write-packages | — |
|
||||
| `registry (ECR)` | Uses AWS OIDC role | `AWS_ACCESS_KEY_ID` + `AWS_SECRET_ACCESS_KEY` |
|
||||
| `registry (Forgejo)` | `FORGEJO_TOKEN` (built-in at runner) | — |
|
||||
| `custom SSH` | — | `SSH_PRIVATE_KEY` (ed25519, generated fresh per repo), `SSH_HOST`, `SSH_USER` |
|
||||
| `RELEASE=cargo-release` | crates.io trusted publishing (2025+) | `CARGO_REGISTRY_TOKEN` |
|
||||
| `RELEASE=changesets` | npm trusted publishing (2026 preview) | `NPM_TOKEN` |
|
||||
|
||||
## 4c — Emit `secrets/ci.env` scaffold (inline; no file write)
|
||||
|
||||
Print as a fenced code block. Example when posture is OIDC-first + cargo-release:
|
||||
|
||||
```bash
|
||||
# secrets/ci.env — paths and NAMES only. chmod 600 + .gitignore before writing values.
|
||||
# RULE 0.8: reference by env-var name. NEVER paste a literal here.
|
||||
|
||||
# OIDC (no secrets stored; vars on the provider side)
|
||||
AWS_ROLE_ARN= # arn:aws:iam::<account>:role/gha-deployer — set as repo VAR, not secret
|
||||
AWS_REGION= # eu-north-1
|
||||
|
||||
# Release publishing
|
||||
CARGO_REGISTRY_TOKEN= # trusted-publishing preferred; fallback PAT only if TP unavailable
|
||||
```
|
||||
|
||||
Append the reminder once:
|
||||
|
||||
> `secrets/ci.env` must be `chmod 600` AND listed in `.gitignore` BEFORE the first write. See `_blocks/domain-has-secrets.md`. Repo-level "Secrets and variables → Actions" is the deployment copy — rotate source `.env` when repo secret rotates, not the other way around.
|
||||
|
||||
## 4d — Confirm repo-side secret registration (AskUserQuestion, multi-select)
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "For each name I listed, confirm it is REGISTERED on the platform (Settings → Actions → Secrets or Repo Variables):",
|
||||
"header": "Registered",
|
||||
"multiSelect": true,
|
||||
"options": [
|
||||
{"label": "All names present and current (rotated within the last 90 days)", "description": "Proceed to Phase 5"},
|
||||
{"label": "Some names missing — I will register now and re-run", "description": "Skill exits; re-enter after registration"},
|
||||
{"label": "I use a secrets manager (Vault / 1Password CLI / Doppler) that syncs to the platform", "description": "Acceptable; confirm sync is green"},
|
||||
{"label": "None registered yet — show me the platform link", "description": "Emit link per PLATFORM and exit"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store the answer as `SECRETS.registration_status`. Any answer other than the first pauses Phase 5.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `SECRETS.posture` is exactly one choice.
|
||||
- `SECRETS.required` is fully enumerated from `DEPLOY` + `RELEASE`; no `TODO` placeholders.
|
||||
- The printed scaffold has NO literal values — every `=` is followed by whitespace or a `#` comment.
|
||||
- Forgejo + OIDC combination has either the bastion pattern documented or the user opted into PAT-fallback.
|
||||
- `SECRETS.registration_status` non-empty.
|
||||
98
skills/ci-scaffold/phase-5-verify.md
Normal file
98
skills/ci-scaffold/phase-5-verify.md
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
# Phase 5 — Verify via kei-ci-lint, then final report
|
||||
|
||||
Close the pipeline by validating every generated workflow with `_primitives/kei-ci-lint.sh`. The lint has seven rules (R1–R7); each finding drives one AskUserQuestion for fix/skip/abort.
|
||||
|
||||
## 5a — Run the linter
|
||||
|
||||
Execute:
|
||||
|
||||
```
|
||||
sh _primitives/kei-ci-lint.sh --dir .github/workflows
|
||||
# or, if PLATFORM = Forgejo Actions:
|
||||
sh _primitives/kei-ci-lint.sh --dir .forgejo/workflows
|
||||
```
|
||||
|
||||
Capture stdout + stderr. Parse output — one line per finding, format `FAIL <file> <R#> <message>` or `WARN …`.
|
||||
|
||||
## 5b — Per-finding triage (AskUserQuestion, single-select per finding)
|
||||
|
||||
For EACH `FAIL` line, emit:
|
||||
|
||||
```json
|
||||
{
|
||||
"questions": [
|
||||
{
|
||||
"question": "Lint finding: <R#> in <file> — <message>. Action?",
|
||||
"header": "Lint",
|
||||
"multiSelect": false,
|
||||
"options": [
|
||||
{"label": "Fix now (skill applies the recommended patch)",
|
||||
"description": "Skill edits the YAML file inline; next lint run must show clean"},
|
||||
{"label": "Skip (add to allowlist with justification)",
|
||||
"description": "Skill prompts for a 1-line justification; stored as a YAML comment + ledger line"},
|
||||
{"label": "Abort (stop the pipeline; user investigates manually)",
|
||||
"description": "Scaffold stays in place, but final report marks the skill run as INCOMPLETE"}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Store each answer under `LINT.triage[<finding-key>]`.
|
||||
|
||||
## 5c — Fix recipes (applied inline when user picks "Fix now")
|
||||
|
||||
| Rule | Fix |
|
||||
|---|---|
|
||||
| R1 missing `name:` / `on:` / `jobs:` | Insert the field with a sensible default (`name` from filename; `on: { pull_request: , push: { branches: [main] } }`) |
|
||||
| R2 no top-level `permissions:` | Insert `permissions: { contents: read }` at top level |
|
||||
| R2 `permissions: write-all` | Replace with `contents: read`; move `write` to the single job that needs it |
|
||||
| R3 OIDC + AWS keys present | Ask AskUserQuestion: "Which one do you keep? OIDC / keys" — remove the other |
|
||||
| R4 `key: github.ref` | Replace with `key: <name>-${{ hashFiles('<lockfile>') }}` |
|
||||
| R5 action pinned by tag | Look up the tag's commit SHA on the action's repo, replace `@vX.Y` with the full 40-hex SHA, add `# vX.Y` comment. If lookup fails, leave the tag with a TODO comment and ABORT rather than inventing a SHA (RULE 0.4) |
|
||||
| R6 `::set-output` / `::save-state` | Replace with `$GITHUB_OUTPUT` / `$GITHUB_STATE` redirect (GH docs 2023+) |
|
||||
| R6 `actions/checkout@v1` / `v2` | Upgrade to `@v4` (and re-run R5 pin-by-SHA) |
|
||||
| R7 `pull_request_target` + PR-head checkout | Either remove `pull_request_target` (prefer) OR remove the `ref: ${{ github.event.pull_request.head.sha }}` line. Present both to user |
|
||||
|
||||
## 5d — Re-run linter after fixes
|
||||
|
||||
After all fixes applied, re-run `kei-ci-lint` once. If still failing, enter the 3-Level Escalation (dev-workflow.md): after 2 automatic fix attempts, STOP and escalate — present the remaining findings to the user with a numbered plan (NO DOWNGRADE: alternative scaffolds, not "accept the violation").
|
||||
|
||||
## 5e — Emit final report
|
||||
|
||||
Template (from SKILL.md):
|
||||
|
||||
```
|
||||
=== CI-SCAFFOLD REPORT ===
|
||||
Repo: <REPO>
|
||||
Platform: <PLATFORM>
|
||||
Languages: <LANGS joined>
|
||||
Deploy: <DEPLOY>
|
||||
Release: <RELEASE>
|
||||
Matrix: <|os|> × <|versions|> × <|targets|> = <N> cells
|
||||
Workflows: <paths, one per line>
|
||||
Secrets: <|SECRETS.required|> env names scaffolded to secrets/ci.env (posture: <SECRETS.posture>)
|
||||
Lint: <PASS | WARN-<N> | FAIL-<N>> — <fixes applied count>/<skips count>/<aborts count>
|
||||
Next: git diff → review → commit on feat/<name>-ci → PR
|
||||
|
||||
Citations used (RULE 0.4):
|
||||
- actions/checkout@v4 [VERIFIED: https://github.com/actions/checkout]
|
||||
- actions/cache@v4 [VERIFIED: https://github.com/actions/cache]
|
||||
- <one line per every uses: in generated files>
|
||||
```
|
||||
|
||||
## 5f — Handoff
|
||||
|
||||
If `LINT` is `PASS` or `WARN-only`, advise:
|
||||
|
||||
> Scaffold complete. Next: `git add .github/ .forgejo/ secrets/ci.env` (NOT the secret values — just the scaffold), commit on a `feat/ci-*` branch, push, and request review.
|
||||
|
||||
If `LINT = FAIL` after 2 fix passes, advise the user to invoke `compose-solution` with the remaining findings as new components — the meta-orchestrator may find missing `_blocks/` or suggest a new primitive.
|
||||
|
||||
## Verify-criterion
|
||||
|
||||
- `kei-ci-lint` was executed against the generated files.
|
||||
- Every `FAIL` line produced exactly one AskUserQuestion triage.
|
||||
- No action tag was invented to satisfy R5 — unresolvable SHA lookups must ABORT with a TODO (RULE 0.4 hard).
|
||||
- Final report lists every citation used in every generated workflow.
|
||||
- `Next:` line tells the user exactly what to stage, where to branch, and where to PR.
|
||||
Loading…
Reference in a new issue