A centralised governance engine that runs as a GitHub Actions workflow across all organisation repositories. It validates pull request contributors, classifies changes, enforces policy, and emits structured audit events to configurable sinks.
On every pull request, the engine:
- Resolves the contributor's role —
maintainer,external, orstudent— from the merged global + repo config. - Classifies the PR type —
infra,docs,bug, orfeature— by inspecting changed files, the title, and the body. - Validates the contributor against email domain rules defined in policy.
- Blocks or approves the PR by setting a GitHub Actions failure status and posting a comment.
- Applies labels (
role:<role>+type:<type>) after cleaning up any stale governance labels. - Emits a structured audit event (
pr.governance.result) to all configured sinks. - Probes and archives standard CI artifacts (
build.log,eslint.sarif, etc.) into a consolidated report.
The engine expects specific CI outputs to populate the governance report and archive.
| Category | Preferred Format | Typical Filename |
|---|---|---|
| Build logs | Plain text | build.log |
| Build summary | JSON (custom) | build-summary.json |
| Unit tests | JUnit XML | junit.xml |
| Coverage (JS/TS) | LCOV | lcov.info |
| Coverage (general) | Cobertura XML | coverage.xml |
| Lint | SARIF | eslint.sarif |
| Static Analysis | SARIF | codeql.sarif, semgrep.sarif |
| Security | SARIF | trivy.sarif, dependency.sarif |
| Dependency graph / SBOM | CycloneDX | cyclonedx.json |
| Benchmark/Performance | JSON | benchmark.json |
engine/
├── run.js # Orchestrator — main entry point
├── configLoader.js # Fetches global policy + loads repo override
├── mergeConfig.js # Merges global + repo config (union rules for domains/whitelists)
├── identity.js # Resolves contributor role from config
├── classifier.js # Classifies PR type from files, title, and body
├── validator.js # Validates contributor against policy rules
├── auditEmitter.js # Builds and emits the structured audit event
├── eventRouter.js # Routes the audit event to enabled sinks
├── formatters/
│ └── discordFormatter.js # Formats event into a Discord embed payload
└── sinks/
├── discordSink.js # Posts to a Discord webhook
├── repoSink.js # Appends to a JSONL file in the audit-log repo
└── webhookSink.js # Posts raw event JSON to a generic webhook
The engine fetches its policy from this repository at runtime:
https://raw.githubusercontent.com/Programming-Club-Curtin-Colombo/governance/main/standards/global.governance.json
This is the source of truth for all org-wide rules.
Any consuming repository can place a .governance.json file in its root to extend the global policy:
{
"governance": {
"lockedVersion": "v1.2.0"
},
"roles": {
"maintainers": ["your-github-username"]
},
"emailValidation": {
"allowedEmailDomains": ["yourcompany.com"]
}
}Merge behaviour:
roles.maintainersandroles.external.whitelistUsers— union (combined, deduplicated)emailValidation.allowedEmailDomains— union- All other keys — repo config overrides global
To pin a repository to a specific governance version, set governance.lockedVersion in .governance.json. Otherwise the engine always uses the main branch policy.
You can also override via the GOVERNANCE_VERSION environment variable in the workflow.
Every governance decision emits an event conforming to this schema (standards/event.schema.json):
| Field | Type | Description |
|---|---|---|
event |
string |
Always governance.result |
eventVersion |
string |
Schema version (now 2.0) |
timestamp |
string |
ISO-8601 |
repo |
string |
owner/repo |
entity |
object |
The subject of the event (PR, Push, etc.) |
entity.type |
string |
pull_request | push |
entity.number |
number |
Pull request number (if applicable) |
entity.branch |
string |
Branch name (if push event) |
user |
string |
GitHub username |
email |
string |
GitHub account email |
role |
string |
student | external | maintainer |
type |
string |
feature | bug | infra | docs |
allowed |
boolean |
Whether the PR passed governance |
reason |
string |
Human-readable decision reason |
policyVersion |
string |
The governance policy version applied |
Sinks are enabled in global.governance.json under audit.sinks:
{
"audit": {
"sinks": {
"discord": true,
"repo": true,
"webhook": false
}
}
}| Sink | Description | Required Secret / Config |
|---|---|---|
discord |
Posts a formatted embed to a Discord channel | DISCORD_AUDIT_WEBHOOK_URL secret |
repo |
Appends event as a JSONL line to Programming-Club-Curtin-Colombo/audit-log |
GITHUB_TOKEN with write access |
webhook |
POSTs raw event JSON to a generic HTTP endpoint | DISCORD_AUDIT_WEBHOOK_URL (reused) |
The classifier runs in priority order:
| Priority | Type | Rule |
|---|---|---|
| 1 | infra |
Any changed file matches .github/, dockerfile, docker-compose, ci, workflow, package.json, package-lock.json |
| 2 | docs |
All changed files are .md or under docs/, OR title/body contains readme or documentation |
| 3 | bug |
Title or body contains fix, bug, error, issue, crash, or broken |
| 4 | feature |
Default fallback |
| Role | How assigned | Validation |
|---|---|---|
maintainer |
Listed in config.roles.maintainers |
Always approved — bypasses all checks |
external |
Listed in config.roles.external.whitelistUsers |
Subject to email domain check |
student |
Default (everyone else) | Subject to email domain check |
Add the following workflow to your repository at .github/workflows/governance.yml:
name: Governance
on:
pull_request:
permissions:
contents: read
pull-requests: write
issues: write
jobs:
governance:
name: Run Governance Engine
runs-on: ubuntu-latest
outputs:
allowed: ${{ steps.governance.outputs.allowed }}
role: ${{ steps.governance.outputs.role }}
type: ${{ steps.governance.outputs.type }}
steps:
- name: Download all reports
uses: actions/download-artifact@v4
with:
path: reports
- name: Install Engine Dependencies
run: npm install --prefix ${{ github.workspace }} @actions/core@1.10.1 @actions/github@6.0.0
- name: Run Governance
id: governance
working-directory: ${{ github.workspace }}
run: node engine/run.js
env:
INPUT_GITHUB-TOKEN: ${{ secrets.GITHUB_TOKEN }}
INPUT_DISCORD-WEBHOOK-URL: ${{ secrets.DISCORD_AUDIT_WEBHOOK_URL }}
INPUT_GOVERNANCE-WEBHOOK-URL: ${{ secrets.GOVERNANCE_WEBHOOK_URL }}
INPUT_BUILD-STATUS: ${{ needs.build.outputs.passed }}
INPUT_LINT-STATUS: ${{ needs.lint.outputs.passed }}
INPUT_STATIC-STATUS: ${{ needs.static.outputs.passed }}
INPUT_TEST-STATUS: ${{ needs.test.outputs.passed }}
INPUT_COVERAGE-STATUS: ${{ needs.coverage.outputs.passed }}
INPUT_SECURITY-STATUS: ${{ needs.security.outputs.passed }}
INPUT_SBOM-STATUS: ${{ needs.sbom.outputs.passed }}
INPUT_BENCHMARK-STATUS: ${{ needs.benchmark.outputs.passed }}
enforce:
name: Enforce Governance Gate
runs-on: ubuntu-latest
needs: governance
steps:
- name: Validate Governance Result
run: |
if [ "${{ needs.governance.outputs.allowed }}" != "true" ]; then
echo "❌ Governance policy blocked this PR"
exit 1
fi
echo "✅ Governance checks passed"Optionally create a .governance.json in your repo root to extend the global policy:
{
"governance": {
"lockedVersion": "v1.0.0"
},
"roles": {
"maintainers": ["your-github-username"]
},
"emailValidation": {
"allowedEmailDomains": ["yourcompany.com"]
},
"audit": {
"sinks": {
"discord": true,
"repo": true,
"webhook": false
}
}
}| Secret | Required | Purpose |
|---|---|---|
GITHUB_TOKEN |
Yes (auto-provided) | Label management, PR comments, repo audit sink |
DISCORD_AUDIT_WEBHOOK_URL |
If discord sink enabled |
Discord audit notifications |
GOVERNANCE_WEBHOOK_URL |
If webhook sink enabled |
Generic HTTP audit delivery |