2026-07-23 05:11:33 +00:00
|
|
|
name: secrets-guard
|
|
|
|
|
|
|
|
|
|
# Fail the build if any infra/deploy/secrets/*.yaml is committed UNENCRYPTED.
|
|
|
|
|
# SOPS stores a `sops:` metadata block and wraps every value in ENC[...]; a
|
|
|
|
|
# plaintext leak has neither. This is the backstop against the classic
|
|
|
|
|
# "committed the decrypted file by accident" incident. .sops.yaml (the
|
|
|
|
|
# plaintext config) and README.md are not *.yaml under infra/deploy/secrets/,
|
|
|
|
|
# so they're correctly ignored.
|
|
|
|
|
#
|
|
|
|
|
# Deliberately NOT path-filtered: it's a ~1s grep, and a backstop that only
|
|
|
|
|
# runs when you expect it to isn't a backstop.
|
|
|
|
|
|
|
|
|
|
on:
|
ci: gate the prod path, fold the repo-wide checks into gate, run the daemon tests
Four holes, all of which let untested or unchecked code reach an environment.
- PRs into `release` ran no build and no test. release is the branch that deploys
to prod, so the intended promotion path was the least-checked path in the repo:
the last seven release PRs ran shellcheck and the secrets check, nothing else.
pr-build now triggers on it.
- shell-lint and secrets-guard reported as standalone statuses outside `gate`,
and pr-build's own setup notes tell the operator to require only `gate`. Taken
literally that means a commit adding a plaintext SOPS file was mergeable. Both
are now called from pr-build and reduced into gate. They are deliberately not
domain-gated and not `needs: changes`: they are repo-wide invariants that must
hold on every PR, including one touching no app domain -- exactly the case
where every domain job skips and gate used to pass vacuously. For the same
reason `skipped` counts as a failure for these two, while it stays a pass for
the domain jobs it legitimately describes.
They keep their standalone pull_request trigger, so they will run twice on a
PR until branch protection is confirmed to require only `gate`. Cheap, and it
avoids breaking a rule that may still require them by name.
- build-push.yml built and pushed with no test step, and deploy.yml consumes a
TAG rather than a commit status -- so an image reaching the registry by any
route other than a dev/main PR (a direct push, a dispatch, a v*.*.* tag) was
never tested by CI, and beta/prod then rolled it. The test now sits between
build and push, so the artifact that deploys is the artifact that was tested.
Gating the artifact is what makes this work; gating the branch would not.
- backend/Dockerfile ran `go build` on daemon/ but never its tests. All 48 of
them (gateway, cron, apiclient, config) ran nowhere: `grep -rn "go test"
.forgejo/workflows/` found nothing, while the daemon owns the prod Discord
gateway websocket and every recurring-job timer. It now runs gofmt + vet +
test before the build, which is the pattern frontend/Dockerfile already uses --
and being inside `docker build` it gates build-push too. Verified clean: gofmt
reports nothing, vet passes, all four packages pass.
2026-07-25 08:36:31 +00:00
|
|
|
# workflow_call so pr-build.yml can fold this into the single required `gate`
|
|
|
|
|
# status. This is the one that mattered most: branch protection requires only
|
|
|
|
|
# `gate`, so a commit adding a PLAINTEXT secrets file failed this check as a
|
|
|
|
|
# separate status and was still mergeable.
|
|
|
|
|
workflow_call: {}
|
2026-07-23 05:11:33 +00:00
|
|
|
pull_request:
|
|
|
|
|
push:
|
|
|
|
|
branches: [dev, main, release]
|
|
|
|
|
|
|
|
|
|
jobs:
|
|
|
|
|
encrypted:
|
|
|
|
|
runs-on: docker
|
|
|
|
|
steps:
|
|
|
|
|
- uses: actions/checkout@v4
|
|
|
|
|
- name: Assert infra/deploy/secrets/*.yaml are SOPS-encrypted
|
|
|
|
|
run: |
|
|
|
|
|
set -euo pipefail
|
|
|
|
|
shopt -s nullglob
|
|
|
|
|
files=(infra/deploy/secrets/*.yaml)
|
|
|
|
|
if [ ${#files[@]} -eq 0 ]; then
|
|
|
|
|
echo "no infra/deploy/secrets/*.yaml yet — nothing to check"
|
|
|
|
|
exit 0
|
|
|
|
|
fi
|
|
|
|
|
fail=0
|
|
|
|
|
for f in "${files[@]}"; do
|
|
|
|
|
if grep -q '^sops:' "$f" && grep -q 'ENC\[AES256_GCM' "$f"; then
|
|
|
|
|
echo "ok: $f is SOPS-encrypted"
|
|
|
|
|
else
|
|
|
|
|
echo "::error file=$f::NOT SOPS-encrypted — refusing to accept a plaintext secrets file"
|
|
|
|
|
fail=1
|
|
|
|
|
fi
|
|
|
|
|
done
|
|
|
|
|
exit $fail
|