feat: reusable secret scanning, SAST, dependency and lint workflows
This commit is contained in:
parent
c9ecafca2f
commit
1e0113c05f
6 changed files with 257 additions and 11 deletions
26
.forgejo/workflows/deps.yml
Normal file
26
.forgejo/workflows/deps.yml
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
# Known-vulnerable dependencies, via osv-scanner against the OSV database.
|
||||||
|
#
|
||||||
|
# Reads every lockfile and manifest it recognises (pnpm/npm/yarn, pip/uv/poetry,
|
||||||
|
# go.mod, NuGet, Cargo and more). Accepted vulnerabilities go in an
|
||||||
|
# osv-scanner.toml in the calling repo as [[IgnoredVulns]] entries with a
|
||||||
|
# reason and an ignoreUntil date, so an exception expires instead of
|
||||||
|
# silently living forever.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
osv-scanner:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: osv-scanner
|
||||||
|
run: |
|
||||||
|
osv-scanner --version | head -1
|
||||||
|
rc=0
|
||||||
|
osv-scanner scan source --recursive . || rc=$?
|
||||||
|
# 128 means no lockfile or manifest was found: nothing to check, not a failure.
|
||||||
|
if [ "$rc" -eq 128 ]; then
|
||||||
|
echo "No supported lockfiles or manifests found; nothing to scan."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
exit "$rc"
|
||||||
103
.forgejo/workflows/lint.yml
Normal file
103
.forgejo/workflows/lint.yml
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
# Lint gates, each switched on by the files present in the calling repo.
|
||||||
|
#
|
||||||
|
# shell scripts shellcheck (*.sh, *.bash)
|
||||||
|
# Python ruff check (repo's ruff/pyproject config if any)
|
||||||
|
# Dockerfiles hadolint (repo's .hadolint.yaml if any)
|
||||||
|
# YAML yamllint (repo's .yamllint if any, else relaxed)
|
||||||
|
# CI workflows actionlint (.forgejo/ and .github/ workflows)
|
||||||
|
# OpenTofu/HCL tofu fmt + tflint (repo's .tflint.hcl if any)
|
||||||
|
#
|
||||||
|
# Every linter runs even if an earlier one failed, so one push shows every
|
||||||
|
# problem; the job fails if any of them did. Turn one off with its input.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
shellcheck:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
shellcheck-severity:
|
||||||
|
description: Lowest shellcheck severity that fails the job (error, warning, info, style).
|
||||||
|
type: string
|
||||||
|
default: warning
|
||||||
|
ruff:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
ruff-format:
|
||||||
|
description: Also require `ruff format --check` to pass.
|
||||||
|
type: boolean
|
||||||
|
default: false
|
||||||
|
hadolint:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
yamllint:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
actionlint:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
terraform:
|
||||||
|
type: boolean
|
||||||
|
default: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: shellcheck
|
||||||
|
if: ${{ !cancelled() && inputs.shellcheck }}
|
||||||
|
env:
|
||||||
|
SEVERITY: ${{ inputs.shellcheck-severity }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git ls-files '*.sh' '*.bash')" ]; then echo "No shell scripts; skipping."; exit 0; fi
|
||||||
|
git ls-files -z '*.sh' '*.bash' | xargs -0 shellcheck --severity="$SEVERITY"
|
||||||
|
|
||||||
|
- name: ruff
|
||||||
|
if: ${{ !cancelled() && inputs.ruff }}
|
||||||
|
env:
|
||||||
|
FORMAT: ${{ inputs.ruff-format }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git ls-files '*.py' '*.pyi')" ]; then echo "No Python; skipping."; exit 0; fi
|
||||||
|
ruff --version
|
||||||
|
ruff check --output-format=full .
|
||||||
|
if [ "$FORMAT" = "true" ]; then ruff format --check .; fi
|
||||||
|
|
||||||
|
- name: hadolint
|
||||||
|
if: ${{ !cancelled() && inputs.hadolint }}
|
||||||
|
run: |
|
||||||
|
set -- 'Dockerfile' '*/Dockerfile' 'Dockerfile.*' '*/Dockerfile.*' '*.Dockerfile' 'Containerfile' '*/Containerfile'
|
||||||
|
if [ -z "$(git ls-files "$@")" ]; then echo "No Dockerfiles; skipping."; exit 0; fi
|
||||||
|
git ls-files -z "$@" | xargs -0 hadolint --failure-threshold warning
|
||||||
|
|
||||||
|
- name: yamllint
|
||||||
|
if: ${{ !cancelled() && inputs.yamllint }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git ls-files '*.yml' '*.yaml')" ]; then echo "No YAML; skipping."; exit 0; fi
|
||||||
|
cfg=""
|
||||||
|
for f in .yamllint .yamllint.yml .yamllint.yaml; do if [ -f "$f" ]; then cfg="$f"; fi; done
|
||||||
|
# Without a repo config: relaxed rules, and no line-length check,
|
||||||
|
# which mostly flags long URLs and Kubernetes annotations.
|
||||||
|
if [ -n "$cfg" ]; then set -- -c "$cfg"
|
||||||
|
else set -- -d '{extends: relaxed, rules: {line-length: disable}}'; fi
|
||||||
|
git ls-files -z '*.yml' '*.yaml' | xargs -0 yamllint -f parsable "$@"
|
||||||
|
|
||||||
|
- name: actionlint
|
||||||
|
if: ${{ !cancelled() && inputs.actionlint }}
|
||||||
|
run: |
|
||||||
|
set -- '.forgejo/workflows/*.yml' '.forgejo/workflows/*.yaml' '.github/workflows/*.yml' '.github/workflows/*.yaml'
|
||||||
|
if [ -z "$(git ls-files "$@")" ]; then echo "No workflow files; skipping."; exit 0; fi
|
||||||
|
# Forgejo needs runs-on on a job that calls a cross-repo reusable
|
||||||
|
# workflow (the runner expands it, not the server), which GitHub
|
||||||
|
# forbids. Ignore that one GitHub-only rule.
|
||||||
|
git ls-files -z "$@" | xargs -0 actionlint -shellcheck= -ignore '"runs-on" is not available'
|
||||||
|
|
||||||
|
- name: OpenTofu fmt and tflint
|
||||||
|
if: ${{ !cancelled() && inputs.terraform }}
|
||||||
|
run: |
|
||||||
|
if [ -z "$(git ls-files '*.tf')" ]; then echo "No .tf files; skipping."; exit 0; fi
|
||||||
|
rc=0
|
||||||
|
tofu fmt -check -recursive -diff || rc=1
|
||||||
|
if [ -f .tflint.hcl ]; then tflint --init; fi
|
||||||
|
tflint --recursive --format compact || rc=1
|
||||||
|
exit "$rc"
|
||||||
|
|
@ -1,11 +0,0 @@
|
||||||
on:
|
|
||||||
workflow_call:
|
|
||||||
inputs:
|
|
||||||
msg:
|
|
||||||
type: string
|
|
||||||
default: hello
|
|
||||||
jobs:
|
|
||||||
probe:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- run: echo "reusable probe ran in ${GITHUB_REPOSITORY} msg=${{ inputs.msg }}"
|
|
||||||
28
.forgejo/workflows/sast.yml
Normal file
28
.forgejo/workflows/sast.yml
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
# Static analysis with semgrep.
|
||||||
|
#
|
||||||
|
# Defaults to the curated p/default ruleset rather than `--config auto`, because
|
||||||
|
# auto requires sending metrics to semgrep.dev. Paths are excluded with a
|
||||||
|
# .semgrepignore in the calling repo; a single finding can be suppressed inline
|
||||||
|
# with a `nosemgrep: <rule-id>` comment.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
config:
|
||||||
|
description: Space-separated semgrep configs (registry packs or paths in the repo).
|
||||||
|
type: string
|
||||||
|
default: p/default
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
semgrep:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: semgrep
|
||||||
|
env:
|
||||||
|
CONFIGS: ${{ inputs.config }}
|
||||||
|
SEMGREP_SEND_METRICS: "off"
|
||||||
|
run: |
|
||||||
|
semgrep --version
|
||||||
|
set -- scan --error --metrics=off --disable-version-check
|
||||||
|
for c in $CONFIGS; do set -- "$@" --config "$c"; done
|
||||||
|
semgrep "$@"
|
||||||
34
.forgejo/workflows/secrets.yml
Normal file
34
.forgejo/workflows/secrets.yml
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Secret scanning with gitleaks over the repository's full git history.
|
||||||
|
#
|
||||||
|
# Full history rather than the pushed range: a push range is unreliable on
|
||||||
|
# force-pushes and first pushes, and these repos are small enough that a full
|
||||||
|
# scan takes seconds. A secret that was committed and later deleted is still
|
||||||
|
# exposed, so history is exactly what needs scanning.
|
||||||
|
#
|
||||||
|
# The calling repo controls what counts as a finding:
|
||||||
|
# .gitleaks.toml extra rules or allowlists, picked up automatically
|
||||||
|
# .gitleaksignore fingerprints of accepted findings, one per line, as
|
||||||
|
# printed in the "Fingerprint:" line of this job's output
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
log-opts:
|
||||||
|
description: Extra `git log` options to narrow the scan, e.g. a commit range. Empty scans all history.
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
gitleaks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: gitleaks
|
||||||
|
env:
|
||||||
|
LOG_OPTS: ${{ inputs.log-opts }}
|
||||||
|
run: |
|
||||||
|
gitleaks version
|
||||||
|
set -- git --redact --verbose --no-banner --exit-code 1
|
||||||
|
if [ -n "$LOG_OPTS" ]; then set -- "$@" --log-opts "$LOG_OPTS"; fi
|
||||||
|
gitleaks "$@" .
|
||||||
66
README.md
Normal file
66
README.md
Normal file
|
|
@ -0,0 +1,66 @@
|
||||||
|
# ci-workflows
|
||||||
|
|
||||||
|
Shared reusable Forgejo Actions workflows for every repo on git.capybara.network.
|
||||||
|
|
||||||
|
| Workflow | Tool | Fails on |
|
||||||
|
|---|---|---|
|
||||||
|
| `secrets.yml` | gitleaks, full git history | any secret not listed in `.gitleaksignore` |
|
||||||
|
| `sast.yml` | semgrep, `p/default` | any finding not suppressed with `nosemgrep` or `.semgrepignore` |
|
||||||
|
| `deps.yml` | osv-scanner | a known vulnerability in a lockfile or manifest, unless ignored in `osv-scanner.toml` |
|
||||||
|
| `lint.yml` | shellcheck, ruff, hadolint, yamllint, actionlint, `tofu fmt` + tflint | lint errors; each linter runs only if matching files exist |
|
||||||
|
|
||||||
|
The tools are baked into the runner image (`flux-talos`,
|
||||||
|
`clusters/talos/apps/forgejo-runner/image/Dockerfile`); these workflows install
|
||||||
|
nothing at run time.
|
||||||
|
|
||||||
|
## Using them
|
||||||
|
|
||||||
|
Add `.forgejo/workflows/checks.yml` to a repo:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: checks
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
secrets:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: capyadmin/ci-workflows/.forgejo/workflows/secrets.yml@v1
|
||||||
|
sast:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: capyadmin/ci-workflows/.forgejo/workflows/sast.yml@v1
|
||||||
|
deps:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: capyadmin/ci-workflows/.forgejo/workflows/deps.yml@v1
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: capyadmin/ci-workflows/.forgejo/workflows/lint.yml@v1
|
||||||
|
```
|
||||||
|
|
||||||
|
Two Forgejo 11 constraints shape this, both verified on this instance on 18 Oct 2026:
|
||||||
|
|
||||||
|
- **This repo must stay public.** The runner fetches a cross-repo reusable
|
||||||
|
workflow anonymously; with the repo private the calling job fails. Never put
|
||||||
|
anything secret here.
|
||||||
|
- **The calling job needs `runs-on`.** Forgejo 11 expands the reusable workflow
|
||||||
|
in the runner, not the server, so the caller has to be scheduled onto one.
|
||||||
|
GitHub forbids this combination; `lint.yml` tells actionlint to ignore it.
|
||||||
|
|
||||||
|
Inputs (all optional) are documented at the top of each workflow, e.g.:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
lint:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
uses: capyadmin/ci-workflows/.forgejo/workflows/lint.yml@v1
|
||||||
|
with:
|
||||||
|
ruff-format: true
|
||||||
|
shellcheck-severity: error
|
||||||
|
```
|
||||||
|
|
||||||
|
## Versioning
|
||||||
|
|
||||||
|
`v1` is a moving tag on the latest compatible commit. A change that could
|
||||||
|
newly fail existing callers (a stricter default, a new linter switched on by
|
||||||
|
default) goes out as `v2`.
|
||||||
Loading…
Add table
Add a link
Reference in a new issue