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