Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,17 +1,42 @@
## Summary
## Canonical issue

Describe the outcome and the evidence that supports it.
Closes #

## Linked issue
## Outcome

Fixes #
Describe the user or operational result this PR produces.

## Scope

- Included:
- Explicitly excluded:

## Risk

- Risk level: low / medium / high
- Failure mode:
- Rollback:

## Verification

List exact automated and manual checks, tied to the current head SHA.

- [ ] Focused tests
- [ ] Required CI
- [ ] Review threads resolved

## Production evidence

Provide the Vercel preview, production deployment, runtime evidence, or state why production evidence is not applicable.

## Agent handoff

- [ ] One canonical issue is linked
- [ ] No competing PR implements the same issue
- [ ] Acceptance criteria are satisfied
- [ ] Required checks pass on the current head
- [ ] Human decision is requested only for product, security, irreversible infrastructure, or production approval

## Agent provenance

Human-authored pull requests may delete this section. Agent-authored pull requests must replace agent-lock-example with agent-lock-manifest and fill the values. Scope and test paths remain authoritative in the linked issue.
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/AUDIT.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ concrete reason, verified against the actual repository tree.
| `ci.yml` | **FIX** | Added blocking `apps/web` type-check and ESLint steps before the build so CI fails fast on TypeScript or lint regressions. |
| `codeql-analysis.yml` | **FIX** | Removed the OWASP `dependency-check` job — pinned to unstable `@main` and pointed at dead paths (`frontend/node_modules`, `src/mcp-bridge.py`); produced no usable SARIF. Switched the Node cache from the dead `frontend/node_modules` path to the npm download cache (`~/.npm`), which is correct for this npm-workspaces repo. CodeQL analysis itself retained. Dependency coverage already lives in `dependency-review.yml` + `security.yml`. |
| `coverage.yml` | **FIX** | Added a top-level `name:` and the `workflow_dispatch` trigger the README already documented as available. |
| `gh-aw-validation.yml` | **ADD** | Adds pinned gh-aw (`v0.82.14`) validation for EventRelay's custom markdown workflows. Enforces compile/validate plus actionlint, zizmor, and poutine checks, and verifies committed lock files. |
| `dependabot-auto-merge.yml` | KEEP | Comprehensive guards (same-repo, non-draft, SHA match, major excluded). |
| `dependency-review.yml` | KEEP | PR dependency review with documented allow-lists. |
| `deploy-cloud-run.yml` | KEEP | The real deployment path (GCP Cloud Run); manual dispatch. |
Expand Down Expand Up @@ -66,4 +65,9 @@ valid. Referenced paths were checked against the working tree:

| `agent-completion-enforcement.yml` | **ADD** | Protected-default-branch verifier that creates the independent **Agent completion enforcement** Check directly against the PR head SHA. It accepts only an exact-head machine-readable report from the configured dedicated GitHub App; missing/stale/mutable evidence, untrusted label provenance, and custom roles all fail closed. The existing `agent-completion/truth-gate` status stays advisory and must not be made required. |

The protected policy at `.github/agent-lock/trusted-publishers.json` starts with empty allowlists and therefore blocks until a repository administrator provisions the dedicated App and trusted actor identities through protected review. The repository ruleset must then require **Agent completion enforcement**, one independent approval, and resolved conversations.
The protected policy at `.github/agent-lock/trusted-publishers.json` starts with empty allowlists and therefore blocks until a repository administrator provisions the dedicated App and trusted actor identities through protected review. The repository ruleset must then require **Agent completion enforcement**, one independent approval, and resolved conversations.

## Repository governance workflows

| `pr-governance.yml` | **ADD** | Validates that every non-draft ready PR links exactly one real open issue (not a PR number) with non-empty delivery evidence sections (Outcome, Risk, Verification, Production evidence). Fails closed on competing implementation PRs. Triggers on `pull_request_target`. |
| `repository-reconciliation.yml` | **ADD** | Scheduled (13:17 UTC daily) non-destructive reconciliation report: identifies ready PRs missing a canonical issue, issues with competing implementation PRs (references validated via Issues API), and stale unattached branches. Excludes draft PRs and fork-branch name collisions. Upserts a single issue titled "[automation] Repository drift report". |
5 changes: 2 additions & 3 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ workflow; this README is the index.
|----------|------|---------|---------|
| CI | `ci.yml` | push / PR to `main` | Type-check + lint `apps/web`, build the web app, lint Python (informational), run unit tests |
| Coverage | `coverage.yml` | push / PR to `main`,`develop`; manual | Generate pytest coverage and upload lcov to Qlty |
| gh-aw Validation | `gh-aw-validation.yml` | push / PR to `main` on gh-aw files; manual | Pin `gh aw` to `v0.82.14`, compile custom EventRelay `.md` workflows, and run validate + actionlint + zizmor + poutine checks |
| CodeQL Analysis | `codeql-analysis.yml` | push / PR to `main`; weekly (Mon 06:00 UTC) | Static security analysis for JavaScript/TypeScript and Python |
| Security Scan | `security.yml` | push / PR to `main`; weekly (Sun 00:00 UTC) | npm audit, Python safety, bandit, Trivy image scan |
| Dependency Review | `dependency-review.yml` | PR to `main`,`develop` | Review new dependencies for vulnerabilities and license policy |
Expand Down Expand Up @@ -70,8 +69,6 @@ Generates pytest coverage and uploads lcov to Qlty.
<https://qlty.sh>, then add it under **Settings → Secrets and variables →
Actions**.
- Coverage HTML and lcov are stored as artifacts for 30 days.
- The test step is authoritative (`--cov-fail-under=90`, no `continue-on-error`,
no `|| true`) so failures cannot report green.

