@rmartz/pr-policy 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # @rmartz/pr-policy
2
+
3
+ **Is this pull request's content acceptable under policy?** `pr-policy` answers
4
+ that on every PR, statically, from the PR's diff, title, and changed paths. It
5
+ doesn't rely on a reviewer remembering to check.
6
+
7
+ Each policy rule is a small read-only classifier. All of them report into **one**
8
+ `pr-policy` check-run, which the consumer's ruleset requires. A fixable problem
9
+ (a bad title) turns it red; a PR waiting on a human sign-off shows it as
10
+ pending, not failing. Either holds the merge. Adding a rule later never adds a
11
+ new required-status name.
12
+
13
+ ## Checks
14
+
15
+ | Check | Blocks when |
16
+ | --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
17
+ | **CI-change classification** ([docs](docs/checks/ci-change.md)) | A `.github/workflows/**` change loosens CI (a removed job, `continue-on-error`, a narrowed trigger, …) and no human has applied `CI change approved` (shows as pending). Owns `CI approval needed`. |
18
+ | **Title-type rules** ([docs](docs/checks/title.md)) | The squash title isn't a valid Conventional Commit, puts `!` on a non-functional type, or types a workflow change as anything but `ci` without the `breaking change` label. |
19
+ | Domain labels (backlog) | Never blocks. Additive path-glob → domain-label map. |
20
+ | Milestone inheritance (backlog) | Never blocks. Issue → PR milestone. |
21
+
22
+ The design, and why this is separate from `merge-safety` and `pr-lifecycle`, is
23
+ in [docs/decisions.md](docs/decisions.md).
24
+
25
+ ## Using it
26
+
27
+ Add the [`rmartz/pr-policy-action`](https://github.com/rmartz/pr-policy-action)
28
+ caller workflow and require the `pr-policy` status on your default branch. Setup
29
+ is in its [consumer guide](https://github.com/rmartz/pr-policy-action/blob/main/docs/consuming.md).
30
+
31
+ ## Library and CLI
32
+
33
+ Every check is importable. The `ai-pr-policy` CLI is a thin wrapper around them:
34
+
35
+ ```bash
36
+ ai-pr-policy evaluate --pr 12 --repo rmartz/pr-policy # post the check-run + labels
37
+ ai-pr-policy evaluate --pr 12 --repo rmartz/pr-policy --json # evaluate only, change nothing
38
+ ai-pr-policy evaluate --facts pr.json # offline, from a JSON facts file
39
+ ```
40
+
41
+ ## Documentation
42
+
43
+ - [What pr-policy is](docs/overview.md)
44
+ - [Policy checks](docs/checks/index.md)
45
+ - [The check-run and label contract](docs/check-run-contract.md)
46
+ - [Adding a policy check](docs/adding-a-check.md)
47
+ - [How pr-policy reaches consuming repos](docs/distribution.md)
48
+ - [Design decisions](docs/decisions.md)
49
+
50
+ ---
51
+
52
+ 🤖 Created by Claude Opus 5.5
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ applyLabelEdits,
4
+ evaluatePolicy,
5
+ gatherFacts,
6
+ parseFacts,
7
+ postCheckRun,
8
+ resolveRepoTarget
9
+ } from "../chunk-QINHHFJN.js";
10
+
11
+ // src/bin/pr-policy.ts
12
+ import { readFile } from "fs/promises";
13
+ import { parseArgs } from "util";
14
+ var USAGE = `Usage:
15
+ ai-pr-policy evaluate --pr <n> [--repo <owner/repo>] [--json]
16
+ ai-pr-policy evaluate --facts <path|->
17
+
18
+ --pr Evaluate a live PR, post the pr-policy check-run on its head, and
19
+ apply the label edits the checks planned. --json prints the
20
+ evaluation instead and changes nothing.
21
+ --facts Evaluate an offline JSON facts document ("-" reads stdin) and print
22
+ the evaluation. Exits 1 only on "failure" ("pending" exits 0).`;
23
+ async function readInput(path) {
24
+ if (path !== "-") return readFile(path, "utf8");
25
+ const chunks = [];
26
+ for await (const chunk of process.stdin) chunks.push(Buffer.from(chunk));
27
+ return Buffer.concat(chunks).toString("utf8");
28
+ }
29
+ async function main(argv) {
30
+ const { values, positionals } = parseArgs({
31
+ args: [...argv],
32
+ allowPositionals: true,
33
+ options: {
34
+ facts: { type: "string" },
35
+ pr: { type: "string" },
36
+ repo: { type: "string" },
37
+ json: { type: "boolean" },
38
+ help: { type: "boolean", short: "h" }
39
+ }
40
+ });
41
+ if (values.help) {
42
+ console.log(USAGE);
43
+ return 0;
44
+ }
45
+ if (positionals[0] !== "evaluate") {
46
+ console.error(USAGE);
47
+ return 2;
48
+ }
49
+ if (values.facts !== void 0) {
50
+ const evaluation2 = await evaluatePolicy(parseFacts(await readInput(values.facts)));
51
+ console.log(JSON.stringify(evaluation2, null, 2));
52
+ return evaluation2.outcome === "failure" ? 1 : 0;
53
+ }
54
+ const pr = Number(values.pr);
55
+ if (!Number.isInteger(pr) || pr <= 0) {
56
+ console.error(USAGE);
57
+ return 2;
58
+ }
59
+ const repo = await resolveRepoTarget({ repo: values.repo });
60
+ if (!repo) {
61
+ console.error("Could not resolve owner/repo \u2014 pass --repo <owner/repo>.");
62
+ return 2;
63
+ }
64
+ const target = { repo, pr };
65
+ const { facts, headSha } = await gatherFacts(target);
66
+ const evaluation = await evaluatePolicy(facts);
67
+ if (values.json) {
68
+ console.log(JSON.stringify(evaluation, null, 2));
69
+ return 0;
70
+ }
71
+ await applyLabelEdits(target, evaluation);
72
+ await postCheckRun(target, headSha, evaluation);
73
+ console.log(`${repo}#${pr}: ${evaluation.outcome} \u2014 ${evaluation.title}`);
74
+ return 0;
75
+ }
76
+ try {
77
+ process.exitCode = await main(process.argv.slice(2));
78
+ } catch (error) {
79
+ console.error(error instanceof Error ? error.message : error);
80
+ process.exitCode = 2;
81
+ }