diff --git a/docs/contributor-guide.md b/docs/contributor-guide.md index 1fbab8250..de0b4ad71 100644 --- a/docs/contributor-guide.md +++ b/docs/contributor-guide.md @@ -34,6 +34,7 @@ The table below reflects **`continue-on-error`**, conditional `if:` steps, and j | **Frontend** | `ci.yml` → *Frontend Checks* | **Partially** | **Tests** failing fail the job. **Lint** (`lint:ci-scope`) and **build** use `continue-on-error: true` (advisory in CI). Prefer running full `npm run lint` and `npm run build` locally before pushing. | | **Contracts** | `ci.yml` → *Soroban Contract Checks* | **No** | Entire job sets `continue-on-error: true`. Formatting (`cargo fmt`) still runs without that flag on the step—treat contract hygiene as **required by policy** even when the job is lenient. | | **Docs / README** | `ci.yml` → *README Command Verification* | **No** | Job-level `continue-on-error: true`. | +| **Generated Files** | `ci.yml` → *Generated Files Guard* | **Yes** | Blocks if `issue.md` or `pr.md` are present in the branch diff. See [Generated issue scripts](#generated-issue-scripts) below. | | **Security (Rust)** | `security.yml` | **Mixed** | Jobs post **PR comments** (`cargo-audit`, security-focused Clippy, Soroban pattern scan). Explicit **fail-on-push** guards exist for some steps; PRs rely on visibility in comments rather than failing the audit job by default—still fix reported issues. | | **CodeQL** | `codeql.yml` | **If required** | Fails when analysis fails unless overridden. Typically treated as blocking when enabled for the repo. Hard to replicate fully offline. | | **Dependency Review** | `dependency-review.yml` | **Soft** | Runs only for PRs from the **same** repository (not forks). The review step uses `continue-on-error: true` so missing Dependency Graph support does not hard-fail CI. High-severity findings are still surfaced in the PR. | @@ -300,6 +301,45 @@ from the repository root. It checks that documented commands and doc links in `R --- +## Generated issue scripts + +The files `issue.md` and `pr.md` are **local operational artifacts** — auto-generated summaries that maintainers and contributors use to track issue scope and compose PR descriptions during active development. They must **never** be committed to the repository. + +### Guard + +CI runs `node scripts/check-generated-files.js` from the repository root as part of `ci.yml` → *Generated Files Guard*. It compares the branch diff against `origin/main` and fails if either filename appears in any changed path. This job is **blocking**: a red guard prevents merging. + +### Where local maintainer scripts live + +Repository-local scripts that are safe to commit belong under `scripts/`. The following are already committed and CI-safe: + +| Script | Purpose | +|--------|---------| +| `scripts/issue-triage.js` | Maintainer issue triage summary via GitHub Search API | +| `scripts/verify-readme-commands.js` | Verifies README commands match package.json scripts | +| `scripts/validate-workspace.js` | Runs env var consistency checks | +| `scripts/smoke-test.sh` / `scripts/smoke-test.js` | Backend / frontend endpoint smoke tests | +| `scripts/setup-doctor.js` | Toolchain and workspace setup checker | +| `scripts/maintainer_saved_searches.sh` | Maintainer triage shortcut links | +| `scripts/check-frontend-env.js` | CI guardrail for unsafe VITE_ secrets | +| `scripts/check-env-vars.js` / `scripts/check-env-drift.ts` | Environment variable consistency checkers | +| `scripts/check-generated-files.js` | CI guardrail for accidental `issue.md` / `pr.md` commits | + +Generated helpers like `issue.md` and `pr.md` that summarize issue scope or compose PR bodies are **not** under `scripts/` because they are ephemeral, author-specific, and should never be reviewed or merged. Keep them in the repository root (where `.gitignore` already excludes them) or in a temporary working directory outside the repo. + +### Recovering from an accidental commit + +If `issue.md` or `pr.md` end up in a branch despite the `.gitignore` entry (e.g. via `git add --force`), remove them from tracking: + +```bash +git rm --cached issue.md pr.md +git commit -m "chore: remove accidentally committed generated files" +``` + +Then rebase or create a new PR branch from a clean point on `main`. + +--- + ## Running GitHub Actions locally The maintainers do not commit a pinned `act` configuration. Two practical options: diff --git a/scripts/check-generated-files.js b/scripts/check-generated-files.js new file mode 100755 index 000000000..4a6f218bb --- /dev/null +++ b/scripts/check-generated-files.js @@ -0,0 +1,61 @@ +#!/usr/bin/env node +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); + +const FORBIDDEN_PATTERNS = [ + { pattern: /^issue\.md$/i, label: "issue.md" }, + { pattern: /^pr\.md$/i, label: "pr.md" }, +]; + +const REPO_ROOT = path.resolve(__dirname, ".."); + +function getChangedFiles() { + try { + const mergeBase = execSync( + "git merge-base HEAD origin/main 2>/dev/null || echo HEAD", + { encoding: "utf8", cwd: REPO_ROOT } + ).trim(); + + const output = execSync( + `git diff --name-only ${mergeBase}..HEAD 2>/dev/null`, + { encoding: "utf8", cwd: REPO_ROOT } + ).trim(); + + return output ? output.split("\n") : []; + } catch { + return []; + } +} + +let violations = []; + +const changedFiles = getChangedFiles(); + +for (const filePath of changedFiles) { + const fileName = path.basename(filePath); + for (const entry of FORBIDDEN_PATTERNS) { + if (entry.pattern.test(fileName)) { + violations.push({ + file: filePath, + label: entry.label, + }); + } + } +} + +if (violations.length > 0) { + console.error("\n\u274c Forbidden generated files detected in this branch:\n"); + for (const v of violations) { + console.error(` ${v.file}`); + } + console.error( + "\nGenerated issue/PR scripts (issue.md, pr.md) must not be committed.\n" + + "These files are auto-generated operational artifacts meant for local use only.\n" + + "Remove them from the branch before merging.\n" + ); + process.exit(1); +} else { + console.log("\u2705 No forbidden generated files detected in this branch."); + process.exit(0); +}