repokeeper 0.2.3 → 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 +30 -1
- package/dist/cli.js +21 -0
- package/dist/commands/github.js +36 -0
- package/dist/config/load.js +1 -0
- package/dist/config/schema.js +35 -1
- package/dist/config/types.js +1 -0
- package/dist/errors.js +6 -0
- package/dist/github/api.js +49 -0
- package/dist/github/settings.js +184 -0
- package/dist/platforms/github.js +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -56,10 +56,12 @@ adds; jobs you add yourself are left alone, and so is the formatting of the rest
|
|
|
56
56
|
|
|
57
57
|
The script stack runs ShellCheck and `shfmt -d` on `*.sh` (format with `shfmt -w` before pushing)
|
|
58
58
|
and PSScriptAnalyzer on `*.ps1`, which fails on errors and warnings (not on information-level rules). To
|
|
59
|
-
choose the rules yourself, add a `PSScriptAnalyzerSettings.psd1` at the repository root; the job then
|
|
59
|
+
choose the rules yourself, add a `PSScriptAnalyzerSettings.psd1` at the repository root; the job then
|
|
60
|
+
uses it instead of its own filter, so keep `Severity` in it unless you want information-level rules too:
|
|
60
61
|
|
|
61
62
|
```powershell
|
|
62
63
|
@{
|
|
64
|
+
Severity = @('Error', 'Warning')
|
|
63
65
|
# installers print for the person running them
|
|
64
66
|
ExcludeRules = @('PSAvoidUsingWriteHost')
|
|
65
67
|
}
|
|
@@ -75,6 +77,33 @@ work:
|
|
|
75
77
|
and issues write access). Without it the release pull request is opened with `GITHUB_TOKEN`, and
|
|
76
78
|
GitHub does not run CI on pull requests opened that way.
|
|
77
79
|
|
|
80
|
+
Set `modules.drift: true` to add a `repokeeper` job to `ci.yml` that runs `repokeeper check` with
|
|
81
|
+
the version that wrote the standard, so a pull request that edits a managed file fails until the edit
|
|
82
|
+
is resolved.
|
|
83
|
+
|
|
84
|
+
## GitHub settings
|
|
85
|
+
|
|
86
|
+
`repokeeper github apply` brings the repository's settings in line with the `github:` section of
|
|
87
|
+
`.repokeeper.yml`. Only the keys you write are managed; anything left out stays as it is.
|
|
88
|
+
|
|
89
|
+
```yaml
|
|
90
|
+
github:
|
|
91
|
+
description: Keeps repositories on one standard
|
|
92
|
+
topics: [cli, conventional-commits]
|
|
93
|
+
merge: { squash: true, merge_commit: false, rebase: false, delete_branch_on_merge: true }
|
|
94
|
+
security: { dependabot_alerts: true, dependabot_security_updates: true }
|
|
95
|
+
protect: # a ruleset named "repokeeper" on the default branch; false removes it
|
|
96
|
+
require_pull_request: true
|
|
97
|
+
required_approvals: 0
|
|
98
|
+
required_checks: ["commits / commitlint"] # check names exactly as pull requests show them
|
|
99
|
+
allow_force_push: false
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
It prints every change first and applies them with `--yes`, or after you confirm in a terminal;
|
|
103
|
+
`--dry-run` only prints. The token comes from `GITHUB_TOKEN` or `gh auth token` and needs admin
|
|
104
|
+
access to the repository. Legacy branch protection, visibility, secrets and collaborators are never
|
|
105
|
+
touched.
|
|
106
|
+
|
|
78
107
|
## License
|
|
79
108
|
|
|
80
109
|
MIT
|
package/dist/cli.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { realpathSync } from "node:fs";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
3
4
|
import { fileURLToPath } from "node:url";
|
|
4
5
|
import { parseArgs } from "node:util";
|
|
5
6
|
import { checkCommand } from "./commands/check.js";
|
|
7
|
+
import { githubApplyCommand } from "./commands/github.js";
|
|
6
8
|
import { initCommand } from "./commands/init.js";
|
|
7
9
|
import { updateCommand } from "./commands/update.js";
|
|
8
10
|
import { STACK_IDS } from "./config/types.js";
|
|
@@ -15,6 +17,7 @@ export const USAGE = [
|
|
|
15
17
|
" init detect stacks, write .repokeeper.yml and apply the standard",
|
|
16
18
|
" check report drift from the standard without writing (exit 1 on drift)",
|
|
17
19
|
" update move to the standard of this repokeeper version and resync",
|
|
20
|
+
" github apply diff the GitHub settings against .repokeeper.yml and apply them",
|
|
18
21
|
"",
|
|
19
22
|
"options:",
|
|
20
23
|
" --dry-run show what would change without writing",
|
|
@@ -24,6 +27,7 @@ export const USAGE = [
|
|
|
24
27
|
" --stack <id> stack to use instead of detection (init, repeatable)",
|
|
25
28
|
" --relock rebuild .repokeeper/lock.json from the current files (init)",
|
|
26
29
|
" --json machine-readable output (check)",
|
|
30
|
+
" -y, --yes apply without asking (github apply)",
|
|
27
31
|
" -v, --version print the version",
|
|
28
32
|
].join("\n");
|
|
29
33
|
const toPosix = (path) => path.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
@@ -42,6 +46,7 @@ export async function run(argv, io) {
|
|
|
42
46
|
stack: { type: "string", multiple: true, default: [] },
|
|
43
47
|
relock: { type: "boolean", default: false },
|
|
44
48
|
json: { type: "boolean", default: false },
|
|
49
|
+
yes: { type: "boolean", short: "y", default: false },
|
|
45
50
|
version: { type: "boolean", short: "v", default: false },
|
|
46
51
|
help: { type: "boolean", short: "h", default: false },
|
|
47
52
|
},
|
|
@@ -70,6 +75,7 @@ export async function run(argv, io) {
|
|
|
70
75
|
stacks: stacks,
|
|
71
76
|
relock: values.relock,
|
|
72
77
|
json: values.json,
|
|
78
|
+
yes: values.yes,
|
|
73
79
|
};
|
|
74
80
|
if (command === "init")
|
|
75
81
|
return await initCommand(io.cwd, options, io);
|
|
@@ -77,6 +83,11 @@ export async function run(argv, io) {
|
|
|
77
83
|
return await checkCommand(io.cwd, options, io);
|
|
78
84
|
if (command === "update")
|
|
79
85
|
return await updateCommand(io.cwd, options, io);
|
|
86
|
+
if (command === "github") {
|
|
87
|
+
if (positionals[1] !== "apply")
|
|
88
|
+
throw new UsageError("usage: repokeeper github apply [--dry-run] [--yes]");
|
|
89
|
+
return await githubApplyCommand(io.cwd, options, io);
|
|
90
|
+
}
|
|
80
91
|
throw new UsageError(`unknown command: ${command}`);
|
|
81
92
|
}
|
|
82
93
|
catch (error) {
|
|
@@ -94,12 +105,22 @@ export async function run(argv, io) {
|
|
|
94
105
|
throw error;
|
|
95
106
|
}
|
|
96
107
|
}
|
|
108
|
+
async function askYesNo(question) {
|
|
109
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
110
|
+
try {
|
|
111
|
+
return /^y(es)?$/i.test((await rl.question(`${question} [y/N] `)).trim());
|
|
112
|
+
}
|
|
113
|
+
finally {
|
|
114
|
+
rl.close();
|
|
115
|
+
}
|
|
116
|
+
}
|
|
97
117
|
const invokedPath = process.argv[1];
|
|
98
118
|
if (invokedPath !== undefined && realpathSync(invokedPath) === fileURLToPath(import.meta.url)) {
|
|
99
119
|
run(process.argv.slice(2), {
|
|
100
120
|
cwd: process.cwd(),
|
|
101
121
|
out: (line) => process.stdout.write(`${line}\n`),
|
|
102
122
|
err: (line) => process.stderr.write(`${line}\n`),
|
|
123
|
+
...(process.stdin.isTTY ? { confirm: askYesNo } : {}),
|
|
103
124
|
}).then((code) => {
|
|
104
125
|
process.exitCode = code;
|
|
105
126
|
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { loadConfig } from "../config/load.js";
|
|
2
|
+
import { UsageError } from "../errors.js";
|
|
3
|
+
import { repoInfo } from "../git.js";
|
|
4
|
+
import { resolveToken, restApi } from "../github/api.js";
|
|
5
|
+
import { planGithub } from "../github/settings.js";
|
|
6
|
+
/** `repokeeper github apply`: diff the repository's GitHub settings against `github:` and apply on confirmation. */
|
|
7
|
+
export async function githubApplyCommand(root, options, io) {
|
|
8
|
+
const config = await loadConfig(root);
|
|
9
|
+
const repo = await repoInfo(root);
|
|
10
|
+
if (!repo.owner)
|
|
11
|
+
throw new UsageError("the origin remote is not a GitHub repository");
|
|
12
|
+
const slug = `${repo.owner}/${repo.name}`;
|
|
13
|
+
const api = io.githubApi ?? restApi(await resolveToken());
|
|
14
|
+
const changes = await planGithub(api, repo.owner, repo.name, config.github ?? {});
|
|
15
|
+
if (changes.length === 0) {
|
|
16
|
+
io.out(`GitHub settings of ${slug} match .repokeeper.yml`);
|
|
17
|
+
return 0;
|
|
18
|
+
}
|
|
19
|
+
for (const change of changes)
|
|
20
|
+
io.out(`change ${change.setting}: ${change.from} -> ${change.to}`);
|
|
21
|
+
if (options.dryRun) {
|
|
22
|
+
io.out("dry run: nothing applied");
|
|
23
|
+
return 0;
|
|
24
|
+
}
|
|
25
|
+
const question = `apply ${changes.length} change(s) to ${slug}?`;
|
|
26
|
+
const confirmed = options.yes || (io.confirm ? await io.confirm(question) : false);
|
|
27
|
+
if (!confirmed) {
|
|
28
|
+
io.out("nothing applied; pass --yes to apply");
|
|
29
|
+
return 1;
|
|
30
|
+
}
|
|
31
|
+
for (const change of changes) {
|
|
32
|
+
await change.apply(api);
|
|
33
|
+
io.out(`applied ${change.setting}`);
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
package/dist/config/load.js
CHANGED
package/dist/config/schema.js
CHANGED
|
@@ -20,6 +20,7 @@ export const configSchema = {
|
|
|
20
20
|
release: flag,
|
|
21
21
|
deps: flag,
|
|
22
22
|
gitignore: flag,
|
|
23
|
+
drift: flag,
|
|
23
24
|
health: {
|
|
24
25
|
anyOf: [
|
|
25
26
|
{ const: false },
|
|
@@ -40,6 +41,39 @@ export const configSchema = {
|
|
|
40
41
|
},
|
|
41
42
|
owned: { type: "array", items: { type: "string", minLength: 1 } },
|
|
42
43
|
stack_options: { type: "object" },
|
|
43
|
-
github: {
|
|
44
|
+
github: {
|
|
45
|
+
type: "object",
|
|
46
|
+
additionalProperties: false,
|
|
47
|
+
properties: {
|
|
48
|
+
default_branch: { type: "string", minLength: 1 },
|
|
49
|
+
description: { type: "string" },
|
|
50
|
+
topics: { type: "array", uniqueItems: true, items: { type: "string", pattern: "^[a-z0-9][a-z0-9-]{0,49}$" } },
|
|
51
|
+
merge: {
|
|
52
|
+
type: "object",
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
properties: { squash: flag, merge_commit: flag, rebase: flag, delete_branch_on_merge: flag },
|
|
55
|
+
},
|
|
56
|
+
security: {
|
|
57
|
+
type: "object",
|
|
58
|
+
additionalProperties: false,
|
|
59
|
+
properties: { dependabot_alerts: flag, dependabot_security_updates: flag },
|
|
60
|
+
},
|
|
61
|
+
protect: {
|
|
62
|
+
anyOf: [
|
|
63
|
+
{ const: false },
|
|
64
|
+
{
|
|
65
|
+
type: "object",
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
properties: {
|
|
68
|
+
require_pull_request: flag,
|
|
69
|
+
required_approvals: { type: "integer", minimum: 0, maximum: 10 },
|
|
70
|
+
required_checks: { type: "array", uniqueItems: true, items: { type: "string", minLength: 1 } },
|
|
71
|
+
allow_force_push: flag,
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
},
|
|
44
78
|
},
|
|
45
79
|
};
|
package/dist/config/types.js
CHANGED
package/dist/errors.js
CHANGED
|
@@ -18,6 +18,12 @@ export class UsageError extends RepokeeperError {
|
|
|
18
18
|
super(message, 2);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
+
/** A GitHub API call failed; nothing on disk was touched. */
|
|
22
|
+
export class ApiError extends RepokeeperError {
|
|
23
|
+
constructor(message) {
|
|
24
|
+
super(message, 3);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
21
27
|
/** `.repokeeper/lock.json` is unreadable; the repository state is unknown, which counts as drift. */
|
|
22
28
|
export class LockError extends RepokeeperError {
|
|
23
29
|
constructor(message) {
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
3
|
+
import { ApiError } from "../errors.js";
|
|
4
|
+
const execFileAsync = promisify(execFile);
|
|
5
|
+
/** `GITHUB_TOKEN`, or the token of the GitHub CLI. */
|
|
6
|
+
export async function resolveToken(env = process.env) {
|
|
7
|
+
if (env.GITHUB_TOKEN)
|
|
8
|
+
return env.GITHUB_TOKEN;
|
|
9
|
+
try {
|
|
10
|
+
const { stdout } = await execFileAsync("gh", ["auth", "token"], { encoding: "utf8" });
|
|
11
|
+
if (stdout.trim())
|
|
12
|
+
return stdout.trim();
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
// fall through to the error below
|
|
16
|
+
}
|
|
17
|
+
throw new ApiError("no GitHub token: set GITHUB_TOKEN or sign in with `gh auth login`");
|
|
18
|
+
}
|
|
19
|
+
export function restApi(token, base = "https://api.github.com") {
|
|
20
|
+
return {
|
|
21
|
+
async request(method, path, body) {
|
|
22
|
+
let response;
|
|
23
|
+
try {
|
|
24
|
+
response = await fetch(`${base}${path}`, {
|
|
25
|
+
method,
|
|
26
|
+
headers: {
|
|
27
|
+
accept: "application/vnd.github+json",
|
|
28
|
+
authorization: `Bearer ${token}`,
|
|
29
|
+
"x-github-api-version": "2022-11-28",
|
|
30
|
+
...(body === undefined ? {} : { "content-type": "application/json" }),
|
|
31
|
+
},
|
|
32
|
+
body: body === undefined ? undefined : JSON.stringify(body),
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new ApiError(`GitHub API ${method} ${path} failed: ${error.message}`);
|
|
37
|
+
}
|
|
38
|
+
const text = await response.text();
|
|
39
|
+
let data = null;
|
|
40
|
+
try {
|
|
41
|
+
data = text ? JSON.parse(text) : null;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
data = text;
|
|
45
|
+
}
|
|
46
|
+
return { status: response.status, data };
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { ApiError } from "../errors.js";
|
|
2
|
+
export const RULESET_NAME = "repokeeper";
|
|
3
|
+
function expect2xx(response, what, repo) {
|
|
4
|
+
if (response.status < 400)
|
|
5
|
+
return response;
|
|
6
|
+
const message = response.data?.message ?? `HTTP ${response.status}`;
|
|
7
|
+
const access = response.status === 403 || response.status === 404
|
|
8
|
+
? `; this needs admin access to ${repo} (a token with the repo scope, or Administration: write)`
|
|
9
|
+
: "";
|
|
10
|
+
throw new ApiError(`${what} failed: ${message}${access}`);
|
|
11
|
+
}
|
|
12
|
+
const show = (value) => value === undefined || value === null || value === ""
|
|
13
|
+
? "(none)"
|
|
14
|
+
: Array.isArray(value)
|
|
15
|
+
? value.join(", ") || "(none)"
|
|
16
|
+
: String(value);
|
|
17
|
+
const MERGE_FIELDS = {
|
|
18
|
+
squash: "allow_squash_merge",
|
|
19
|
+
merge_commit: "allow_merge_commit",
|
|
20
|
+
rebase: "allow_rebase_merge",
|
|
21
|
+
delete_branch_on_merge: "delete_branch_on_merge",
|
|
22
|
+
};
|
|
23
|
+
function summarize(rules) {
|
|
24
|
+
const pr = rules.find((r) => r.type === "pull_request");
|
|
25
|
+
const checks = rules.find((r) => r.type === "required_status_checks");
|
|
26
|
+
return {
|
|
27
|
+
pullRequest: pr !== undefined,
|
|
28
|
+
approvals: pr?.parameters?.required_approving_review_count ?? 0,
|
|
29
|
+
checks: (checks?.parameters?.required_status_checks ?? []).map((c) => c.context).sort(),
|
|
30
|
+
forcePush: !rules.some((r) => r.type === "non_fast_forward"),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
function describeRules(summary) {
|
|
34
|
+
if (summary === null)
|
|
35
|
+
return "(none)";
|
|
36
|
+
const parts = [
|
|
37
|
+
summary.pullRequest ? `pull request required (${summary.approvals} approvals)` : "no pull request required",
|
|
38
|
+
summary.checks.length > 0 ? `checks: ${summary.checks.join(", ")}` : "no required checks",
|
|
39
|
+
summary.forcePush ? "force push allowed" : "no force push",
|
|
40
|
+
];
|
|
41
|
+
return parts.join("; ");
|
|
42
|
+
}
|
|
43
|
+
function desiredRules(protect) {
|
|
44
|
+
const rules = [];
|
|
45
|
+
if (protect.require_pull_request ?? true) {
|
|
46
|
+
rules.push({
|
|
47
|
+
type: "pull_request",
|
|
48
|
+
parameters: {
|
|
49
|
+
required_approving_review_count: protect.required_approvals ?? 0,
|
|
50
|
+
dismiss_stale_reviews_on_push: false,
|
|
51
|
+
require_code_owner_review: false,
|
|
52
|
+
require_last_push_approval: false,
|
|
53
|
+
required_review_thread_resolution: false,
|
|
54
|
+
},
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
const checks = protect.required_checks ?? [];
|
|
58
|
+
if (checks.length > 0) {
|
|
59
|
+
rules.push({
|
|
60
|
+
type: "required_status_checks",
|
|
61
|
+
parameters: {
|
|
62
|
+
strict_required_status_checks_policy: false,
|
|
63
|
+
required_status_checks: checks.map((context) => ({ context })),
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
if (!(protect.allow_force_push ?? false))
|
|
68
|
+
rules.push({ type: "non_fast_forward" });
|
|
69
|
+
return rules;
|
|
70
|
+
}
|
|
71
|
+
const sameSummary = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
72
|
+
/** Reads the repository's current settings and lists what differs from `settings`. Reads only. */
|
|
73
|
+
export async function planGithub(api, owner, name, settings) {
|
|
74
|
+
const repo = `${owner}/${name}`;
|
|
75
|
+
const base = `/repos/${owner}/${name}`;
|
|
76
|
+
const get = async (path, what, missingOk = false) => {
|
|
77
|
+
const response = await api.request("GET", `${base}${path}`);
|
|
78
|
+
return missingOk && response.status === 404 ? response : expect2xx(response, what, repo);
|
|
79
|
+
};
|
|
80
|
+
const send = async (method, path, what, body) => {
|
|
81
|
+
expect2xx(await api.request(method, `${base}${path}`, body), what, repo);
|
|
82
|
+
};
|
|
83
|
+
const changes = [];
|
|
84
|
+
const current = (await get("", `reading ${repo}`)).data;
|
|
85
|
+
for (const [key, field] of Object.entries(MERGE_FIELDS)) {
|
|
86
|
+
const want = settings.merge?.[key];
|
|
87
|
+
if (want === undefined || current[field] === want)
|
|
88
|
+
continue;
|
|
89
|
+
changes.push({
|
|
90
|
+
setting: `merge.${key}`,
|
|
91
|
+
from: show(current[field]),
|
|
92
|
+
to: show(want),
|
|
93
|
+
apply: () => send("PATCH", "", `setting merge.${key}`, { [field]: want }),
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (settings.description !== undefined && (current.description ?? "") !== settings.description) {
|
|
97
|
+
const description = settings.description;
|
|
98
|
+
changes.push({
|
|
99
|
+
setting: "description",
|
|
100
|
+
from: show(current.description),
|
|
101
|
+
to: show(description),
|
|
102
|
+
apply: () => send("PATCH", "", "setting the description", { description }),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (settings.topics !== undefined) {
|
|
106
|
+
const have = ((await get("/topics", "reading topics")).data.names ?? []).slice().sort();
|
|
107
|
+
const want = [...settings.topics].sort();
|
|
108
|
+
if (JSON.stringify(have) !== JSON.stringify(want)) {
|
|
109
|
+
changes.push({
|
|
110
|
+
setting: "topics",
|
|
111
|
+
from: show(have),
|
|
112
|
+
to: show(want),
|
|
113
|
+
apply: () => send("PUT", "/topics", "setting topics", { names: want }),
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const alerts = settings.security?.dependabot_alerts;
|
|
118
|
+
if (alerts !== undefined) {
|
|
119
|
+
const have = (await get("/vulnerability-alerts", "reading Dependabot alerts", true)).status === 204;
|
|
120
|
+
if (have !== alerts) {
|
|
121
|
+
changes.push({
|
|
122
|
+
setting: "security.dependabot_alerts",
|
|
123
|
+
from: show(have),
|
|
124
|
+
to: show(alerts),
|
|
125
|
+
apply: () => send(alerts ? "PUT" : "DELETE", "/vulnerability-alerts", "setting Dependabot alerts"),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const fixes = settings.security?.dependabot_security_updates;
|
|
130
|
+
if (fixes !== undefined) {
|
|
131
|
+
const response = await get("/automated-security-fixes", "reading Dependabot security updates", true);
|
|
132
|
+
const have = response.status !== 404 && response.data?.enabled === true;
|
|
133
|
+
if (have !== fixes) {
|
|
134
|
+
changes.push({
|
|
135
|
+
setting: "security.dependabot_security_updates",
|
|
136
|
+
from: show(have),
|
|
137
|
+
to: show(fixes),
|
|
138
|
+
apply: () => send(fixes ? "PUT" : "DELETE", "/automated-security-fixes", "setting Dependabot security updates"),
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (settings.protect !== undefined) {
|
|
143
|
+
const list = (await get("/rulesets?includes_parents=false", "reading rulesets")).data;
|
|
144
|
+
const existing = list.find((r) => r.name === RULESET_NAME);
|
|
145
|
+
const detail = existing
|
|
146
|
+
? (await get(`/rulesets/${existing.id}`, "reading the repokeeper ruleset")).data
|
|
147
|
+
: null;
|
|
148
|
+
const have = detail ? summarize(detail.rules ?? []) : null;
|
|
149
|
+
if (settings.protect === false) {
|
|
150
|
+
if (existing) {
|
|
151
|
+
changes.push({
|
|
152
|
+
setting: "protect",
|
|
153
|
+
from: describeRules(have),
|
|
154
|
+
to: "(none)",
|
|
155
|
+
apply: () => send("DELETE", `/rulesets/${existing.id}`, "removing the repokeeper ruleset"),
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
160
|
+
const rules = desiredRules(settings.protect);
|
|
161
|
+
const want = summarize(rules);
|
|
162
|
+
const targetsDefault = detail?.conditions?.ref_name?.include?.includes("~DEFAULT_BRANCH") ?? false;
|
|
163
|
+
const upToDate = have !== null && sameSummary(have, want) && detail?.enforcement === "active" && targetsDefault;
|
|
164
|
+
if (!upToDate) {
|
|
165
|
+
const body = {
|
|
166
|
+
name: RULESET_NAME,
|
|
167
|
+
target: "branch",
|
|
168
|
+
enforcement: "active",
|
|
169
|
+
conditions: { ref_name: { include: ["~DEFAULT_BRANCH"], exclude: [] } },
|
|
170
|
+
rules,
|
|
171
|
+
};
|
|
172
|
+
changes.push({
|
|
173
|
+
setting: "protect",
|
|
174
|
+
from: describeRules(have),
|
|
175
|
+
to: describeRules(want),
|
|
176
|
+
apply: () => existing
|
|
177
|
+
? send("PUT", `/rulesets/${existing.id}`, "updating the repokeeper ruleset", body)
|
|
178
|
+
: send("POST", "/rulesets", "creating the repokeeper ruleset", body),
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return changes;
|
|
184
|
+
}
|
package/dist/platforms/github.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { stringify } from "yaml";
|
|
2
2
|
import { defaultBranch } from "../config/types.js";
|
|
3
3
|
import { MANAGED_HEADER } from "../model.js";
|
|
4
|
-
import { REUSABLE_REPO, WORKFLOW_REF } from "../version.js";
|
|
4
|
+
import { PACKAGE_VERSION, REUSABLE_REPO, WORKFLOW_REF } from "../version.js";
|
|
5
5
|
const yamlFile = (module, path, data) => ({
|
|
6
6
|
kind: "file",
|
|
7
7
|
module,
|
|
@@ -120,6 +120,12 @@ export const githubPlatform = {
|
|
|
120
120
|
if (ctx.config.modules.commits) {
|
|
121
121
|
jobs.push(workflowKey("ci", path, ["jobs", "commits"], { uses: workflowRef(ctx, "commitlint.yml") }));
|
|
122
122
|
}
|
|
123
|
+
if (ctx.config.modules.drift) {
|
|
124
|
+
jobs.push(workflowKey("ci", path, ["jobs", "repokeeper"], {
|
|
125
|
+
uses: workflowRef(ctx, "repokeeper-check.yml"),
|
|
126
|
+
with: { version: PACKAGE_VERSION },
|
|
127
|
+
}));
|
|
128
|
+
}
|
|
123
129
|
if (jobs.length === 0)
|
|
124
130
|
return [];
|
|
125
131
|
return [
|
package/package.json
CHANGED