PR Pipeline — Authorize → Validate → Review → Merge / auth (pull_request) Successful in 5s
PR Pipeline — Authorize → Validate → Review → Merge / validate (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / lint (pull_request) Successful in 18s
PR Pipeline — Authorize → Validate → Review → Merge / ai-review (pull_request) Successful in 3s
PR Pipeline — Authorize → Validate → Review → Merge / gate (pull_request) Successful in 1s
The 2026-09-17 purge removed six live credentials that had sat in this repo for weeks, several in .md prose. Nothing blocked that class of commit, so a warning in a stream nobody reads was the only signal. This adds a guard that fails the build instead of warning. Guard - scripts/secret-scan.sh: bash + coreutils + grep/sed/awk + git only (the Gitea Actions runner executes job steps inside the runner container — BusyBox grep, no node/python). Modes: --tree (git-tracked, default), --path DIR (no git), --staged (pre-commit), --diff REF. Exit 1 on a finding, 2 on config error. - scripts/secret-patterns.tsv: checked-in pattern list — sk-, sk-or-v1-, sk_live_, literal Bearer tokens, PVEAPIToken=, raw Authorization values, PEM private-key blocks, prose credential lines, and password/api_key/secret/token assignments carrying a literal value. Prose is scanned exactly like code. - scripts/secret-allowlist.tsv: one entry per deliberate synthetic example, each with a reason. A missing reason is a hard error (fail closed). The 2026-09-17 purge's `«vault: ...»` placeholders are listed explicitly rather than filtered by a general "vault"/"synthetic" rule, so a new occurrence still needs a reviewed, reasoned entry. - A small inert-value classifier drops env refs, paths, dotted code access, variable names and right-truncated redactions; it does not know the words "synthetic"/"example", so a fabrication is always an explicit exception. - Findings are printed with the credential masked; a scan never echoes a full secret into the log. Wiring - .gitea/workflows/pr-pipeline.yaml lint job: explicit "Committed-credential scan" step plus the self-test. A finding fails the required `pr-pipeline / lint` context, which the merge gate depends on. - scripts/prose-lint.sh (the local gate): a "Secret scan" section, so `bash scripts/prose-lint.sh` before pushing is equivalent to CI. Tests - tests/test_secret_scan.sh: 20 cases. Plants pattern-matching fixtures in temp trees (outside every allowlisted path) and asserts the guard FAILS, including the --staged commit-time path; asserts the tree is quiet; asserts allowlisted text at an unlisted path still fails (path-explicit, not word-based); asserts a reasonless allowlist entry exits 2. Verified: guard run against 8245716^ (the pre-fix revision, before the purge) fails on the real OpenRouter/LiteLLM/Zulip/Proxmox/Stirling credentials; guard run over the current tree is clean.
122 lines
5.0 KiB
YAML
122 lines
5.0 KiB
YAML
name: PR Pipeline — Authorize → Validate → Review → Merge
|
|
# TRIGGER IS INTENTIONALLY UNFILTERED — DO NOT RE-ADD A `paths:` FILTER.
|
|
#
|
|
# This workflow previously carried `paths: ['**.prose.md', 'scripts/**.sh',
|
|
# '**.yaml', '**.yml']` on both `push` and `pull_request`. Any PR whose diff
|
|
# touched none of those patterns (for example a `deliverables/`-only PR, or a
|
|
# `scripts/*.py` / `bin/*` change) therefore produced NO Gitea Actions run at
|
|
# all: validation, lint, ai-review and the merge gate were silently skipped.
|
|
# Validation must run for every pull request and every push to master, so the
|
|
# trigger is deliberately unconditional.
|
|
on:
|
|
push:
|
|
branches: [master]
|
|
pull_request:
|
|
types: [opened, synchronize, reopened]
|
|
|
|
jobs:
|
|
auth:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout repository
|
|
run: |
|
|
git clone --depth=50 "http://192.168.68.17:3000/${{ gitea.repository }}.git" .
|
|
git fetch origin "${{ gitea.ref }}" --depth=50
|
|
git checkout "${{ gitea.sha }}"
|
|
|
|
- name: Authorization check
|
|
run: bash scripts/prose-auth-check.sh
|
|
|
|
validate:
|
|
runs-on: ubuntu-latest
|
|
needs: auth
|
|
steps:
|
|
- name: Checkout repository
|
|
run: |
|
|
git clone --depth=50 "http://192.168.68.17:3000/${{ gitea.repository }}.git" .
|
|
git fetch origin "${{ gitea.ref }}" --depth=50
|
|
git checkout "${{ gitea.sha }}"
|
|
|
|
- name: YAML frontmatter validation
|
|
run: |
|
|
echo "=== Prose Contract Frontmatter Validation ==="
|
|
FAILED=0
|
|
for f in $(find . -name "*.prose.md" -not -path "./.git/*" -not -path "./runs/*"); do
|
|
# NOTE: use herestrings, not `echo "$FM" | grep ...`. Under the runner's
|
|
# `-e -o pipefail`, `grep -q` exits on first match and can SIGPIPE the
|
|
# producer, making the pipeline report non-zero and raising a false
|
|
# "Missing name/description" whose file set varies run to run.
|
|
FM=$(sed -n '/^---$/,/^---$/p' "$f" | sed '1d;$d')
|
|
[ -z "$FM" ] && { echo " ❌ $f: No YAML frontmatter"; FAILED=$((FAILED+1)); continue; }
|
|
|
|
KIND=$(grep '^kind:' <<< "$FM" | awk '{print $2}')
|
|
case "$KIND" in
|
|
function|responsibility|gateway|pattern|test|template|architecture|enforcement) echo " ✅ $f: kind=$KIND" ;;
|
|
*) echo " ❌ $f: Invalid kind='$KIND'"; FAILED=$((FAILED+1)) ;;
|
|
esac
|
|
|
|
grep -q '^name:' <<< "$FM" || { echo " ❌ $f: Missing name"; FAILED=$((FAILED+1)); }
|
|
grep -q '^description:' <<< "$FM" || { echo " ❌ $f: Missing description"; FAILED=$((FAILED+1)); }
|
|
done
|
|
[ $FAILED -gt 0 ] && { echo "❌ FRONTMATTER FAILED ($FAILED error(s))"; exit 1; }
|
|
echo "✅ Frontmatter validation passed"
|
|
|
|
lint:
|
|
runs-on: ubuntu-latest
|
|
needs: validate
|
|
steps:
|
|
- name: Checkout repository
|
|
run: |
|
|
git clone --depth=50 "http://192.168.68.17:3000/${{ gitea.repository }}.git" .
|
|
git fetch origin "${{ gitea.ref }}" --depth=50
|
|
git checkout "${{ gitea.sha }}"
|
|
|
|
- name: Committed-credential scan (secret guard)
|
|
run: |
|
|
# Fails the build on a credential-shaped string in the tree. Patterns
|
|
# live in scripts/secret-patterns.tsv; the only tolerated literal
|
|
# examples are in scripts/secret-allowlist.tsv, each with a reason.
|
|
# Do not turn this into a warning: a warning in a stream nobody reads
|
|
# is how six live credentials sat in this repo for weeks.
|
|
bash scripts/secret-scan.sh
|
|
|
|
- name: Secret guard self-test
|
|
run: bash tests/test_secret_scan.sh
|
|
|
|
- name: Structure + regression + consistency lint
|
|
run: bash scripts/prose-lint.sh
|
|
|
|
ai-review:
|
|
runs-on: ubuntu-latest
|
|
needs: validate
|
|
steps:
|
|
- name: Checkout repository
|
|
run: |
|
|
git clone --depth=50 "http://192.168.68.17:3000/${{ gitea.repository }}.git" .
|
|
git fetch origin "${{ gitea.ref }}" --depth=50
|
|
git checkout "${{ gitea.sha }}"
|
|
|
|
- name: AI-powered contract review
|
|
env:
|
|
LITELLM_URL: ${{ secrets.LITELLM_URL }}
|
|
LITELLM_KEY: ${{ secrets.LITELLM_KEY }}
|
|
run: bash scripts/prose-ai-review.sh
|
|
|
|
gate:
|
|
runs-on: ubuntu-latest
|
|
needs: [auth, validate, lint, ai-review]
|
|
if: success()
|
|
steps:
|
|
- name: Merge gate
|
|
run: |
|
|
echo "╔══════════════════════════════════════╗"
|
|
echo "║ ALL CHECKS PASSED — SAFE TO MERGE ║"
|
|
echo "╚══════════════════════════════════════╝"
|
|
echo ""
|
|
echo " ✅ auth — authorized agent"
|
|
echo " ✅ validate — frontmatter valid"
|
|
echo " ✅ lint — structure + no regressions"
|
|
echo " ✅ ai-review — no contradictions with ground truth"
|
|
echo ""
|
|
echo "Merge this PR to deploy to main."
|