feat(blocks): 4 CI/CD blocks — gh-actions/forgejo-actions/release/security-gate
This commit is contained in:
parent
48d4dd0733
commit
719324e0a9
4 changed files with 318 additions and 0 deletions
61
_blocks/ci-forgejo-actions.md
Normal file
61
_blocks/ci-forgejo-actions.md
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
# CI — Forgejo Actions (self-hosted, Tailscale-only admin)
|
||||
|
||||
Forgejo Actions is GitHub-Actions compatible at the workflow-syntax layer (derived from Gitea Actions, which re-uses the `actions/*` runtime via `act`). A workflow that runs on GH usually runs on Forgejo with only the runner labels and registry URLs changed. Pair with RULE 0.1 — KeiGit repos MUST stay on private Forgejo, never mirror to github.com.
|
||||
|
||||
## Layout
|
||||
|
||||
Workflows live under `.forgejo/workflows/*.yml` (primary) — `.gitea/workflows/` also works for legacy repos. Keep the same narrow split as GH:
|
||||
|
||||
- `ci.yml` — build + test
|
||||
- `release.yml` — tag-driven
|
||||
- `security.yml` — scheduled scanners
|
||||
|
||||
## Self-hosted runner
|
||||
|
||||
Forgejo has no SaaS runner fleet — you provide the compute. Install `forgejo-runner` [VERIFIED: https://code.forgejo.org/forgejo/runner] on a node that is reachable ONLY over Tailscale.
|
||||
|
||||
Registration:
|
||||
|
||||
```bash
|
||||
forgejo-runner register \
|
||||
--no-interactive \
|
||||
--instance http://100.91.246.53:3000 \
|
||||
--name kgl-runner-01 \
|
||||
--labels "self-hosted,linux,x64,docker" \
|
||||
--token "$FORGEJO_RUNNER_TOKEN" # from secrets/runner.env (RULE 0.8)
|
||||
```
|
||||
|
||||
`FORGEJO_RUNNER_TOKEN` stays in `secrets/runner.env` — reference via env name only, never paste the literal value.
|
||||
|
||||
Target in workflow:
|
||||
|
||||
```yaml
|
||||
jobs:
|
||||
build:
|
||||
runs-on: [self-hosted, linux, x64]
|
||||
```
|
||||
|
||||
## GitHub-compat surface
|
||||
|
||||
Works out of the box: `actions/checkout@v4`, `actions/cache@v4`, `actions/setup-node@v4`, `Swatinem/rust-cache@v2`, shell/docker steps, matrix, reusable workflows (`uses: <forgejo-host>/<owner>/<repo>/.forgejo/workflows/<file>@<sha>`).
|
||||
|
||||
Does NOT work: `permissions:` block (Forgejo token is scoped at the runner level, not per-job), OIDC federation to AWS/GCP (no JWKS endpoint served by Forgejo), GitHub-Marketplace actions that call `api.github.com` directly.
|
||||
|
||||
Workaround for OIDC: for cloud deploys from Forgejo, prefer short-lived STS tokens minted by a bastion that has an IAM role, passed into the runner via a sealed env file rotated daily.
|
||||
|
||||
## Tailscale-only admin posture
|
||||
|
||||
Forgejo Web UI is http://100.91.246.53:3000, SSH is `ssh://git@100.91.246.53:2222/...`. Both on Tailscale CGNAT. NEVER bind Forgejo to a public IP — runner tokens, PATs, and repo contents are unfiled patent IP (RULE 0.1).
|
||||
|
||||
Key fingerprint for the existing KeiGit host: `SHA256:TxHcs7YuEZiy4Gu0yZOoVidVqlvj8TPC+QgUGjmh0Mw` labelled `macbook`.
|
||||
|
||||
## Secrets
|
||||
|
||||
Forgejo repo secrets (`Repo → Settings → Actions → Secrets`) mirror GH secrets syntactically: `${{ secrets.FOO }}`. Organisation-scope secrets also supported. Every secret still references the canonical `~/.claude/secrets/.env` / `secrets/*.env` source — repo secrets are cache copies, rotated when the source rotates.
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Exposing Forgejo port 3000 or 2222 on a public IP
|
||||
- Running `forgejo-runner` on a host that is also a production application node
|
||||
- Mirroring a KeiGit repo to github.com to "get free CI" (RULE 0.1)
|
||||
- Hard-coded runner tokens in workflow YAML (always `${{ secrets.* }}`)
|
||||
95
_blocks/ci-github-actions.md
Normal file
95
_blocks/ci-github-actions.md
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# CI — GitHub Actions (OIDC, matrix, cache, reusable workflows)
|
||||
|
||||
Pipeline platform for code hosted on (or mirrored to) github.com. This block ships the defaults; pair with `ci-security-gate.md` for scanners and `ci-release-automation.md` for tags.
|
||||
|
||||
## Workflow layout
|
||||
|
||||
Keep workflow files narrow: ONE responsibility each under `.github/workflows/`.
|
||||
|
||||
- `ci.yml` — build + test on every push/PR
|
||||
- `release.yml` — tag-driven release automation (see `ci-release-automation.md`)
|
||||
- `security.yml` — scheduled scanners (see `ci-security-gate.md`)
|
||||
- `deploy-*.yml` — per-environment deploys, each behind a GitHub Environment with required reviewers
|
||||
|
||||
## OIDC — cloud deploy WITHOUT long-lived keys
|
||||
|
||||
GitHub Actions mints a short-lived JWT per run; the cloud provider trusts `token.actions.githubusercontent.com` and issues temporary credentials. **Never** store `AWS_SECRET_ACCESS_KEY` / `GCP_SA_KEY` in repo secrets.
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
id-token: write # mandatory for OIDC
|
||||
contents: read
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: actions/checkout@v4 # [VERIFIED: https://github.com/actions/checkout]
|
||||
- uses: aws-actions/configure-aws-credentials@v4 # [VERIFIED: https://github.com/aws-actions/configure-aws-credentials]
|
||||
with:
|
||||
role-to-assume: arn:aws:iam::${{ vars.AWS_ACCOUNT_ID }}:role/gha-deployer
|
||||
aws-region: eu-north-1
|
||||
```
|
||||
|
||||
Cloud-side role trust policy pins `repo:<org>/<repo>:ref:refs/heads/main` — wildcards invite cross-repo impersonation.
|
||||
|
||||
## Least-privilege GITHUB_TOKEN
|
||||
|
||||
Default token permissions at the workflow level, then widen per-job:
|
||||
|
||||
```yaml
|
||||
permissions:
|
||||
contents: read # read-only at top level
|
||||
jobs:
|
||||
build:
|
||||
# inherits read-only
|
||||
release:
|
||||
permissions:
|
||||
contents: write # only the release job gets write
|
||||
id-token: write
|
||||
```
|
||||
|
||||
Org-level default should be `read` (Settings → Actions → Workflow permissions). Any job requiring write must opt in explicitly.
|
||||
|
||||
## Matrix builds
|
||||
|
||||
Fan out across OS × language version × target; `fail-fast: false` prevents one red cell from cancelling the whole matrix.
|
||||
|
||||
```yaml
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-24.04, macos-14]
|
||||
rust: [stable, 1.80] # MSRV pin
|
||||
```
|
||||
|
||||
## Cache hygiene
|
||||
|
||||
- Lock-file as key, never branch name: `key: cargo-${{ hashFiles('**/Cargo.lock') }}`.
|
||||
- `restore-keys` is a PREFIX fallback — safe for cold PRs.
|
||||
- `actions/cache@v4` [VERIFIED: https://github.com/actions/cache] for generic; language-specific actions (`actions/setup-node@v4`, `Swatinem/rust-cache@v2`) manage cache internally — don't double-cache.
|
||||
- Cache POISONING check: never cache directories that contain your built artefacts alongside downloaded deps.
|
||||
|
||||
## Reusable workflows
|
||||
|
||||
Shared logic lives in one repo and is called by `uses: <org>/<repo>/.github/workflows/<file>.yml@<sha>`. Pin by SHA, not tag — tags are mutable. `workflow_call` contract:
|
||||
|
||||
```yaml
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
rust-version: { required: true, type: string }
|
||||
secrets:
|
||||
CARGO_TOKEN: { required: false }
|
||||
```
|
||||
|
||||
## Pinning third-party actions
|
||||
|
||||
Pin by full commit SHA, not tag: `uses: foo/bar@3a4b5c6d7e8f9012...` with a comment `# v2.1.0`. Dependabot updates SHAs the same way — supply-chain hijack via tag-overwrite is a documented class (e.g. `tj-actions/changed-files` 2025). [E2]
|
||||
|
||||
## Forbidden
|
||||
|
||||
- `secrets.AWS_SECRET_ACCESS_KEY` in any workflow (use OIDC)
|
||||
- `permissions: write-all` at workflow level
|
||||
- Third-party action pinned by tag
|
||||
- `pull_request_target` with `checkout` of PR head + secrets access (classic pwn-request)
|
||||
- Caching `target/` or `node_modules/` alongside `.git` or user config
|
||||
80
_blocks/ci-release-automation.md
Normal file
80
_blocks/ci-release-automation.md
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
# CI — Release automation (SemVer, changelog, tagging)
|
||||
|
||||
Automates "merge to main → versioned release" so the next step (build artefact, publish, deploy) has a predictable trigger. Picks ONE tool per repo — mixing release-please with cargo-release creates duplicate tags. Pair with `ci-github-actions.md` / `ci-forgejo-actions.md` for the workflow shell.
|
||||
|
||||
## Tool picks per ecosystem
|
||||
|
||||
| Stack | Tool | Trigger | Changelog source |
|
||||
|---|---|---|---|
|
||||
| Monorepo / polyglot / apps | release-please [VERIFIED: https://github.com/googleapis/release-please] | merge to main | Conventional Commits |
|
||||
| JS/TS packages (npm publish) | changesets [VERIFIED: https://github.com/changesets/changesets] | merge of `.changeset/*.md` | Explicit changeset files |
|
||||
| Rust crates (crates.io) | cargo-release [VERIFIED: https://github.com/crate-ci/cargo-release] | manual `cargo release` | git log + Conventional Commits |
|
||||
| Go modules | goreleaser [VERIFIED: https://github.com/goreleaser/goreleaser] | tag push | git log + `.goreleaser.yaml` |
|
||||
|
||||
## SemVer contract
|
||||
|
||||
- `MAJOR` — breaking change to public API, wire format, on-disk schema, config file keys
|
||||
- `MINOR` — additive feature, no breakage, new optional fields
|
||||
- `PATCH` — bug fix, performance, docs, dep bump without API change
|
||||
|
||||
Conventional Commits mapping: `feat!:` / `BREAKING CHANGE:` → MAJOR; `feat:` → MINOR; `fix:` / `perf:` / `refactor:` → PATCH; `checkpoint:` / `audit:` / `chore:` → no-bump (ignored by release-please).
|
||||
|
||||
## release-please minimal config
|
||||
|
||||
`.github/workflows/release.yml` (or `.forgejo/workflows/release.yml`):
|
||||
|
||||
```yaml
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: write # create tags + releases
|
||||
pull-requests: write # update the Release-PR
|
||||
jobs:
|
||||
release-please:
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4 # [VERIFIED: https://github.com/googleapis/release-please-action]
|
||||
with:
|
||||
release-type: rust # or node, python, go, simple, etc.
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
release-please opens a long-lived "Release PR" that updates `CHANGELOG.md` + version file on every main merge; merging that PR creates the tag and GitHub Release. No human writes the changelog.
|
||||
|
||||
## changesets minimal config (JS/TS monorepo)
|
||||
|
||||
```yaml
|
||||
- uses: changesets/action@v1 # [VERIFIED: https://github.com/changesets/action]
|
||||
with:
|
||||
publish: pnpm release # runs `changeset publish`
|
||||
env:
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
```
|
||||
|
||||
Each PR that changes a package ships a `.changeset/<name>.md` describing the bump. CI blocks merge without one (`changeset status --since=origin/main`).
|
||||
|
||||
## cargo-release minimal config (Rust crates.io)
|
||||
|
||||
`release.toml` at repo root:
|
||||
|
||||
```toml
|
||||
sign-tag = true
|
||||
push = true
|
||||
tag-message = "{{crate_name}} {{version}}"
|
||||
pre-release-commit-message = "release: {{version}}"
|
||||
```
|
||||
|
||||
Publish workflow runs on tag push: `cargo publish --token "$CARGO_REGISTRY_TOKEN"` where the token is minted just-in-time from the `ci-security-gate.md` trusted-publishing flow.
|
||||
|
||||
## Lock-file discipline
|
||||
|
||||
`Cargo.lock` / `package-lock.json` / `pnpm-lock.yaml` / `pubspec.lock` / `go.sum` — ALWAYS committed (RULE git-conventions). Release workflows must FAIL if the lock file is stale: `cargo update --locked --dry-run`, `pnpm install --frozen-lockfile`, `go mod verify`.
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Manual `git tag vX.Y.Z && git push --tags` when a release tool is configured (drift between CHANGELOG and tag)
|
||||
- Two release tools in the same repo (release-please + cargo-release both tagging)
|
||||
- Publishing from a `pull_request` trigger (never — only from `push` to main or `workflow_dispatch`)
|
||||
- Forcing a tag with `git push --force origin refs/tags/*` — breaks every consumer that pinned by SHA
|
||||
- Stale lock files passing CI (must be a hard fail, not a warning)
|
||||
82
_blocks/ci-security-gate.md
Normal file
82
_blocks/ci-security-gate.md
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# CI — Security gate (secrets, SCA, SBOM, semgrep, licenses)
|
||||
|
||||
Every PR passes through this gate before merge. Every scheduled run re-scans `main`. Pair with `ci-github-actions.md` / `ci-forgejo-actions.md` (the shell) and RULE 0.8 (secrets SSoT) / RULE 0.1 (no-github-push) — the gate enforces both.
|
||||
|
||||
## Scanner set (one job each, matrix is fine)
|
||||
|
||||
| Concern | Tool | Trigger | Fail threshold |
|
||||
|---|---|---|---|
|
||||
| Leaked secrets | gitleaks [VERIFIED: https://github.com/gitleaks/gitleaks] | PR + push | any finding |
|
||||
| Rust SCA | cargo-audit [VERIFIED: https://github.com/rustsec/rustsec] | PR + cron daily | any `Vulnerability` |
|
||||
| Node SCA | `npm audit` / `pnpm audit` (native) | PR + cron | `high` and above |
|
||||
| Python SCA | pip-audit [VERIFIED: https://github.com/pypa/pip-audit] | PR + cron | any CVE |
|
||||
| SBOM generation | syft [VERIFIED: https://github.com/anchore/syft] | release only | CycloneDX JSON as artefact |
|
||||
| SAST / patterns | semgrep [VERIFIED: https://github.com/semgrep/semgrep] | PR | any `ERROR` severity |
|
||||
| License policy | cargo-deny [VERIFIED: https://github.com/EmbarkStudios/cargo-deny] (Rust) / license-checker (JS) | PR | disallowed SPDX ID |
|
||||
|
||||
## gitleaks — secrets scan (always first)
|
||||
|
||||
Runs before any build step so that a detected secret aborts the job without ever shipping a binary that used it.
|
||||
|
||||
```yaml
|
||||
- uses: gitleaks/gitleaks-action@v2 # [VERIFIED: https://github.com/gitleaks/gitleaks-action]
|
||||
env:
|
||||
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }} # orgs only; free for ≤25 users
|
||||
```
|
||||
|
||||
Custom rules in `.gitleaks.toml` at repo root — mirror the patterns from `~/.claude/rules/secrets-single-source.md` (sk-, ghp_, sk-ant-, Telegram bot, AWS access key, etc.). Any hit FAILS the run. No "informational" severity for secrets.
|
||||
|
||||
## cargo-audit / pip-audit / npm audit
|
||||
|
||||
Daily cron to catch CVEs published after merge. Fail-fast on HIGH/CRITICAL; report MEDIUM to a tracking issue rather than blocking the PR.
|
||||
|
||||
```yaml
|
||||
- run: cargo audit --deny warnings --deny unmaintained --deny yanked
|
||||
```
|
||||
|
||||
Pin the advisory-DB commit in vendored copies; upstream can get taken down.
|
||||
|
||||
## SBOM via syft
|
||||
|
||||
Generate CycloneDX JSON for every published artefact. Attach to the GitHub Release (see `ci-release-automation.md`) and to the container image as an OCI annotation.
|
||||
|
||||
```yaml
|
||||
- uses: anchore/sbom-action@v0 # [VERIFIED: https://github.com/anchore/sbom-action]
|
||||
with:
|
||||
format: cyclonedx-json
|
||||
artifact-name: sbom.cdx.json
|
||||
```
|
||||
|
||||
SLSA provenance (`slsa-framework/slsa-github-generator`) is an optional upgrade; required when shipping to any customer under a supply-chain contract.
|
||||
|
||||
## semgrep — SAST
|
||||
|
||||
`p/default` + `p/secrets` + `p/owasp-top-ten` + any language pack relevant to the repo. Custom rules under `.semgrep/*.yaml` for project-specific patterns (e.g. "no `unwrap()` in request handlers").
|
||||
|
||||
```yaml
|
||||
- uses: semgrep/semgrep-action@v1 # [VERIFIED: https://github.com/semgrep/semgrep-action]
|
||||
env:
|
||||
SEMGREP_RULES: p/default p/secrets p/owasp-top-ten
|
||||
```
|
||||
|
||||
## License policy
|
||||
|
||||
`cargo-deny` `deny.toml` declares allowed SPDX identifiers (`MIT`, `Apache-2.0`, `BSD-3-Clause`, `ISC`, `Unicode-DFS-2016`). Anything else FAILS the PR. GPL / AGPL / SSPL in a commercial repo = hard stop. For JS, `license-checker --failOn 'GPL;AGPL;SSPL'`.
|
||||
|
||||
## Scheduling
|
||||
|
||||
```yaml
|
||||
on:
|
||||
pull_request:
|
||||
push: { branches: [main] }
|
||||
schedule:
|
||||
- cron: "17 3 * * *" # daily 03:17 UTC — off-hour, avoids global burst
|
||||
```
|
||||
|
||||
## Forbidden
|
||||
|
||||
- Running the security gate AFTER build/test (secret must block before the secret-using binary exists)
|
||||
- Allowing "informational" severity on secrets scans (gitleaks = binary; 0 or 1)
|
||||
- Skipping `cargo-audit` / `pip-audit` on release workflows (a CVE published yesterday ships today without it)
|
||||
- Uploading SBOM to a public artefact store from a RULE-0.1 repo (internal artefact store only)
|
||||
- Copy-pasting a secret detected by gitleaks into the chat to "discuss" — rotate at provider FIRST, then discuss
|
||||
Loading…
Reference in a new issue