github-settings-as-code 3.1.0 → 3.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 +54 -34
- package/dist/cmd/repo-settings.mjs +13 -0
- package/dist/cmd/repo-settings.mjs.map +1 -1
- package/dist/github/constants.d.mts +2 -0
- package/dist/github/constants.d.mts.map +1 -1
- package/dist/github/constants.mjs +2 -0
- package/dist/github/constants.mjs.map +1 -1
- package/dist/github/environments/api/get-environments.d.mts +16 -0
- package/dist/github/environments/api/get-environments.d.mts.map +1 -0
- package/dist/github/environments/api/get-environments.mjs +98 -0
- package/dist/github/environments/api/get-environments.mjs.map +1 -0
- package/dist/github/environments/api/index.d.mts +3 -0
- package/dist/github/environments/api/index.d.mts.map +1 -0
- package/dist/github/environments/api/index.mjs +3 -0
- package/dist/github/environments/api/index.mjs.map +1 -0
- package/dist/github/environments/api/set-environment.d.mts +10 -0
- package/dist/github/environments/api/set-environment.d.mts.map +1 -0
- package/dist/github/environments/api/set-environment.mjs +73 -0
- package/dist/github/environments/api/set-environment.mjs.map +1 -0
- package/dist/github/environments/apply.d.mts +11 -0
- package/dist/github/environments/apply.d.mts.map +1 -0
- package/dist/github/environments/apply.mjs +67 -0
- package/dist/github/environments/apply.mjs.map +1 -0
- package/dist/github/environments/backup.d.mts +5 -0
- package/dist/github/environments/backup.d.mts.map +1 -0
- package/dist/github/environments/backup.mjs +24 -0
- package/dist/github/environments/backup.mjs.map +1 -0
- package/dist/github/environments/constants.d.mts +354 -0
- package/dist/github/environments/constants.d.mts.map +1 -0
- package/dist/github/environments/constants.mjs +62 -0
- package/dist/github/environments/constants.mjs.map +1 -0
- package/dist/github/environments/index.d.mts +6 -0
- package/dist/github/environments/index.d.mts.map +1 -0
- package/dist/github/environments/index.mjs +6 -0
- package/dist/github/environments/index.mjs.map +1 -0
- package/dist/github/environments/read-environment-contents.d.mts +4 -0
- package/dist/github/environments/read-environment-contents.d.mts.map +1 -0
- package/dist/github/environments/read-environment-contents.mjs +32 -0
- package/dist/github/environments/read-environment-contents.mjs.map +1 -0
- package/dist/github/index.d.mts +2 -0
- package/dist/github/index.d.mts.map +1 -1
- package/dist/github/index.mjs +2 -0
- package/dist/github/index.mjs.map +1 -1
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts +10 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs +25 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/api/index.d.mts +3 -0
- package/dist/github/vulnerability-alerts/api/index.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/index.mjs +3 -0
- package/dist/github/vulnerability-alerts/api/index.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts +10 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs +19 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/apply.d.mts +11 -0
- package/dist/github/vulnerability-alerts/apply.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/apply.mjs +47 -0
- package/dist/github/vulnerability-alerts/apply.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/backup.d.mts +5 -0
- package/dist/github/vulnerability-alerts/backup.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/backup.mjs +22 -0
- package/dist/github/vulnerability-alerts/backup.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/constants.d.mts +72 -0
- package/dist/github/vulnerability-alerts/constants.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/constants.mjs +21 -0
- package/dist/github/vulnerability-alerts/constants.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/index.d.mts +5 -0
- package/dist/github/vulnerability-alerts/index.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/index.mjs +5 -0
- package/dist/github/vulnerability-alerts/index.mjs.map +1 -0
- package/package.json +1 -1
- package/src/cmd/repo-settings.mts +17 -0
- package/src/github/constants.mts +7 -0
- package/src/github/environments/api/get-environments.mts +155 -0
- package/src/github/environments/api/index.mts +2 -0
- package/src/github/environments/api/set-environment.mts +106 -0
- package/src/github/environments/apply.mts +87 -0
- package/src/github/environments/backup.mts +38 -0
- package/src/github/environments/constants.mts +78 -0
- package/src/github/environments/index.mts +5 -0
- package/src/github/environments/read-environment-contents.mts +53 -0
- package/src/github/index.mts +2 -0
- package/src/github/vulnerability-alerts/api/get-vulnerability-alerts.mts +31 -0
- package/src/github/vulnerability-alerts/api/index.mts +2 -0
- package/src/github/vulnerability-alerts/api/set-vulnerability-alerts.mts +31 -0
- package/src/github/vulnerability-alerts/apply.mts +77 -0
- package/src/github/vulnerability-alerts/backup.mts +36 -0
- package/src/github/vulnerability-alerts/constants.mts +25 -0
- package/src/github/vulnerability-alerts/index.mts +4 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as t from 'ts-fortress';
|
|
6
|
+
import { formatUncommittedFiles, isDirectlyExecuted, pathExists, Result, } from 'ts-repo-utils';
|
|
7
|
+
import { settingsJsonName, vulnerabilityAlertsDir } from '../constants.mjs';
|
|
8
|
+
import { getVulnerabilityAlerts, setVulnerabilityAlerts, } from './api/index.mjs';
|
|
9
|
+
import { VulnerabilityAlertsSettings } from './constants.mjs';
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
export const applyVulnerabilityAlerts = async () => {
|
|
18
|
+
const settingsPath = path.resolve(vulnerabilityAlertsDir, settingsJsonName);
|
|
19
|
+
if (!(await pathExists(settingsPath))) {
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
const settings = await readSettings(settingsPath);
|
|
23
|
+
await setVulnerabilityAlerts(settings);
|
|
24
|
+
|
|
25
|
+
{
|
|
26
|
+
const applied = await getVulnerabilityAlerts();
|
|
27
|
+
const str = JSON.stringify(applied, undefined, 2);
|
|
28
|
+
|
|
29
|
+
await fs.writeFile(settingsPath, str);
|
|
30
|
+
|
|
31
|
+
await fs.writeFile(path.resolve(vulnerabilityAlertsDir, 'bk', settingsJsonName), str);
|
|
32
|
+
await formatUncommittedFiles();
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
const readSettings = async (settingsPath) => {
|
|
36
|
+
|
|
37
|
+
const settingsText = await fs.readFile(settingsPath, { encoding: 'utf8' });
|
|
38
|
+
const validationResult = VulnerabilityAlertsSettings.validate(JSON.parse(settingsText));
|
|
39
|
+
if (Result.isErr(validationResult)) {
|
|
40
|
+
throw new Error(t.validationErrorsToMessages(validationResult.value).join('\n'));
|
|
41
|
+
}
|
|
42
|
+
return validationResult.value;
|
|
43
|
+
};
|
|
44
|
+
if (isDirectlyExecuted(import.meta.url)) {
|
|
45
|
+
await applyVulnerabilityAlerts();
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=apply.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"apply.mjs","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/apply.mts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,KAAK,CAAC,MAAM,aAAa,CAAC;AACjC,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,UAAU,EACV,MAAM,GACP,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EACL,sBAAsB,EACtB,sBAAsB,GACvB,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,2BAA2B,EAAE,MAAM,iBAAiB,CAAC;AAE9D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,KAAK,IAAmB,EAAE;IAChE,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,gBAAgB,CAAC,CAAC;IAE5E,IAAI,CAAC,CAAC,MAAM,UAAU,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC;QACtC,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,YAAY,CAAC,CAAC;IAElD,MAAM,sBAAsB,CAAC,QAAQ,CAAC,CAAC;IAEvC,0BAA0B;IAC1B,CAAC;QACC,MAAM,OAAO,GAAG,MAAM,sBAAsB,EAAE,CAAC;QAE/C,MAAM,GAAG,GAAG,IAAI,CAAC,SAAS,CAAC,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,CAAC;QAElD,mEAAmE;QACnE,MAAM,EAAE,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,CAAC,CAAC;QAEtC,mEAAmE;QACnE,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,IAAI,EAAE,gBAAgB,CAAC,EAC5D,GAAG,CACJ,CAAC;QAEF,MAAM,sBAAsB,EAAE,CAAC;IACjC,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,KAAK,EACxB,YAAoB,EACkB,EAAE;IACxC,mEAAmE;IACnE,MAAM,YAAY,GAAG,MAAM,EAAE,CAAC,QAAQ,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC,CAAC;IAE3E,MAAM,gBAAgB,GAAG,2BAA2B,CAAC,QAAQ,CAC3D,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,CACzB,CAAC;IAEF,IAAI,MAAM,CAAC,KAAK,CAAC,gBAAgB,CAAC,EAAE,CAAC;QACnC,MAAM,IAAI,KAAK,CACb,CAAC,CAAC,0BAA0B,CAAC,gBAAgB,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAChE,CAAC;IACJ,CAAC;IAED,OAAO,gBAAgB,CAAC,KAAK,CAAC;AAChC,CAAC,CAAC;AAEF,IAAI,kBAAkB,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACxC,MAAM,wBAAwB,EAAE,CAAC;AACnC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.d.mts","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/backup.mts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AAavB,2CAA2C;AAC3C,eAAO,MAAM,yBAAyB,SAC/B,OAAO,KACX,OAAO,CAAC,IAAI,CAcd,CAAC"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { formatUncommittedFiles, isDirectlyExecuted, makeEmptyDir, } from 'ts-repo-utils';
|
|
6
|
+
import { settingsJsonName, vulnerabilityAlertsDir } from '../constants.mjs';
|
|
7
|
+
import { getVulnerabilityAlerts } from './api/index.mjs';
|
|
8
|
+
const backupDir = path.resolve(vulnerabilityAlertsDir, './bk');
|
|
9
|
+
|
|
10
|
+
export const backupVulnerabilityAlerts = async (fmt = true) => {
|
|
11
|
+
await makeEmptyDir(backupDir);
|
|
12
|
+
const settings = await getVulnerabilityAlerts();
|
|
13
|
+
|
|
14
|
+
await fs.writeFile(path.resolve(backupDir, settingsJsonName), JSON.stringify(settings, undefined, 2));
|
|
15
|
+
if (fmt) {
|
|
16
|
+
await formatUncommittedFiles();
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
if (isDirectlyExecuted(import.meta.url)) {
|
|
20
|
+
await backupVulnerabilityAlerts();
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=backup.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"backup.mjs","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/backup.mts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,KAAK,EAAE,MAAM,kBAAkB,CAAC;AACvC,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EACL,sBAAsB,EACtB,kBAAkB,EAClB,YAAY,GACb,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC5E,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,sBAAsB,EAAE,MAAM,CAAC,CAAC;AAE/D,2CAA2C;AAC3C,MAAM,CAAC,MAAM,yBAAyB,GAAG,KAAK,EAC5C,GAAG,GAAY,IAAI,EACJ,EAAE;IACjB,MAAM,YAAY,CAAC,SAAS,CAAC,CAAC;IAE9B,MAAM,QAAQ,GAAG,MAAM,sBAAsB,EAAE,CAAC;IAEhD,mEAAmE;IACnE,MAAM,EAAE,CAAC,SAAS,CAChB,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,gBAAgB,CAAC,EACzC,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,SAAS,EAAE,CAAC,CAAC,CACvC,CAAC;IAEF,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,sBAAsB,EAAE,CAAC;IACjC,CAAC;AACH,CAAC,CAAC;AAEF,IAAI,kBAAkB,CAAC,OAAO,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACxC,MAAM,yBAAyB,EAAE,CAAC;AACpC,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import * as t from 'ts-fortress';
|
|
2
|
+
/**
|
|
3
|
+
* `repo-settings/vulnerability-alerts/settings.json` の型。
|
|
4
|
+
*
|
|
5
|
+
* Settings > Advanced Security > Dependabot alerts に対応する。
|
|
6
|
+
*
|
|
7
|
+
* `security_and_analysis` とは別の API
|
|
8
|
+
* (`GET|PUT|DELETE /repos/{owner}/{repo}/vulnerability-alerts`) で、
|
|
9
|
+
* 値も JSON body ではなく 204 / 404 で返ってくる。そのため
|
|
10
|
+
* `repository-settings/settings.json` には載せられず、 pages と同じく
|
|
11
|
+
* 独立した設定ファイルにしている。
|
|
12
|
+
*
|
|
13
|
+
* `repository-settings/settings.json` の `dependabot_security_updates` は
|
|
14
|
+
* これとは別物 — あちらは「脆弱な依存を直す PR を自動で作るか」で、
|
|
15
|
+
* この repository では `pnpm-update` が lockfile を持っているため無効のまま。
|
|
16
|
+
* ここで管理するのは「既知の脆弱性を知らせるか」だけ。
|
|
17
|
+
*/
|
|
18
|
+
export declare const VulnerabilityAlertsSettings: Readonly<{
|
|
19
|
+
typeName: string;
|
|
20
|
+
defaultValue: Readonly<{
|
|
21
|
+
enabled: boolean;
|
|
22
|
+
}>;
|
|
23
|
+
is: (a: unknown) => a is Readonly<{
|
|
24
|
+
enabled: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
assertIs: (a: unknown) => asserts a is Readonly<{
|
|
27
|
+
enabled: boolean;
|
|
28
|
+
}>;
|
|
29
|
+
cast: (a: unknown) => Readonly<{
|
|
30
|
+
enabled: boolean;
|
|
31
|
+
}>;
|
|
32
|
+
fill: (a: unknown) => Readonly<{
|
|
33
|
+
enabled: boolean;
|
|
34
|
+
}>;
|
|
35
|
+
prune: <B extends Readonly<{
|
|
36
|
+
enabled: boolean;
|
|
37
|
+
}>>(a: B) => Readonly<{
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}>;
|
|
40
|
+
validate: (a: unknown) => t.Result<Readonly<{
|
|
41
|
+
enabled: boolean;
|
|
42
|
+
}>, readonly Readonly<{
|
|
43
|
+
path: readonly string[];
|
|
44
|
+
actualValue: unknown;
|
|
45
|
+
expectedType: string;
|
|
46
|
+
typeName: string;
|
|
47
|
+
details?: t.ValidationErrorDetails | undefined;
|
|
48
|
+
}>[]>;
|
|
49
|
+
optional?: true;
|
|
50
|
+
}> & Readonly<{
|
|
51
|
+
shape: {
|
|
52
|
+
readonly enabled: Readonly<{
|
|
53
|
+
typeName: string;
|
|
54
|
+
defaultValue: boolean;
|
|
55
|
+
is: (a: unknown) => a is boolean;
|
|
56
|
+
assertIs: (a: unknown) => asserts a is boolean;
|
|
57
|
+
cast: (a: unknown) => boolean;
|
|
58
|
+
fill: (a: unknown) => boolean;
|
|
59
|
+
prune: <B extends boolean>(a: B) => boolean;
|
|
60
|
+
validate: (a: unknown) => t.Result<boolean, readonly Readonly<{
|
|
61
|
+
path: readonly string[];
|
|
62
|
+
actualValue: unknown;
|
|
63
|
+
expectedType: string;
|
|
64
|
+
typeName: string;
|
|
65
|
+
details?: t.ValidationErrorDetails | undefined;
|
|
66
|
+
}>[]>;
|
|
67
|
+
optional?: true;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
}>;
|
|
71
|
+
export type VulnerabilityAlertsSettings = t.TypeOf<typeof VulnerabilityAlertsSettings>;
|
|
72
|
+
//# sourceMappingURL=constants.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.d.mts","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/constants.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,aAAa,CAAC;AAEjC;;;;;;;;;;;;;;;GAeG;AACH,eAAO,MAAM,2BAA2B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAEtC,CAAC;AAEH,MAAM,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAChD,OAAO,2BAA2B,CACnC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"constants.mjs","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/constants.mts"],"names":[],"mappings":"AAAA,OAAO,KAAK,CAAC,MAAM,aAAa,CAAC;AAEjC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,CAAC,CAAC,MAAM,CAAC;IAClD,OAAO,EAAE,CAAC,CAAC,OAAO,CAAC,KAAK,CAAC;CAC1B,CAAC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/index.mts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","sourceRoot":"","sources":["../../../src/github/vulnerability-alerts/index.mts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,aAAa,CAAC;AAC5B,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC"}
|
package/package.json
CHANGED
|
@@ -22,6 +22,9 @@ const HELP = dedent`
|
|
|
22
22
|
variables repository variables ( apply のみ )
|
|
23
23
|
actions repo-settings/actions-settings/settings.json
|
|
24
24
|
pages repo-settings/pages/settings.json
|
|
25
|
+
environments repo-settings/environments/*.json
|
|
26
|
+
vulnerability-alerts
|
|
27
|
+
repo-settings/vulnerability-alerts/settings.json
|
|
25
28
|
|
|
26
29
|
Options:
|
|
27
30
|
--owner <owner> 対象の owner
|
|
@@ -54,6 +57,8 @@ const TARGETS = [
|
|
|
54
57
|
'variables',
|
|
55
58
|
'actions',
|
|
56
59
|
'pages',
|
|
60
|
+
'environments',
|
|
61
|
+
'vulnerability-alerts',
|
|
57
62
|
] as const;
|
|
58
63
|
|
|
59
64
|
type Target = (typeof TARGETS)[number];
|
|
@@ -125,12 +130,18 @@ const runners: ReadonlyRecord<
|
|
|
125
130
|
await github.applyActionsSettings();
|
|
126
131
|
|
|
127
132
|
await github.applyPagesSettings();
|
|
133
|
+
|
|
134
|
+
await github.applyEnvironments();
|
|
135
|
+
|
|
136
|
+
await github.applyVulnerabilityAlerts();
|
|
128
137
|
},
|
|
129
138
|
repository: github.applyRepositorySettings,
|
|
130
139
|
rulesets: github.applyRulesets,
|
|
131
140
|
variables: github.applyVariables,
|
|
132
141
|
actions: github.applyActionsSettings,
|
|
133
142
|
pages: github.applyPagesSettings,
|
|
143
|
+
environments: github.applyEnvironments,
|
|
144
|
+
'vulnerability-alerts': github.applyVulnerabilityAlerts,
|
|
134
145
|
},
|
|
135
146
|
backup: {
|
|
136
147
|
all: async () => {
|
|
@@ -141,11 +152,17 @@ const runners: ReadonlyRecord<
|
|
|
141
152
|
await github.backupActionsSettings();
|
|
142
153
|
|
|
143
154
|
await github.backupPagesSettings();
|
|
155
|
+
|
|
156
|
+
await github.backupEnvironments();
|
|
157
|
+
|
|
158
|
+
await github.backupVulnerabilityAlerts();
|
|
144
159
|
},
|
|
145
160
|
repository: github.backupRepositorySettings,
|
|
146
161
|
rulesets: github.backupRulesets,
|
|
147
162
|
actions: github.backupActionsSettings,
|
|
148
163
|
pages: github.backupPagesSettings,
|
|
164
|
+
environments: github.backupEnvironments,
|
|
165
|
+
'vulnerability-alerts': github.backupVulnerabilityAlerts,
|
|
149
166
|
},
|
|
150
167
|
} as const;
|
|
151
168
|
|
package/src/github/constants.mts
CHANGED
|
@@ -25,6 +25,13 @@ export const actionsSettingsDir = path.resolve(
|
|
|
25
25
|
|
|
26
26
|
export const pagesSettingsDir = path.resolve(settingsDir, './pages');
|
|
27
27
|
|
|
28
|
+
export const environmentsDir = path.resolve(settingsDir, './environments');
|
|
29
|
+
|
|
30
|
+
export const vulnerabilityAlertsDir = path.resolve(
|
|
31
|
+
settingsDir,
|
|
32
|
+
'./vulnerability-alerts',
|
|
33
|
+
);
|
|
34
|
+
|
|
28
35
|
export const rulesetsDir = path.resolve(settingsDir, './rulesets');
|
|
29
36
|
|
|
30
37
|
export const octokitHeaders = {
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type EndpointKeys } from 'octokit-safe-types';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import * as t from 'ts-fortress';
|
|
4
|
+
import { Result } from 'ts-repo-utils';
|
|
5
|
+
import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
|
|
6
|
+
import { octokit } from '../../octokit.mjs';
|
|
7
|
+
import { EnvironmentSettings } from '../constants.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Settings > Environments の現在値を、宣言ファイルと同じ形にして返す。
|
|
11
|
+
*
|
|
12
|
+
* 名前順に整える。 GitHub が返す順は作成順で、環境を 1 つ足しただけで
|
|
13
|
+
* `bk/` の中身が並び替わると差分が読めなくなるため。
|
|
14
|
+
*/
|
|
15
|
+
export const getAllEnvironments = async (): Promise<
|
|
16
|
+
readonly EnvironmentSettings[]
|
|
17
|
+
> => {
|
|
18
|
+
// https://docs.github.com/rest/deployments/environments#list-environments
|
|
19
|
+
//
|
|
20
|
+
// per_page の上限。環境は数個しか無く、 2 ページ目が要る状況は
|
|
21
|
+
// 「 GUI で何かが大量に作られた」ときだけなので、ページングは張らない。
|
|
22
|
+
const response = await octokit.request(
|
|
23
|
+
'GET /repos/{owner}/{repo}/environments' satisfies EndpointKeys,
|
|
24
|
+
{ owner: OWNER, repo: REPO, headers: octokitHeaders, per_page: 100 },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const environments = response.data.environments ?? [];
|
|
28
|
+
|
|
29
|
+
const settings = await Promise.all(
|
|
30
|
+
environments.map(async (environment) => {
|
|
31
|
+
const policy = environment.deployment_branch_policy ?? undefined;
|
|
32
|
+
|
|
33
|
+
return EnvironmentSettings.fill({
|
|
34
|
+
name: environment.name,
|
|
35
|
+
...parseProtectionRules(environment.protection_rules),
|
|
36
|
+
deployment_branch_policy:
|
|
37
|
+
policy === undefined
|
|
38
|
+
? null
|
|
39
|
+
: {
|
|
40
|
+
protected_branches: policy.protected_branches,
|
|
41
|
+
custom_branch_policies: policy.custom_branch_policies,
|
|
42
|
+
},
|
|
43
|
+
deployment_branch_policies:
|
|
44
|
+
policy?.custom_branch_policies === true
|
|
45
|
+
? await getDeploymentBranchPolicyNames(environment.name)
|
|
46
|
+
: [],
|
|
47
|
+
});
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return settings.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 宣言ファイルに載る形。 `id` は GitHub 側の都合なので落とす。 */
|
|
55
|
+
const getDeploymentBranchPolicyNames = async (
|
|
56
|
+
environmentName: string,
|
|
57
|
+
): Promise<readonly Readonly<{ name: string; type: 'branch' | 'tag' }>[]> => {
|
|
58
|
+
const policies = await getDeploymentBranchPolicies(environmentName);
|
|
59
|
+
|
|
60
|
+
return policies.map(({ name, type }) => ({ name, type }));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** 「 Selected branches and tags 」に登録されている pattern を列挙する。 */
|
|
64
|
+
export const getDeploymentBranchPolicies = async (
|
|
65
|
+
environmentName: string,
|
|
66
|
+
): Promise<readonly DeploymentBranchPolicy[]> => {
|
|
67
|
+
// https://docs.github.com/rest/deployments/branch-policies#list-deployment-branch-policies
|
|
68
|
+
const response = await octokit.request(
|
|
69
|
+
'GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies' satisfies EndpointKeys,
|
|
70
|
+
{
|
|
71
|
+
owner: OWNER,
|
|
72
|
+
repo: REPO,
|
|
73
|
+
environment_name: environmentName,
|
|
74
|
+
headers: octokitHeaders,
|
|
75
|
+
per_page: 100,
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return response.data.branch_policies.map((policy) => ({
|
|
80
|
+
id: policy.id ?? 0,
|
|
81
|
+
name: policy.name ?? '',
|
|
82
|
+
type: policy.type === 'tag' ? 'tag' : 'branch',
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type DeploymentBranchPolicy = Readonly<{
|
|
87
|
+
id: number;
|
|
88
|
+
name: string;
|
|
89
|
+
type: 'branch' | 'tag';
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET が返す `protection_rules` を PUT の body の形に戻す。
|
|
94
|
+
*
|
|
95
|
+
* GitHub は有効な保護ルールだけを配列で返し、その要素の `type` は openapi の
|
|
96
|
+
* 型定義では素の `string` — つまり判別可能 union になっていないので、絞り込みが
|
|
97
|
+
* 効かない。実データを ts-fortress で読み直すほうが、上流の型に合わせて
|
|
98
|
+
* キャストを書くより素直で、形が変わったときに黙って通り過ぎることもない。
|
|
99
|
+
*/
|
|
100
|
+
const parseProtectionRules = (
|
|
101
|
+
protectionRules: unknown,
|
|
102
|
+
): Readonly<{
|
|
103
|
+
wait_timer: number;
|
|
104
|
+
prevent_self_review: boolean;
|
|
105
|
+
reviewers: readonly Readonly<{ type: 'User' | 'Team'; id: number }>[];
|
|
106
|
+
}> => {
|
|
107
|
+
const validationResult = ProtectionRules.validate(protectionRules ?? []);
|
|
108
|
+
|
|
109
|
+
if (Result.isErr(validationResult)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
Arr.toUnshifted(
|
|
112
|
+
'GitHub が返した environment の protection_rules を読めませんでした。',
|
|
113
|
+
)(t.validationErrorsToMessages(validationResult.value)).join('\n'),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rules = validationResult.value;
|
|
118
|
+
|
|
119
|
+
const waitTimer = rules.find((rule) => rule.type === 'wait_timer');
|
|
120
|
+
|
|
121
|
+
const requiredReviewers = rules.find(
|
|
122
|
+
(rule) => rule.type === 'required_reviewers',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
wait_timer: waitTimer?.wait_timer ?? 0,
|
|
127
|
+
prevent_self_review: requiredReviewers?.prevent_self_review ?? false,
|
|
128
|
+
reviewers: (requiredReviewers?.reviewers ?? []).map((entry) => ({
|
|
129
|
+
type: entry.type,
|
|
130
|
+
id: entry.reviewer.id,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ProtectionRules = t.array(
|
|
136
|
+
t.record({
|
|
137
|
+
type: t.string(''),
|
|
138
|
+
wait_timer: t.optional(t.number(0)),
|
|
139
|
+
prevent_self_review: t.optional(t.boolean(false)),
|
|
140
|
+
reviewers: t.optional(
|
|
141
|
+
t.array(
|
|
142
|
+
t.record({
|
|
143
|
+
type: t.enumType(['User', 'Team'], {
|
|
144
|
+
typeName: 'EnvironmentReviewerType',
|
|
145
|
+
defaultValue: 'User',
|
|
146
|
+
}),
|
|
147
|
+
// `reviewer` は User / Team の完全な表現だが、書き戻しに要るのは id
|
|
148
|
+
// だけ。 record は既定で余剰キーを通すので、残りはそのまま無視される。
|
|
149
|
+
reviewer: t.record({ id: t.number(0) }),
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
}),
|
|
154
|
+
{ typeName: 'EnvironmentProtectionRules' },
|
|
155
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type EndpointKeys } from 'octokit-safe-types';
|
|
2
|
+
import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
|
|
3
|
+
import { octokit } from '../../octokit.mjs';
|
|
4
|
+
import { type EnvironmentSettings } from '../constants.mjs';
|
|
5
|
+
import { getDeploymentBranchPolicies } from './get-environments.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 環境を JSON の内容に合わせる。無ければ作られる。
|
|
9
|
+
*
|
|
10
|
+
* `PUT` は保護ルールを丸ごと置き換えるので、省いたキーは「変更しない」ではなく
|
|
11
|
+
* 「無効にする」になる。宣言ファイルをそのまま渡しているのはそのため —
|
|
12
|
+
* 部分更新にすると、ファイルから消した設定が GitHub 側に残る。
|
|
13
|
+
*/
|
|
14
|
+
export const setEnvironment = async (
|
|
15
|
+
settings: EnvironmentSettings,
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
// https://docs.github.com/rest/deployments/environments#create-or-update-an-environment
|
|
18
|
+
await octokit.request(
|
|
19
|
+
'PUT /repos/{owner}/{repo}/environments/{environment_name}' satisfies EndpointKeys,
|
|
20
|
+
{
|
|
21
|
+
owner: OWNER,
|
|
22
|
+
repo: REPO,
|
|
23
|
+
environment_name: settings.name,
|
|
24
|
+
headers: octokitHeaders,
|
|
25
|
+
wait_timer: settings.wait_timer,
|
|
26
|
+
prevent_self_review: settings.prevent_self_review,
|
|
27
|
+
reviewers: settings.reviewers.map((reviewer) => ({
|
|
28
|
+
type: reviewer.type,
|
|
29
|
+
id: reviewer.id,
|
|
30
|
+
})),
|
|
31
|
+
deployment_branch_policy:
|
|
32
|
+
settings.deployment_branch_policy === null
|
|
33
|
+
? null
|
|
34
|
+
: {
|
|
35
|
+
protected_branches:
|
|
36
|
+
settings.deployment_branch_policy.protected_branches,
|
|
37
|
+
custom_branch_policies:
|
|
38
|
+
settings.deployment_branch_policy.custom_branch_policies,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await setDeploymentBranchPolicies(settings);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 「 Selected branches and tags 」の pattern を宣言に合わせる。
|
|
48
|
+
*
|
|
49
|
+
* 個別の追加・削除しか無いエンドポイントなので、現在値との差分を取って埋める。
|
|
50
|
+
* 名前と種別が一致するものは触らない — 消して作り直すと id が変わり、
|
|
51
|
+
* その間だけ deploy できない ref ができる。
|
|
52
|
+
*/
|
|
53
|
+
const keyOf = (policy: Readonly<{ name: string; type: string }>): string =>
|
|
54
|
+
`${policy.type}:${policy.name}` as const;
|
|
55
|
+
|
|
56
|
+
const setDeploymentBranchPolicies = async (
|
|
57
|
+
settings: EnvironmentSettings,
|
|
58
|
+
): Promise<void> => {
|
|
59
|
+
if (settings.deployment_branch_policy?.custom_branch_policies !== true) {
|
|
60
|
+
// 選択されていないなら pattern は存在し得ない。 PUT 側が既に消している。
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const current = await getDeploymentBranchPolicies(settings.name);
|
|
65
|
+
|
|
66
|
+
const desiredKeys: ReadonlySet<string> = new Set(
|
|
67
|
+
settings.deployment_branch_policies.map(keyOf),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const currentKeys: ReadonlySet<string> = new Set(current.map(keyOf));
|
|
71
|
+
|
|
72
|
+
const toDelete = current.filter((p) => !desiredKeys.has(keyOf(p)));
|
|
73
|
+
|
|
74
|
+
const toCreate = settings.deployment_branch_policies.filter(
|
|
75
|
+
(p) => !currentKeys.has(keyOf(p)),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const policy of toDelete) {
|
|
79
|
+
// https://docs.github.com/rest/deployments/branch-policies#delete-a-deployment-branch-policy
|
|
80
|
+
await octokit.request(
|
|
81
|
+
'DELETE /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}' satisfies EndpointKeys,
|
|
82
|
+
{
|
|
83
|
+
owner: OWNER,
|
|
84
|
+
repo: REPO,
|
|
85
|
+
environment_name: settings.name,
|
|
86
|
+
branch_policy_id: policy.id,
|
|
87
|
+
headers: octokitHeaders,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const policy of toCreate) {
|
|
93
|
+
// https://docs.github.com/rest/deployments/branch-policies#create-a-deployment-branch-policy
|
|
94
|
+
await octokit.request(
|
|
95
|
+
'POST /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies' satisfies EndpointKeys,
|
|
96
|
+
{
|
|
97
|
+
owner: OWNER,
|
|
98
|
+
repo: REPO,
|
|
99
|
+
environment_name: settings.name,
|
|
100
|
+
headers: octokitHeaders,
|
|
101
|
+
name: policy.name,
|
|
102
|
+
type: policy.type,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import 'dotenv/config';
|
|
3
|
+
import * as fs from 'node:fs/promises';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { Arr } from 'ts-data-forge';
|
|
6
|
+
import { isDirectlyExecuted } from 'ts-repo-utils';
|
|
7
|
+
import { environmentsDir } from '../constants.mjs';
|
|
8
|
+
import { getAllEnvironments, setEnvironment } from './api/index.mjs';
|
|
9
|
+
import { backupEnvironments } from './backup.mjs';
|
|
10
|
+
import { type EnvironmentSettings } from './constants.mjs';
|
|
11
|
+
import { readEnvironmentFiles } from './read-environment-contents.mjs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* `repo-settings/environments/*.json` を Settings > Environments に反映する。
|
|
15
|
+
*
|
|
16
|
+
* ruleset と同じく、宣言に無い環境は消さない。 GUI で作られたものが `bk/` に
|
|
17
|
+
* 現れることが drift の見え方であって、 apply が黙って片付けてしまうと
|
|
18
|
+
* 「いつ誰が何を作ったか」を見る機会がなくなる。
|
|
19
|
+
*/
|
|
20
|
+
export const applyEnvironments = async (): Promise<void> => {
|
|
21
|
+
await backupEnvironments(false);
|
|
22
|
+
|
|
23
|
+
const environments = await readEnvironmentFiles();
|
|
24
|
+
|
|
25
|
+
for (const environment of environments) {
|
|
26
|
+
assertConsistent(environment);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
for (const environment of environments) {
|
|
30
|
+
await setEnvironment(environment);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// 反映後の実際の値でローカルのファイルを更新する
|
|
34
|
+
{
|
|
35
|
+
const applied = await getAllEnvironments();
|
|
36
|
+
|
|
37
|
+
for (const environment of applied) {
|
|
38
|
+
const str = JSON.stringify(environment, undefined, 2);
|
|
39
|
+
|
|
40
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
41
|
+
await fs.writeFile(
|
|
42
|
+
path.resolve(environmentsDir, `${environment.name}.json`),
|
|
43
|
+
str,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// `bk/` は backup 側が makeEmptyDir から作り直す。ここで書き足すのではなく
|
|
48
|
+
// 呼び直しているのは、消えた環境のファイルが残らないようにするため。
|
|
49
|
+
// 整形もそこでまとめて走る。
|
|
50
|
+
await backupEnvironments();
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* GitHub 側が黙って無視する組み合わせを、送る前に落とす。
|
|
56
|
+
*
|
|
57
|
+
* `custom_branch_policies` が立っていないのに pattern が書いてあるファイルは、
|
|
58
|
+
* 読む側には「この ref だけに制限されている」と読めて、実際には何の制限にも
|
|
59
|
+
* なっていない — 一番危ない種類の嘘なので、エラーにする。
|
|
60
|
+
*/
|
|
61
|
+
const assertConsistent = (environment: EnvironmentSettings): void => {
|
|
62
|
+
const custom =
|
|
63
|
+
environment.deployment_branch_policy?.custom_branch_policies === true;
|
|
64
|
+
|
|
65
|
+
if (!custom && Arr.isNonEmpty(environment.deployment_branch_policies)) {
|
|
66
|
+
throw new Error(
|
|
67
|
+
[
|
|
68
|
+
`環境 "${environment.name}": deployment_branch_policies が書かれていますが、`,
|
|
69
|
+
'deployment_branch_policy.custom_branch_policies が true ではありません。',
|
|
70
|
+
'pattern は "Selected branches and tags" を選んだときにしか効きません。',
|
|
71
|
+
].join('\n'),
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// `custom` が立っている時点で TypeScript は `deployment_branch_policy` を
|
|
76
|
+
// record 側に絞り込んでいる。両方 true は JSON としては書けてしまうので、
|
|
77
|
+
// この組み合わせは送る前に落とす。
|
|
78
|
+
if (custom && environment.deployment_branch_policy.protected_branches) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`環境 "${environment.name}": protected_branches と custom_branch_policies は同時に true にできません。`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
if (isDirectlyExecuted(import.meta.url)) {
|
|
86
|
+
await applyEnvironments();
|
|
87
|
+
}
|