### Deploy to Google Cloud Run — `deploy-cloud-run.yml`

Expand Down Expand Up @@ -124,6 +121,8 @@ A full audit of this directory was performed (see


| Agent completion enforcement | `agent-completion-enforcement.yml` | `pull_request_target`; manual | Creates the independent, head-bound `Agent completion enforcement` Check from protected default-branch code. |
| PR Governance | `pr-governance.yml` | `pull_request_target` (opened/edited/reopened/synchronize/ready_for_review) | Validates that every ready PR links exactly one real open canonical issue and contains non-empty delivery evidence sections; fails on competing PRs. |
| Repository Reconciliation | `repository-reconciliation.yml` | daily (13:17 UTC); manual | Non-destructive daily report of ready PRs missing a canonical issue, issues with competing implementation PRs, and stale unattached branches. |

## Agent-completion enforcement

Expand Down
173 changes: 173 additions & 0 deletions .github/workflows/pr-governance.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
name: PR Governance

on:
pull_request_target:
types: [opened, edited, reopened, synchronize, ready_for_review]

permissions:
checks: write
contents: read
issues: read
pull-requests: read

concurrency:
group: pr-governance-${{ github.event.pull_request.number }}
cancel-in-progress: true

jobs:
policy:
name: Canonical issue and evidence
runs-on: ubuntu-latest
steps:
- name: Validate delivery contract and publish exact-head Check
uses: actions/github-script@v8
with:
script: |
const pr = context.payload.pull_request;
const runUrl =
`${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;

async function publish(conclusion, title, summary) {
await github.rest.checks.create({
owner: context.repo.owner,
repo: context.repo.repo,
name: "PR Governance",
head_sha: pr.head.sha,
status: "completed",
conclusion,
details_url: runUrl,
output: {
title,
summary: summary.slice(0, 60000)
}
});
if (conclusion === "failure") {
core.setFailed(summary);
}
}

if (pr.draft) {
await publish(
"neutral",
"Governance deferred for draft PR",
`Draft PR #${pr.number} is not enforced. The Check is bound to exact head ${pr.head.sha}.`
);
return;
}

const body = pr.body || "";

function getSectionContent(text, heading) {
const escapedHeading = heading.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(
escapedHeading + "\\s*\\n([\\s\\S]*?)(?=\\n## |$)",
"i"
);
const match = text.match(pattern);
if (!match) return null;
return match[1].replace(/<!--[\s\S]*?-->/g, "").trim();
}

const placeholderPatterns = [
/^Describe the user or operational result this PR produces\.?$/i,
/^List exact automated and manual checks, tied to the current head SHA\.?$/i,
/^Provide the Vercel preview, production deployment, runtime evidence, or state why production evidence is not applicable\.?$/i,
/^-\s*Risk level:\s*low\s*\/\s*medium\s*\/\s*high\s*$/i,
/^-\s*Failure mode:\s*$/i,
/^-\s*Rollback:\s*$/i,
/^-\s*\[\s\]\s*(Focused tests|Required CI|Review threads resolved)\s*$/i,
/^(Closes?|Fix(?:es|ed)?|Resolves?)\s+#\s*$/i
];

function hasMeaningfulContent(content) {
if (content === null) return false;
const meaningfulLines = content
.split(/\r?\n/)
.map(line => line.trim())
.filter(Boolean)
.filter(line => !placeholderPatterns.some(pattern => pattern.test(line)));
return meaningfulLines.length > 0;
}

const requiredSections = [
"## Canonical issue",
"## Outcome",
"## Risk",
"## Verification",
"## Production evidence"
];
const findings = requiredSections
.filter(section => !hasMeaningfulContent(getSectionContent(body, section)))
.map(section => `${section} is missing or still contains only template placeholders`);

const closingPattern =
/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s+#(\d+)/gi;
const canonicalIssues = [
...new Set(
[...body.matchAll(closingPattern)].map(match => Number(match[1]))
)
];

if (canonicalIssues.length !== 1) {
findings.push("exactly one closing reference is required: Closes #<issue>");
}

if (canonicalIssues.length === 1) {
const canonical = canonicalIssues[0];
try {
const issueResp = await github.rest.issues.get({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: canonical
});
const issue = issueResp.data;
if (issue.pull_request) {
findings.push(`#${canonical} is a pull request, not an issue`);
} else if (issue.state !== "open") {
findings.push(`#${canonical} is not open (state: ${issue.state})`);
}
} catch (error) {
if (error.status === 404) {
findings.push(`#${canonical} does not exist in this repository`);
} else {
throw error;
}
}

if (findings.length === 0) {
const pulls = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: "open",
per_page: 100
});
const competing = pulls.filter(candidate => {
if (candidate.number === pr.number) return false;
const matches = [
...(candidate.body || "").matchAll(closingPattern)
].map(match => Number(match[1]));
return matches.includes(canonical);
});
if (competing.length) {
findings.push(
`Issue #${canonical} already has another open implementation PR: ` +
competing.map(candidate => `#${candidate.number}`).join(", ")
);
}
}
}

if (findings.length) {
await publish(
"failure",
"Canonical delivery contract blocked",
findings.join("; ")
);
return;
}

await publish(
"success",
"Canonical delivery contract verified",
`PR #${pr.number} has one real open canonical issue and meaningful evidence. Verified exact head ${pr.head.sha}.`
);
Loading
Loading