pr-report-core 0.2.0 → 0.3.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 +5 -2
- package/dist/branch-rules.d.mts +30 -0
- package/dist/branch-rules.d.mts.map +1 -0
- package/dist/branch-rules.mjs +71 -0
- package/dist/branch-rules.mjs.map +1 -0
- package/dist/checks.d.mts +61 -62
- package/dist/checks.d.mts.map +1 -1
- package/dist/checks.mjs +0 -1
- package/dist/checks.mjs.map +1 -1
- package/dist/claim.d.mts +21 -15
- package/dist/claim.d.mts.map +1 -1
- package/dist/claim.mjs +1 -1
- package/dist/claim.mjs.map +1 -1
- package/dist/closing-issues.d.mts +18 -19
- package/dist/closing-issues.d.mts.map +1 -1
- package/dist/closing-issues.mjs +0 -1
- package/dist/closing-issues.mjs.map +1 -1
- package/dist/code-owners.d.mts +33 -34
- package/dist/code-owners.d.mts.map +1 -1
- package/dist/code-owners.mjs +0 -2
- package/dist/code-owners.mjs.map +1 -1
- package/dist/index.d.mts +3 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +3 -0
- package/dist/index.mjs.map +1 -1
- package/dist/issue-claim.d.mts +56 -0
- package/dist/issue-claim.d.mts.map +1 -0
- package/dist/issue-claim.mjs +87 -0
- package/dist/issue-claim.mjs.map +1 -0
- package/dist/issue-priority.d.mts +44 -0
- package/dist/issue-priority.d.mts.map +1 -0
- package/dist/issue-priority.mjs +91 -0
- package/dist/issue-priority.mjs.map +1 -0
- package/dist/labels.d.mts +43 -42
- package/dist/labels.d.mts.map +1 -1
- package/dist/labels.mjs +2 -1
- package/dist/labels.mjs.map +1 -1
- package/dist/merge-after.d.mts +22 -16
- package/dist/merge-after.d.mts.map +1 -1
- package/dist/merge-after.mjs +87 -24
- package/dist/merge-after.mjs.map +1 -1
- package/dist/queue-comments.d.mts +2 -2
- package/dist/queue-order.d.mts +32 -32
- package/dist/queue-order.mjs +0 -2
- package/dist/queue-order.mjs.map +1 -1
- package/dist/queue-owner.d.mts +14 -14
- package/dist/queue-owner.d.mts.map +1 -1
- package/dist/queue-owner.mjs +18 -0
- package/dist/queue-owner.mjs.map +1 -1
- package/dist/report.d.mts +4 -4
- package/dist/review-hold.d.mts +14 -14
- package/dist/ruleset.d.mts +12 -13
- package/dist/ruleset.d.mts.map +1 -1
- package/dist/ruleset.mjs +0 -1
- package/dist/ruleset.mjs.map +1 -1
- package/dist/set-aside.d.mts +29 -28
- package/dist/set-aside.d.mts.map +1 -1
- package/dist/set-aside.mjs +2 -0
- package/dist/set-aside.mjs.map +1 -1
- package/dist/stack.d.mts +14 -15
- package/dist/stack.d.mts.map +1 -1
- package/dist/stack.mjs +0 -1
- package/dist/stack.mjs.map +1 -1
- package/dist/start-after.d.mts +20 -21
- package/dist/start-after.d.mts.map +1 -1
- package/dist/start-after.mjs +0 -1
- package/dist/start-after.mjs.map +1 -1
- package/dist/summarize.d.mts +2 -2
- package/dist/timestamp.d.mts +10 -10
- package/dist/tree.d.mts +5 -5
- package/dist/types.d.mts +32 -33
- package/dist/types.d.mts.map +1 -1
- package/package.json +10 -10
- package/src/branch-rules.mts +105 -0
- package/src/branch-rules.test.mts +221 -0
- package/src/checks.mts +64 -65
- package/src/checks.test.mts +30 -31
- package/src/claim.mts +16 -16
- package/src/closing-issues.mts +18 -19
- package/src/closing-issues.test.mts +2 -2
- package/src/code-owners.mts +39 -41
- package/src/index.mts +3 -0
- package/src/issue-claim.mts +138 -0
- package/src/issue-claim.test.mts +149 -0
- package/src/issue-priority.mts +116 -0
- package/src/issue-priority.test.mts +71 -0
- package/src/labels.mts +43 -42
- package/src/merge-after.mts +142 -49
- package/src/merge-after.test.mts +80 -3
- package/src/queue-comments.mts +2 -2
- package/src/queue-order.mts +60 -62
- package/src/queue-order.test.mts +6 -6
- package/src/queue-owner.mts +46 -25
- package/src/queue-owner.test.mts +4 -2
- package/src/report.mts +9 -9
- package/src/review-hold.mts +14 -14
- package/src/review-hold.test.mts +3 -3
- package/src/ruleset.mts +12 -13
- package/src/set-aside.mts +30 -28
- package/src/set-aside.test.mts +22 -0
- package/src/stack.mts +14 -15
- package/src/stack.test.mts +4 -4
- package/src/start-after.mts +20 -21
- package/src/summarize.mts +2 -2
- package/src/timestamp.mts +14 -14
- package/src/tree.mts +5 -5
- package/src/types.mts +32 -33
package/dist/types.d.mts
CHANGED
|
@@ -12,22 +12,21 @@ export type RepoRef = Readonly<{
|
|
|
12
12
|
* What one context has reported on a head commit.
|
|
13
13
|
*
|
|
14
14
|
* `skipped` is its own state rather than a kind of `passed`. GitHub counts a
|
|
15
|
-
* skip as met, and so does the verdict — but once the two are one word a
|
|
16
|
-
*
|
|
17
|
-
*
|
|
15
|
+
* skip as met, and so does the verdict — but once the two are one word a reader
|
|
16
|
+
* cannot tell them apart, and the aggregate that skipped may be from a round
|
|
17
|
+
* that was superseded by one about to fail.
|
|
18
18
|
*/
|
|
19
19
|
export type ContextState = 'failed' | 'passed' | 'pending' | 'skipped';
|
|
20
20
|
export type ChecksSummary = Readonly<{
|
|
21
21
|
/**
|
|
22
|
-
* `paused` is `skip-ci`: the gated jobs boot no runner and
|
|
23
|
-
*
|
|
24
|
-
* "failing" describes it.
|
|
22
|
+
* `paused` is `skip-ci`: the gated jobs boot no runner and `no-skip-ci-label`
|
|
23
|
+
* sits `pending` by design, so neither "pending" nor "failing" describes it.
|
|
25
24
|
*/
|
|
26
25
|
verdict: 'failing' | 'passed' | 'paused' | 'pending';
|
|
27
26
|
/**
|
|
28
27
|
* Every required context by what it reported. These four,
|
|
29
|
-
* {@link ChecksSummary.missing} and {@link ChecksSummary.notRun} are
|
|
30
|
-
*
|
|
28
|
+
* {@link ChecksSummary.missing} and {@link ChecksSummary.notRun} are disjoint
|
|
29
|
+
* and cover the required list.
|
|
31
30
|
*/
|
|
32
31
|
passed: readonly string[];
|
|
33
32
|
failed: readonly string[];
|
|
@@ -41,9 +40,9 @@ export type ChecksSummary = Readonly<{
|
|
|
41
40
|
missing: readonly string[];
|
|
42
41
|
/**
|
|
43
42
|
* Required contexts with nothing reported, on a pull request whose base the
|
|
44
|
-
* ruleset does not cover — a stacked one. A workflow may run on pull
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
* ruleset does not cover — a stacked one. A workflow may run on pull requests
|
|
44
|
+
* into `main` only, and then its contexts never come here; nothing waits for
|
|
45
|
+
* them, since GitHub requires nothing of this base either.
|
|
47
46
|
*/
|
|
48
47
|
notRun: readonly string[];
|
|
49
48
|
/** How many contexts the verdict was reached over. */
|
|
@@ -82,28 +81,28 @@ export type PullRequestFacts = Readonly<{
|
|
|
82
81
|
labels: readonly Label[];
|
|
83
82
|
/**
|
|
84
83
|
* Whether auto-merge is armed. Separate from `merge-queued`, which is the
|
|
85
|
-
* author saying a pull request is to be landed: the label is the request
|
|
86
|
-
*
|
|
87
|
-
*
|
|
84
|
+
* author saying a pull request is to be landed: the label is the request and
|
|
85
|
+
* this is the mechanism, which `unblock-prs` arms when it picks the pull
|
|
86
|
+
* request.
|
|
88
87
|
*/
|
|
89
88
|
autoMerge: boolean;
|
|
90
89
|
headRef: string;
|
|
91
90
|
headSha: string;
|
|
92
91
|
baseRef: string;
|
|
93
92
|
/**
|
|
94
|
-
* Whether the head branch lives in a fork, which keeps it from being read
|
|
95
|
-
*
|
|
93
|
+
* Whether the head branch lives in a fork, which keeps it from being read as
|
|
94
|
+
* the parent of a stacked pull request. See `stack.mts`.
|
|
96
95
|
*/
|
|
97
96
|
fromFork: boolean;
|
|
98
97
|
url: string;
|
|
99
98
|
/**
|
|
100
|
-
* When anything about the pull request last changed — a label, a comment,
|
|
101
|
-
*
|
|
99
|
+
* When anything about the pull request last changed — a label, a comment, an
|
|
100
|
+
* edit — which is why it is not {@link PullRequestFacts.headCommittedAt}.
|
|
102
101
|
*/
|
|
103
102
|
updatedAt: string;
|
|
104
103
|
/**
|
|
105
|
-
* When the head commit was made, which is when the branch was last pushed
|
|
106
|
-
*
|
|
104
|
+
* When the head commit was made, which is when the branch was last pushed to
|
|
105
|
+
* as nearly as GitHub records it. Undefined when it could not be read.
|
|
107
106
|
*/
|
|
108
107
|
headCommittedAt: string | undefined;
|
|
109
108
|
/** Undefined when the comparison could not be read. */
|
|
@@ -111,8 +110,8 @@ export type PullRequestFacts = Readonly<{
|
|
|
111
110
|
/** What every context reported on the head commit, required or not. */
|
|
112
111
|
reported: ReadonlyMap<string, ContextState>;
|
|
113
112
|
/**
|
|
114
|
-
* Whether any check run on the head commit has not finished, required or
|
|
115
|
-
*
|
|
113
|
+
* Whether any check run on the head commit has not finished, required or not.
|
|
114
|
+
* See `anyRunInProgress` for why the required ones are not enough.
|
|
116
115
|
*/
|
|
117
116
|
checksRunning: boolean;
|
|
118
117
|
linkedIssues: readonly LinkedIssue[];
|
|
@@ -126,8 +125,8 @@ export type ReportEntry = PullRequestFacts & Readonly<{
|
|
|
126
125
|
*/
|
|
127
126
|
stackedOn: number | undefined;
|
|
128
127
|
/**
|
|
129
|
-
* The open pull requests it merges after because an issue it closes
|
|
130
|
-
*
|
|
128
|
+
* The open pull requests it merges after because an issue it closes starts
|
|
129
|
+
* after them (`start-after.mts`'s `inheritStartAfter`), less what
|
|
131
130
|
* `mergeAfter` and `stackedOn` already say.
|
|
132
131
|
*/
|
|
133
132
|
inheritedMergeAfter: readonly number[];
|
|
@@ -146,9 +145,9 @@ export type ReportEntry = PullRequestFacts & Readonly<{
|
|
|
146
145
|
}>;
|
|
147
146
|
/**
|
|
148
147
|
* One pull request in the merge order. `repeated` marks the second and later
|
|
149
|
-
* appearances of a pull request with more than one predecessor — declared,
|
|
150
|
-
*
|
|
151
|
-
*
|
|
148
|
+
* appearances of a pull request with more than one predecessor — declared, or
|
|
149
|
+
* the one it is stacked on: it is drawn under each of them, but expanded under
|
|
150
|
+
* the first only.
|
|
152
151
|
*/
|
|
153
152
|
export type TreeNode = Readonly<{
|
|
154
153
|
number: number;
|
|
@@ -158,8 +157,8 @@ export type TreeNode = Readonly<{
|
|
|
158
157
|
/**
|
|
159
158
|
* One pull request that has already landed, for the section that says what
|
|
160
159
|
* shipped. Nothing that is true of an open pull request — a merge order, a
|
|
161
|
-
* check verdict, a distance from its base — is true of one that has merged,
|
|
162
|
-
*
|
|
160
|
+
* check verdict, a distance from its base — is true of one that has merged, so
|
|
161
|
+
* it is its own shape rather than an entry with the fields blanked.
|
|
163
162
|
*/
|
|
164
163
|
export type MergedPullRequest = Readonly<{
|
|
165
164
|
number: number;
|
|
@@ -193,8 +192,8 @@ export type OpenIssue = Readonly<{
|
|
|
193
192
|
*/
|
|
194
193
|
startAfter: readonly number[];
|
|
195
194
|
/**
|
|
196
|
-
* What a `Merge-After:` in its body names. That line is not read on an
|
|
197
|
-
*
|
|
195
|
+
* What a `Merge-After:` in its body names. That line is not read on an issue,
|
|
196
|
+
* and is shown as a slip to fix: `Start-After:` is the issue's.
|
|
198
197
|
*/
|
|
199
198
|
misplacedMergeAfter: readonly number[];
|
|
200
199
|
}>;
|
|
@@ -215,8 +214,8 @@ export type IssueDeclarations = Readonly<{
|
|
|
215
214
|
/** Each issue's `Start-After:`; an issue not in it declares nothing. */
|
|
216
215
|
startAfter: ReadonlyMap<number, readonly number[]>;
|
|
217
216
|
/**
|
|
218
|
-
* The numbers known to be open issues, beyond the ones an open pull
|
|
219
|
-
*
|
|
217
|
+
* The numbers known to be open issues, beyond the ones an open pull request
|
|
218
|
+
* links as open. Decides only what is said not to be started.
|
|
220
219
|
*/
|
|
221
220
|
open: ReadonlySet<number>;
|
|
222
221
|
}>;
|
package/dist/types.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AAE1E;;;GAGG;AAEH,wCAAwC;AACxC,MAAM,MAAM,OAAO,GAAG,QAAQ,CAAC;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC;AAEhE;;;;;;;GAOG;AACH,MAAM,MAAM,YAAY,GAAG,QAAQ,GAAG,QAAQ,GAAG,SAAS,GAAG,SAAS,CAAC;AAEvE,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC
|
|
1
|
+
{"version":3,"file":"types.d.mts","sourceRoot":"","sources":["../src/types.mts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pr-report-core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "What a GitHub merge queue driven by labels and `Merge-After:` trailers decides about the open pull requests — verdicts, merge order, stacks, owners — with no I/O, shared by the GitHub Pull Requests Manager page and the unblock-prs worker",
|
|
6
6
|
"keywords": [
|
|
@@ -39,26 +39,26 @@
|
|
|
39
39
|
"LICENSE"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"ts-data-forge": "^14.
|
|
43
|
-
"ts-fortress": "^12.4.
|
|
42
|
+
"ts-data-forge": "^14.11.0",
|
|
43
|
+
"ts-fortress": "^12.4.4"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|
|
46
46
|
"@octokit/openapi-types": "^29.0.1",
|
|
47
47
|
"@types/node": "26.6.5",
|
|
48
|
-
"@vitest/coverage-
|
|
48
|
+
"@vitest/coverage-istanbul": "5.0.5",
|
|
49
49
|
"cspell": "10.4.0",
|
|
50
50
|
"dedent": "1.7.2",
|
|
51
51
|
"eslint": "10.13.0",
|
|
52
|
-
"eslint-config-typed": "7.
|
|
53
|
-
"eslint-plugin-ts-data-forge": "0.10.
|
|
54
|
-
"eslint-plugin-ts-fortress": "1.3.
|
|
55
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
52
|
+
"eslint-config-typed": "7.5.0",
|
|
53
|
+
"eslint-plugin-ts-data-forge": "0.10.10",
|
|
54
|
+
"eslint-plugin-ts-fortress": "1.3.14",
|
|
55
|
+
"eslint-plugin-ts-type-forge": "0.8.19",
|
|
56
56
|
"jiti": "2.7.0",
|
|
57
57
|
"npm-run-all2": "9.0.4",
|
|
58
58
|
"prettier": "3.9.10",
|
|
59
59
|
"prettier-plugin-organize-imports": "4.3.0",
|
|
60
60
|
"prettier-plugin-packagejson": "3.0.2",
|
|
61
|
-
"ts-repo-utils": "10.7.
|
|
61
|
+
"ts-repo-utils": "10.7.3",
|
|
62
62
|
"tsx": "4.23.15",
|
|
63
63
|
"typescript": "6.0.3",
|
|
64
64
|
"typescript-native": "npm:typescript@7.0.2",
|
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"pnpm": ">=9.0.0"
|
|
71
71
|
},
|
|
72
72
|
"volta": {
|
|
73
|
-
"node": "26.
|
|
73
|
+
"node": "26.13.0"
|
|
74
74
|
},
|
|
75
75
|
"publishConfig": {
|
|
76
76
|
"access": "public",
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What GitHub enforces on a branch, read the one way every reader reads it:
|
|
3
|
+
* every ruleset that applies to the branch
|
|
4
|
+
* (`GET /repos/{owner}/{repo}/rules/branches/{branch}`), and the contexts a
|
|
5
|
+
* classic branch protection rule requires
|
|
6
|
+
* (`GET /repos/{owner}/{repo}/branches/{branch}`). Neither wants more than
|
|
7
|
+
* reading the repository, where the endpoints that list a ruleset's own
|
|
8
|
+
* settings or a protection rule's whole want an admin.
|
|
9
|
+
*
|
|
10
|
+
* What a classic rule says about reviews is not among what the second answers,
|
|
11
|
+
* so a code owner's approval is waited for only where a ruleset asks for it.
|
|
12
|
+
*
|
|
13
|
+
* Nothing here talks to GitHub: the reader hands over its own way of asking.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Arr, Result } from 'ts-data-forge';
|
|
17
|
+
import * as t from 'ts-fortress';
|
|
18
|
+
import { requirementsOfRules, type RulesetRequirements } from './ruleset.mjs';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* One `GET`, by the reader's own transport. `path` is relative to
|
|
22
|
+
* `/repos/{owner}/{repo}` and may carry a query string; `what` names the
|
|
23
|
+
* request for a message.
|
|
24
|
+
*/
|
|
25
|
+
export type BranchRulesGet = <A>(
|
|
26
|
+
path: string,
|
|
27
|
+
schema: t.Type<A>,
|
|
28
|
+
what: string,
|
|
29
|
+
) => Promise<Result<A, string>>;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The requirements on `branch`, or why they could not be read. Either request
|
|
33
|
+
* failing fails it: a list of required contexts missing half of them would be
|
|
34
|
+
* read as the whole.
|
|
35
|
+
*/
|
|
36
|
+
export const readBranchRequirements = async (
|
|
37
|
+
branch: string,
|
|
38
|
+
get: BranchRulesGet,
|
|
39
|
+
): Promise<Result<RulesetRequirements, string>> => {
|
|
40
|
+
// One path segment, whatever the name holds: `release/1.x` included. The
|
|
41
|
+
// rule's premise, building a query string, is not what this is.
|
|
42
|
+
// eslint-disable-next-line no-restricted-globals
|
|
43
|
+
const segment = encodeURIComponent(branch);
|
|
44
|
+
|
|
45
|
+
const [rules, branchAnswer] = await Promise.all([
|
|
46
|
+
get(
|
|
47
|
+
`/rules/branches/${segment}?per_page=${RULES_PER_PAGE}`,
|
|
48
|
+
RulesSchema,
|
|
49
|
+
`the rules on ${branch}`,
|
|
50
|
+
),
|
|
51
|
+
get(`/branches/${segment}`, BranchSchema, `the protection of ${branch}`),
|
|
52
|
+
]);
|
|
53
|
+
|
|
54
|
+
if (Result.isErr(rules)) {
|
|
55
|
+
return rules;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (Result.isErr(branchAnswer)) {
|
|
59
|
+
return branchAnswer;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const fromRulesets = requirementsOfRules(rules.value);
|
|
63
|
+
|
|
64
|
+
const protection = branchAnswer.value.protection;
|
|
65
|
+
|
|
66
|
+
const classic =
|
|
67
|
+
protection?.enabled === false
|
|
68
|
+
? undefined
|
|
69
|
+
: protection?.required_status_checks;
|
|
70
|
+
|
|
71
|
+
return Result.ok({
|
|
72
|
+
...fromRulesets,
|
|
73
|
+
requiredContexts: Arr.uniq([
|
|
74
|
+
...fromRulesets.requiredContexts,
|
|
75
|
+
// GitHub's description leaves the level out of what it promises, so one
|
|
76
|
+
// not given is taken as enforcing.
|
|
77
|
+
...(classic === undefined || classic.enforcement_level === 'off'
|
|
78
|
+
? []
|
|
79
|
+
: classic.contexts),
|
|
80
|
+
]),
|
|
81
|
+
});
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* As many rules as a page holds; GitHub's default is 30. Each ruleset
|
|
86
|
+
* contributes a handful, so a branch with more than a hundred is not one a
|
|
87
|
+
* person keeps.
|
|
88
|
+
*/
|
|
89
|
+
const RULES_PER_PAGE = 100;
|
|
90
|
+
|
|
91
|
+
const RulesSchema = t.array(t.unknown());
|
|
92
|
+
|
|
93
|
+
const BranchSchema = t.record({
|
|
94
|
+
protection: t.optional(
|
|
95
|
+
t.record({
|
|
96
|
+
enabled: t.optional(t.boolean()),
|
|
97
|
+
required_status_checks: t.optional(
|
|
98
|
+
t.record({
|
|
99
|
+
enforcement_level: t.optional(t.string()),
|
|
100
|
+
contexts: t.array(t.string()),
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
}),
|
|
104
|
+
),
|
|
105
|
+
});
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { Result } from 'ts-data-forge';
|
|
2
|
+
import type * as t from 'ts-fortress';
|
|
3
|
+
import {
|
|
4
|
+
readBranchRequirements,
|
|
5
|
+
type BranchRulesGet,
|
|
6
|
+
} from './branch-rules.mjs';
|
|
7
|
+
|
|
8
|
+
describe(readBranchRequirements, () => {
|
|
9
|
+
test('adds up the rulesets on the branch and its classic protection', async () => {
|
|
10
|
+
const { get, asked } = answering({
|
|
11
|
+
rules: [
|
|
12
|
+
{
|
|
13
|
+
type: 'pull_request',
|
|
14
|
+
parameters: {
|
|
15
|
+
require_code_owner_review: true,
|
|
16
|
+
required_review_thread_resolution: false,
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'required_status_checks',
|
|
21
|
+
parameters: {
|
|
22
|
+
required_status_checks: [
|
|
23
|
+
{ context: 'code-check-result / result' },
|
|
24
|
+
{ context: 'lint' },
|
|
25
|
+
],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
{ type: 'deletion' },
|
|
29
|
+
],
|
|
30
|
+
branch: {
|
|
31
|
+
protection: {
|
|
32
|
+
enabled: true,
|
|
33
|
+
required_status_checks: {
|
|
34
|
+
enforcement_level: 'non_admins',
|
|
35
|
+
contexts: ['lint', 'build'],
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const read = await readBranchRequirements('main', get);
|
|
42
|
+
|
|
43
|
+
assert.isTrue(Result.isOk(read));
|
|
44
|
+
|
|
45
|
+
assert.deepStrictEqual(read.value, {
|
|
46
|
+
requiredContexts: ['code-check-result / result', 'lint', 'build'],
|
|
47
|
+
requireCodeOwnerReview: true,
|
|
48
|
+
requireConversationResolution: false,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
assert.deepStrictEqual(asked.toSorted(), [
|
|
52
|
+
'/branches/main',
|
|
53
|
+
'/rules/branches/main?per_page=100',
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('reads the contexts of a classic rule where no ruleset applies', async () => {
|
|
58
|
+
const { get } = answering({
|
|
59
|
+
rules: [],
|
|
60
|
+
branch: {
|
|
61
|
+
protection: {
|
|
62
|
+
enabled: true,
|
|
63
|
+
required_status_checks: {
|
|
64
|
+
enforcement_level: 'everyone',
|
|
65
|
+
contexts: ['build'],
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const read = await readBranchRequirements('main', get);
|
|
72
|
+
|
|
73
|
+
assert.isTrue(Result.isOk(read));
|
|
74
|
+
|
|
75
|
+
assert.deepStrictEqual(read.value.requiredContexts, ['build']);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test('requires nothing of a branch nothing protects', async () => {
|
|
79
|
+
const { get } = answering({ rules: [], branch: { protection: {} } });
|
|
80
|
+
|
|
81
|
+
const read = await readBranchRequirements('main', get);
|
|
82
|
+
|
|
83
|
+
assert.isTrue(Result.isOk(read));
|
|
84
|
+
|
|
85
|
+
assert.deepStrictEqual(read.value, {
|
|
86
|
+
requiredContexts: [],
|
|
87
|
+
requireCodeOwnerReview: false,
|
|
88
|
+
requireConversationResolution: false,
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('reads no contexts off classic protection that is switched off', async () => {
|
|
93
|
+
const { get } = answering({
|
|
94
|
+
rules: [],
|
|
95
|
+
branch: {
|
|
96
|
+
protection: {
|
|
97
|
+
enabled: true,
|
|
98
|
+
required_status_checks: {
|
|
99
|
+
enforcement_level: 'off',
|
|
100
|
+
contexts: ['stale'],
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const read = await readBranchRequirements('main', get);
|
|
107
|
+
|
|
108
|
+
assert.isTrue(Result.isOk(read));
|
|
109
|
+
|
|
110
|
+
assert.deepStrictEqual(read.value.requiredContexts, []);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('reads no contexts off classic protection that is disabled', async () => {
|
|
114
|
+
const { get } = answering({
|
|
115
|
+
rules: [],
|
|
116
|
+
branch: {
|
|
117
|
+
protection: {
|
|
118
|
+
enabled: false,
|
|
119
|
+
required_status_checks: { contexts: ['stale'] },
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const read = await readBranchRequirements('main', get);
|
|
125
|
+
|
|
126
|
+
assert.isTrue(Result.isOk(read));
|
|
127
|
+
|
|
128
|
+
assert.deepStrictEqual(read.value.requiredContexts, []);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test('puts the branch name in the path as one segment', async () => {
|
|
132
|
+
const { get, asked } = answering({ rules: [], branch: {} });
|
|
133
|
+
|
|
134
|
+
await readBranchRequirements('release/1.x', get);
|
|
135
|
+
|
|
136
|
+
assert.deepStrictEqual(asked.toSorted(), [
|
|
137
|
+
'/branches/release%2F1.x',
|
|
138
|
+
'/rules/branches/release%2F1.x?per_page=100',
|
|
139
|
+
]);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test.each(['/rules/', '/branches/main'] as const)(
|
|
143
|
+
'fails when %s fails',
|
|
144
|
+
async (failing) => {
|
|
145
|
+
const get: BranchRulesGet = (path, schema, what) =>
|
|
146
|
+
Promise.resolve(
|
|
147
|
+
path.startsWith(failing)
|
|
148
|
+
? Result.err(`refused ${what}`)
|
|
149
|
+
: validated(schema, path.startsWith('/rules/') ? [] : {}),
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const read = await readBranchRequirements('main', get);
|
|
153
|
+
|
|
154
|
+
assert.isTrue(Result.isErr(read));
|
|
155
|
+
|
|
156
|
+
assert.include(read.value, 'main');
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
test('fails on rules of another shape', async () => {
|
|
161
|
+
const { get } = answering({ rules: { rules: [] }, branch: {} });
|
|
162
|
+
|
|
163
|
+
const read = await readBranchRequirements('main', get);
|
|
164
|
+
|
|
165
|
+
assert.isTrue(Result.isErr(read));
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('fails on protection of another shape', async () => {
|
|
169
|
+
const { get } = answering({
|
|
170
|
+
rules: [],
|
|
171
|
+
branch: { protection: { required_status_checks: { contexts: 'build' } } },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
const read = await readBranchRequirements('main', get);
|
|
175
|
+
|
|
176
|
+
assert.isTrue(Result.isErr(read));
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('takes a classic rule that names no level as enforcing', async () => {
|
|
180
|
+
const { get } = answering({
|
|
181
|
+
rules: [],
|
|
182
|
+
branch: {
|
|
183
|
+
protection: { required_status_checks: { contexts: ['build'] } },
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const read = await readBranchRequirements('main', get);
|
|
188
|
+
|
|
189
|
+
assert.isTrue(Result.isOk(read));
|
|
190
|
+
|
|
191
|
+
assert.deepStrictEqual(read.value.requiredContexts, ['build']);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
const validated = <A,>(schema: t.Type<A>, body: unknown): Result<A, string> => {
|
|
196
|
+
const checked = schema.validate(body);
|
|
197
|
+
|
|
198
|
+
return Result.isErr(checked)
|
|
199
|
+
? Result.err('not the shape expected')
|
|
200
|
+
: Result.ok(checked.value);
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
/** A `get` answering the two paths with the bodies given. */
|
|
204
|
+
const answering = (
|
|
205
|
+
bodies: Readonly<{ rules: unknown; branch: unknown }>,
|
|
206
|
+
): Readonly<{ get: BranchRulesGet; asked: readonly string[] }> => {
|
|
207
|
+
const mut_asked: string[] = [];
|
|
208
|
+
|
|
209
|
+
const get: BranchRulesGet = (path, schema) => {
|
|
210
|
+
mut_asked.push(path);
|
|
211
|
+
|
|
212
|
+
return Promise.resolve(
|
|
213
|
+
validated(
|
|
214
|
+
schema,
|
|
215
|
+
path.startsWith('/rules/') ? bodies.rules : bodies.branch,
|
|
216
|
+
),
|
|
217
|
+
);
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
return { get, asked: mut_asked };
|
|
221
|
+
};
|