Skip to content
Merged
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
17 changes: 17 additions & 0 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
language: en-US
reviews:
profile: assertive
request_changes_workflow: false
high_level_summary: true
poem: false
review_status: true
collapse_walkthrough: false
path_instructions:
- path: "src/**"
instructions: "Prioritize projection determinism, SIWE/authentication, authorization, finality/reorg safety, PostgreSQL/TypeORM consistency, cache invalidation, secret handling, and the prohibition on backend-authoritative protocol mutation. Do not approve or merge."
- path: "migrations/**"
instructions: "Prioritize projection determinism, SIWE/authentication, authorization, finality/reorg safety, PostgreSQL/TypeORM consistency, cache invalidation, secret handling, and the prohibition on backend-authoritative protocol mutation. Do not approve or merge."
- path: ".github/workflows/**"
instructions: "Prioritize projection determinism, SIWE/authentication, authorization, finality/reorg safety, PostgreSQL/TypeORM consistency, cache invalidation, secret handling, and the prohibition on backend-authoritative protocol mutation. Do not approve or merge."
chat:
auto_reply: true
4 changes: 4 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Default ownership for production and governance changes.
* @dDevAhmed
/.github/ @dDevAhmed
/src/ @dDevAhmed
17 changes: 17 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
groups:
production-dependencies:
dependency-type: production
development-dependencies:
dependency-type: development
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 5
32 changes: 32 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
## Linked task

Closes: <!-- exactly one active V2-BE issue -->
Head SHA reviewed: `<!-- full SHA -->`

## Summary

<!-- Explain the smallest cohesive backend change and its projection behavior. -->

## Scope and assignment

- [ ] The linked issue has the exact `Stellar Wave` label.
- [ ] The PR author is assigned or explicitly approved by a maintainer.
- [ ] This PR resolves one task and all dependencies are safely completed.

## Architecture and security

- [ ] Contracts/finalized events remain authoritative for protocol state.
- [ ] No backend-authoritative claim, vote, dispute, settlement, reward, treasury, or governance mutation was added.
- [ ] TypeORM/PostgreSQL remains the only persistence architecture; Prisma was not introduced.
- [ ] Reorg, duplicate delivery, retry, finality, stale-cache, and degraded-dependency behavior is fail-closed.
- [ ] SIWE/authentication, authorization, validation, redaction, and rate-limit impact was reviewed.
- [ ] No Stellar/Soroban/Freighter runtime, secret, placeholder production value, or production mock is included.

## Validation

- [ ] Lint, typecheck, and build pass.
- [ ] Unit and PostgreSQL integration tests pass.
- [ ] Migrations and rollback validation pass.
- [ ] Indexer/reorg and protocol-invariant tests pass.
- [ ] Security, container, and artifact-drift checks pass.
- [ ] Required human CODEOWNER approval applies to this exact head SHA.
113 changes: 113 additions & 0 deletions .github/scripts/issue-audit.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { appendFile, mkdir, writeFile } from "node:fs/promises";

const token = process.env.GITHUB_TOKEN;
const repository = process.env.GITHUB_REPOSITORY;
const expectedPrefix = process.env.ISSUE_PREFIX;
const reportDir = process.env.REPORT_DIR || "reports";
const maxTask = Number(process.env.MAX_TASK || 150);

if (!token || !repository || !expectedPrefix) {
throw new Error("GITHUB_TOKEN, GITHUB_REPOSITORY, and ISSUE_PREFIX are required");
}

const [owner, repo] = repository.split("/");
const api = async (path) => {
const response = await fetch(`https://api.github.com${path}`, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "truthbounty-issue-auditor"
}
});
if (!response.ok) throw new Error(`GitHub API ${response.status} for ${path}: ${await response.text()}`);
return response.json();
};

const all = [];
for (let page = 1; ; page += 1) {
const batch = await api(`/repos/${owner}/${repo}/issues?state=all&per_page=100&page=${page}`);
all.push(...batch.filter((item) => !item.pull_request));
if (batch.length < 100) break;
}

const taskPattern = new RegExp(`^\\s*(${expectedPrefix})-(\\d{3})\\b`, "i");
const requiredHeadings = [
"## Overview",
"## Problem Context",
"## Technical Scope",
"## Security and Architecture Requirements",
"## Required Tests",
"## Acceptance Criteria",
"## Dependencies",
"## Non-Goals",
"## Complexity and Review",
"# 🏷 Labels"
];
const byId = new Map();
const tasks = [];
const findings = [];

for (const issue of all) {
const match = String(issue.title || "").match(taskPattern);
if (!match) continue;
const number = Number(match[2]);
const id = `${expectedPrefix}-${String(number).padStart(3, "0")}`;
const labels = (issue.labels || []).map((label) => typeof label === "string" ? label : label.name);
const body = String(issue.body || "");
const missingHeadings = requiredHeadings.filter((heading) => !body.includes(heading));
const complexity = labels.filter((label) => /^complexity-(low|medium|high)$/.test(label));
const record = { id, issueNumber: issue.number, state: issue.state, labels, assignees: (issue.assignees || []).map((a) => a.login), missingHeadings };
tasks.push(record);
if (!byId.has(id)) byId.set(id, []);
byId.get(id).push(record);

if (number < 1 || number > maxTask) findings.push({ severity: "warning", code: "range", id, message: `Task number is outside 001-${String(maxTask).padStart(3, "0")}.` });
if (missingHeadings.length) findings.push({ severity: "warning", code: "structure", id, message: `Missing headings: ${missingHeadings.join(", ")}` });
if (complexity.length !== 1) findings.push({ severity: "warning", code: "complexity", id, message: `Expected one complexity label; found ${complexity.length}.` });
if (labels.includes("Stellar Wave") && labels.includes("wave-candidate")) findings.push({ severity: "warning", code: "label-state", id, message: "Issue is simultaneously active and a candidate." });
if (labels.includes("Stellar Wave") && issue.state === "open" && record.assignees.length === 0) findings.push({ severity: "warning", code: "active-unassigned", id, message: "Active Wave issue has no assignee." });
}

for (const [id, records] of byId) {
if (records.length > 1) findings.push({ severity: "warning", code: "duplicate", id, message: `Duplicate task ID appears ${records.length} times.` });
}
for (let number = 1; number <= maxTask; number += 1) {
const id = `${expectedPrefix}-${String(number).padStart(3, "0")}`;
if (!byId.has(id)) findings.push({ severity: "warning", code: "gap", id, message: "Expected task ID is missing." });
}

const active = tasks.filter((task) => task.state === "open" && task.labels.includes("Stellar Wave")).length;
const candidates = tasks.filter((task) => task.state === "open" && task.labels.includes("wave-candidate")).length;
const report = {
schemaVersion: 1,
mode: "report-only",
generatedAt: new Date().toISOString(),
repository,
prefix: expectedPrefix,
totals: { tasks: tasks.length, active, candidates, findings: findings.length },
findings,
tasks
};

await mkdir(reportDir, { recursive: true });
await writeFile(`${reportDir}/issue-audit.json`, JSON.stringify(report, null, 2) + "\n");

const grouped = Object.groupBy(findings, (finding) => finding.code);
const rows = Object.entries(grouped).map(([code, entries]) => `| ${code} | ${entries.length} |`).join("\n") || "| clear | 0 |";
const summary = `# TruthBounty issue inventory audit (report only)

- Repository: \`${repository}\`
- V2 tasks found: **${tasks.length}**
- Open active tasks: **${active}**
- Open candidates: **${candidates}**
- Findings: **${findings.length}**

| Rule | Count |
|---|---:|
${rows}

The JSON artifact contains exact issue-level findings. This job performs no mutations.
`;
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, summary);
console.log(JSON.stringify(report.totals));
118 changes: 118 additions & 0 deletions .github/scripts/pr-guardian.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
import { appendFile, mkdir, writeFile } from "node:fs/promises";

const token = process.env.GITHUB_TOKEN;
const repository = process.env.GITHUB_REPOSITORY;
const prNumber = Number(process.env.PR_NUMBER);
const expectedPrefix = process.env.ISSUE_PREFIX;
const reportDir = process.env.REPORT_DIR || "reports";

if (!token || !repository || !prNumber || !expectedPrefix) {
throw new Error("GITHUB_TOKEN, GITHUB_REPOSITORY, PR_NUMBER, and ISSUE_PREFIX are required");
}

const [owner, repo] = repository.split("/");
const api = async (path) => {
const response = await fetch(`https://api.github.com${path}`, {
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${token}`,
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "truthbounty-pr-guardian"
}
});
if (!response.ok) {
throw new Error(`GitHub API ${response.status} for ${path}: ${await response.text()}`);
}
return response.json();
};

const pr = await api(`/repos/${owner}/${repo}/pulls/${prNumber}`);
const closingPattern = /\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)\s*:?[ \t]*(?:https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/)?#?(\d+)\b/gi;
const linkedNumbers = [...new Set([...String(pr.body || "").matchAll(closingPattern)].map((match) => Number(match[1])))];

const findings = [];
const facts = {
repository,
pullRequest: prNumber,
headSha: pr.head.sha,
author: pr.user.login,
draft: pr.draft,
linkedIssues: linkedNumbers
};

if (pr.draft) findings.push({ severity: "notice", code: "draft", message: "Draft PRs are not merge candidates." });
if (linkedNumbers.length !== 1) {
findings.push({
severity: "warning",
code: "one-task-one-pr",
message: `Expected exactly one closing issue reference; found ${linkedNumbers.length}.`
});
}

if (linkedNumbers.length === 1) {
const issue = await api(`/repos/${owner}/${repo}/issues/${linkedNumbers[0]}`);
const labels = (issue.labels || []).map((label) => typeof label === "string" ? label : label.name);
const assignees = (issue.assignees || []).map((assignee) => assignee.login);
const taskMatch = String(issue.title || "").match(new RegExp(`^\\s*(${expectedPrefix}-\\d{3})\\b`, "i"));
Object.assign(facts, {
issueNumber: issue.number,
issueState: issue.state,
taskId: taskMatch?.[1]?.toUpperCase() || null,
labels,
assignees
});

if (!taskMatch) findings.push({ severity: "warning", code: "task-id", message: `Linked issue title must begin with ${expectedPrefix}-NNN.` });
if (issue.state !== "open") findings.push({ severity: "warning", code: "issue-state", message: "Linked issue is not open." });
if (!labels.includes("Stellar Wave")) findings.push({ severity: "warning", code: "activation", message: 'Linked issue lacks the exact "Stellar Wave" activation label.' });
if (labels.includes("wave-candidate") && labels.includes("Stellar Wave")) findings.push({ severity: "warning", code: "label-state", message: "Issue cannot be both wave-candidate and Stellar Wave." });
if (!assignees.includes(pr.user.login)) findings.push({ severity: "warning", code: "assignment", message: `PR author @${pr.user.login} is not assigned to the linked issue; maintainer approval must be recorded.` });
}

const sensitiveFiles = [
/^\.github\/workflows\//,
/(?:auth|siwe|wallet|signature|settlement|reward|treasury|governance|migration|indexer|reorg|deploy)/i,
/\.sol$/i
];
const files = [];
for (let page = 1; ; page += 1) {
const batch = await api(`/repos/${owner}/${repo}/pulls/${prNumber}/files?per_page=100&page=${page}`);
files.push(...batch);
if (batch.length < 100) break;
}
facts.changedFiles = files.map((file) => file.filename);
facts.securitySensitive = files.some((file) => sensitiveFiles.some((pattern) => pattern.test(file.filename)));
if (facts.securitySensitive) {
findings.push({ severity: "notice", code: "human-review", message: "Security-sensitive paths changed; exact-head human maintainer approval is mandatory." });
}

const report = {
schemaVersion: 1,
mode: "report-only",
generatedAt: new Date().toISOString(),
facts,
findings
};

await mkdir(reportDir, { recursive: true });
await writeFile(`${reportDir}/pr-guardian-${prNumber}.json`, JSON.stringify(report, null, 2) + "\n");

const rows = findings.length
? findings.map((finding) => `| ${finding.severity} | ${finding.code} | ${finding.message.replaceAll("|", "\\|")} |`).join("\n")
: "| notice | clear | No policy findings at this stage. |";
const summary = `# TruthBounty PR Guardian (report only)

- Repository: \`${repository}\`
- Pull request: #${prNumber}
- Head SHA: \`${pr.head.sha}\`
- Linked task: \`${facts.taskId || "unresolved"}\`
- Security-sensitive: \`${facts.securitySensitive}\`

| Severity | Rule | Finding |
|---|---|---|
${rows}

This job does not approve, comment, label, assign, close, or merge.
`;
if (process.env.GITHUB_STEP_SUMMARY) await appendFile(process.env.GITHUB_STEP_SUMMARY, summary);
console.log(JSON.stringify(report, null, 2));
37 changes: 37 additions & 0 deletions .github/workflows/issue-audit-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
name: V2 Issue Inventory Report

on:
workflow_dispatch:
schedule:
- cron: "17 4 * * 1"
pull_request:
paths:
- ".github/scripts/issue-audit.mjs"
- ".github/workflows/issue-audit-report.yml"

permissions:
contents: read
issues: read

jobs:
audit:
name: Issue inventory audit
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate auditor syntax
run: node --check .github/scripts/issue-audit.mjs
- name: Generate issue inventory report
env:
GITHUB_TOKEN: ${{ github.token }}
ISSUE_PREFIX: V2-BE
MAX_TASK: "150"
run: node .github/scripts/issue-audit.mjs
- uses: actions/upload-artifact@v4
with:
name: v2-issue-inventory-report
path: reports/
retention-days: 30
33 changes: 33 additions & 0 deletions .github/workflows/pr-guardian-report.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: PR Guardian Report

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

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

jobs:
guardian:
name: PR policy report
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
- name: Validate guardian syntax
run: node --check .github/scripts/pr-guardian.mjs
- name: Generate PR policy report
env:
GITHUB_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
ISSUE_PREFIX: V2-BE
run: node .github/scripts/pr-guardian.mjs
- uses: actions/upload-artifact@v4
with:
name: pr-guardian-report
path: reports/
retention-days: 30
Loading
Loading