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.
Files changed (90) hide show
  1. package/README.md +54 -34
  2. package/dist/cmd/repo-settings.mjs +13 -0
  3. package/dist/cmd/repo-settings.mjs.map +1 -1
  4. package/dist/github/constants.d.mts +2 -0
  5. package/dist/github/constants.d.mts.map +1 -1
  6. package/dist/github/constants.mjs +2 -0
  7. package/dist/github/constants.mjs.map +1 -1
  8. package/dist/github/environments/api/get-environments.d.mts +16 -0
  9. package/dist/github/environments/api/get-environments.d.mts.map +1 -0
  10. package/dist/github/environments/api/get-environments.mjs +98 -0
  11. package/dist/github/environments/api/get-environments.mjs.map +1 -0
  12. package/dist/github/environments/api/index.d.mts +3 -0
  13. package/dist/github/environments/api/index.d.mts.map +1 -0
  14. package/dist/github/environments/api/index.mjs +3 -0
  15. package/dist/github/environments/api/index.mjs.map +1 -0
  16. package/dist/github/environments/api/set-environment.d.mts +10 -0
  17. package/dist/github/environments/api/set-environment.d.mts.map +1 -0
  18. package/dist/github/environments/api/set-environment.mjs +73 -0
  19. package/dist/github/environments/api/set-environment.mjs.map +1 -0
  20. package/dist/github/environments/apply.d.mts +11 -0
  21. package/dist/github/environments/apply.d.mts.map +1 -0
  22. package/dist/github/environments/apply.mjs +67 -0
  23. package/dist/github/environments/apply.mjs.map +1 -0
  24. package/dist/github/environments/backup.d.mts +5 -0
  25. package/dist/github/environments/backup.d.mts.map +1 -0
  26. package/dist/github/environments/backup.mjs +24 -0
  27. package/dist/github/environments/backup.mjs.map +1 -0
  28. package/dist/github/environments/constants.d.mts +354 -0
  29. package/dist/github/environments/constants.d.mts.map +1 -0
  30. package/dist/github/environments/constants.mjs +62 -0
  31. package/dist/github/environments/constants.mjs.map +1 -0
  32. package/dist/github/environments/index.d.mts +6 -0
  33. package/dist/github/environments/index.d.mts.map +1 -0
  34. package/dist/github/environments/index.mjs +6 -0
  35. package/dist/github/environments/index.mjs.map +1 -0
  36. package/dist/github/environments/read-environment-contents.d.mts +4 -0
  37. package/dist/github/environments/read-environment-contents.d.mts.map +1 -0
  38. package/dist/github/environments/read-environment-contents.mjs +32 -0
  39. package/dist/github/environments/read-environment-contents.mjs.map +1 -0
  40. package/dist/github/index.d.mts +2 -0
  41. package/dist/github/index.d.mts.map +1 -1
  42. package/dist/github/index.mjs +2 -0
  43. package/dist/github/index.mjs.map +1 -1
  44. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts +10 -0
  45. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts.map +1 -0
  46. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs +25 -0
  47. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs.map +1 -0
  48. package/dist/github/vulnerability-alerts/api/index.d.mts +3 -0
  49. package/dist/github/vulnerability-alerts/api/index.d.mts.map +1 -0
  50. package/dist/github/vulnerability-alerts/api/index.mjs +3 -0
  51. package/dist/github/vulnerability-alerts/api/index.mjs.map +1 -0
  52. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts +10 -0
  53. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts.map +1 -0
  54. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs +19 -0
  55. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs.map +1 -0
  56. package/dist/github/vulnerability-alerts/apply.d.mts +11 -0
  57. package/dist/github/vulnerability-alerts/apply.d.mts.map +1 -0
  58. package/dist/github/vulnerability-alerts/apply.mjs +47 -0
  59. package/dist/github/vulnerability-alerts/apply.mjs.map +1 -0
  60. package/dist/github/vulnerability-alerts/backup.d.mts +5 -0
  61. package/dist/github/vulnerability-alerts/backup.d.mts.map +1 -0
  62. package/dist/github/vulnerability-alerts/backup.mjs +22 -0
  63. package/dist/github/vulnerability-alerts/backup.mjs.map +1 -0
  64. package/dist/github/vulnerability-alerts/constants.d.mts +72 -0
  65. package/dist/github/vulnerability-alerts/constants.d.mts.map +1 -0
  66. package/dist/github/vulnerability-alerts/constants.mjs +21 -0
  67. package/dist/github/vulnerability-alerts/constants.mjs.map +1 -0
  68. package/dist/github/vulnerability-alerts/index.d.mts +5 -0
  69. package/dist/github/vulnerability-alerts/index.d.mts.map +1 -0
  70. package/dist/github/vulnerability-alerts/index.mjs +5 -0
  71. package/dist/github/vulnerability-alerts/index.mjs.map +1 -0
  72. package/package.json +1 -1
  73. package/src/cmd/repo-settings.mts +17 -0
  74. package/src/github/constants.mts +7 -0
  75. package/src/github/environments/api/get-environments.mts +155 -0
  76. package/src/github/environments/api/index.mts +2 -0
  77. package/src/github/environments/api/set-environment.mts +106 -0
  78. package/src/github/environments/apply.mts +87 -0
  79. package/src/github/environments/backup.mts +38 -0
  80. package/src/github/environments/constants.mts +78 -0
  81. package/src/github/environments/index.mts +5 -0
  82. package/src/github/environments/read-environment-contents.mts +53 -0
  83. package/src/github/index.mts +2 -0
  84. package/src/github/vulnerability-alerts/api/get-vulnerability-alerts.mts +31 -0
  85. package/src/github/vulnerability-alerts/api/index.mts +2 -0
  86. package/src/github/vulnerability-alerts/api/set-vulnerability-alerts.mts +31 -0
  87. package/src/github/vulnerability-alerts/apply.mts +77 -0
  88. package/src/github/vulnerability-alerts/backup.mts +36 -0
  89. package/src/github/vulnerability-alerts/constants.mts +25 -0
  90. package/src/github/vulnerability-alerts/index.mts +4 -0
@@ -0,0 +1,38 @@
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 {
6
+ formatUncommittedFiles,
7
+ isDirectlyExecuted,
8
+ makeEmptyDir,
9
+ } from 'ts-repo-utils';
10
+ import { environmentsDir } from '../constants.mjs';
11
+ import { getAllEnvironments } from './api/index.mjs';
12
+
13
+ const backupDir = path.resolve(environmentsDir, './bk');
14
+
15
+ /** Settings > Environments の現在値を `bk/` に保存する。 */
16
+ export const backupEnvironments = async (
17
+ fmt: boolean = true,
18
+ ): Promise<void> => {
19
+ await makeEmptyDir(backupDir);
20
+
21
+ const environments = await getAllEnvironments();
22
+
23
+ for (const environment of environments) {
24
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
25
+ await fs.writeFile(
26
+ path.resolve(backupDir, `${environment.name}.json`),
27
+ JSON.stringify(environment, undefined, 2),
28
+ );
29
+ }
30
+
31
+ if (fmt) {
32
+ await formatUncommittedFiles();
33
+ }
34
+ };
35
+
36
+ if (isDirectlyExecuted(import.meta.url)) {
37
+ await backupEnvironments();
38
+ }
@@ -0,0 +1,78 @@
1
+ import * as t from 'ts-fortress';
2
+
3
+ /**
4
+ * `repo-settings/environments/*.json` の型。
5
+ *
6
+ * Settings > Environments の 1 件に対応する。ファイルは「その環境の望ましい
7
+ * 状態」を丸ごと表しており、 GitHub 側で 2 つに分かれている次の API をまとめて
8
+ * 1 ファイルで管理する:
9
+ *
10
+ * - `PUT /repos/{owner}/{repo}/environments/{environment_name}`
11
+ * - `GET|POST|DELETE /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies`
12
+ *
13
+ * GET が返す `protection_rules` は「有効な保護ルールだけが並ぶ配列」だが、この
14
+ * 型は書き込み側( PUT の body )に合わせた平らな形にしている。往復させる側の
15
+ * 形を正とするほうが、ファイルを読んで何が起きるか分かりやすいため。
16
+ */
17
+ export const EnvironmentSettings = t.record({
18
+ name: t.string(''),
19
+
20
+ /** 「 Wait timer 」。分。 0 なら待たない。 */
21
+ wait_timer: t.number(0),
22
+
23
+ /** 「 Prevent self-review 」。 */
24
+ prevent_self_review: t.boolean(false),
25
+
26
+ /** 「 Required reviewers 」。空配列なら承認は要らない。 */
27
+ reviewers: t.array(
28
+ t.record({
29
+ type: t.enumType(['User', 'Team'], {
30
+ typeName: 'EnvironmentReviewerType',
31
+ defaultValue: 'User',
32
+ }),
33
+ id: t.number(0),
34
+ }),
35
+ { typeName: 'EnvironmentReviewers' },
36
+ ),
37
+
38
+ /**
39
+ * 「 Deployment branches and tags 」の選択。
40
+ *
41
+ * - `null` … 「 No restriction 」( どの ref からでも deploy できる )
42
+ * - `protected_branches: true` … 「 Protected branches only 」
43
+ * - `custom_branch_policies: true` … 「 Selected branches and tags 」。
44
+ * 具体的な pattern は下の `deployment_branch_policies` 。
45
+ *
46
+ * 2 つを同時に `true` にはできない。
47
+ */
48
+ deployment_branch_policy: t.union(
49
+ [
50
+ t.record({
51
+ protected_branches: t.boolean(false),
52
+ custom_branch_policies: t.boolean(true),
53
+ }),
54
+ t.nullType,
55
+ ],
56
+ { typeName: 'DeploymentBranchPolicySelection' },
57
+ ),
58
+
59
+ /**
60
+ * `custom_branch_policies` が `true` のときの pattern 一覧。
61
+ *
62
+ * それ以外のときは空配列でなければならない ( `applyEnvironments` が拒否する )。
63
+ * GitHub 側は別エンドポイントだが、「どの ref から deploy できるか」を
64
+ * 決めているのは選択と pattern の組で、片方だけ見ても意味を成さない。
65
+ */
66
+ deployment_branch_policies: t.array(
67
+ t.record({
68
+ name: t.string(''),
69
+ type: t.enumType(['branch', 'tag'], {
70
+ typeName: 'DeploymentBranchPolicyType',
71
+ defaultValue: 'branch',
72
+ }),
73
+ }),
74
+ { typeName: 'DeploymentBranchPolicies' },
75
+ ),
76
+ });
77
+
78
+ export type EnvironmentSettings = t.TypeOf<typeof EnvironmentSettings>;
@@ -0,0 +1,5 @@
1
+ export * from './api/index.mjs';
2
+ export * from './apply.mjs';
3
+ export * from './backup.mjs';
4
+ export * from './constants.mjs';
5
+ export * from './read-environment-contents.mjs';
@@ -0,0 +1,53 @@
1
+ import { type Dirent } from 'node:fs';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as t from 'ts-fortress';
5
+ import { isDirectlyExecuted, Result } from 'ts-repo-utils';
6
+ import { environmentsDir } from '../constants.mjs';
7
+ import { EnvironmentSettings } from './constants.mjs';
8
+
9
+ export const readEnvironmentFiles = async (): Promise<
10
+ readonly EnvironmentSettings[]
11
+ > => readFilesIn(environmentsDir);
12
+
13
+ export const readEnvironmentBackupFiles = async (): Promise<
14
+ readonly EnvironmentSettings[]
15
+ > => readFilesIn(path.resolve(environmentsDir, './bk'));
16
+
17
+ const readFilesIn = async (
18
+ dir: string,
19
+ ): Promise<readonly EnvironmentSettings[]> => {
20
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
21
+ const files: readonly Dirent<string>[] = await fs.readdir(dir, {
22
+ withFileTypes: true,
23
+ });
24
+
25
+ const fileContents: readonly string[] = await Promise.all(
26
+ files
27
+ .filter((d) => d.isFile())
28
+ .map((d) =>
29
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
30
+ fs.readFile(path.resolve(dir, d.name), { encoding: 'utf8' }),
31
+ ),
32
+ );
33
+
34
+ const validationResults = fileContents.map((content) =>
35
+ EnvironmentSettings.validate(JSON.parse(content)),
36
+ );
37
+
38
+ const mut_values: EnvironmentSettings[] = [];
39
+
40
+ for (const r of validationResults) {
41
+ if (Result.isErr(r)) {
42
+ throw new Error(t.validationErrorsToMessages(r.value).join('\n'));
43
+ }
44
+
45
+ mut_values.push(r.value);
46
+ }
47
+
48
+ return mut_values;
49
+ };
50
+
51
+ if (isDirectlyExecuted(import.meta.url)) {
52
+ await readEnvironmentFiles();
53
+ }
@@ -1,7 +1,9 @@
1
1
  export * from './actions-settings/index.mjs';
2
2
  export * from './constants.mjs';
3
+ export * from './environments/index.mjs';
3
4
  export * from './pages/index.mjs';
4
5
  export * from './repository/index.mjs';
5
6
  export * from './resolve-target-repo.mjs';
6
7
  export * from './ruleset/index.mjs';
7
8
  export * from './variables/index.mjs';
9
+ export * from './vulnerability-alerts/index.mjs';
@@ -0,0 +1,31 @@
1
+ import { type EndpointKeys } from 'octokit-safe-types';
2
+ import { hasKey, isRecord } from 'ts-data-forge';
3
+ import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
4
+ import { octokit } from '../../octokit.mjs';
5
+ import { VulnerabilityAlertsSettings } from '../constants.mjs';
6
+
7
+ /**
8
+ * Dependabot alerts が有効かどうかを取得する。
9
+ *
10
+ * このエンドポイントは body を返さない: 有効なら 204、無効なら 404。
11
+ * octokit は 404 を例外にするので、それを `enabled: false` として扱う。
12
+ * 「無効」と「読めなかった」は区別する必要があるため、 404 以外は投げ直す。
13
+ */
14
+ export const getVulnerabilityAlerts =
15
+ async (): Promise<VulnerabilityAlertsSettings> => {
16
+ try {
17
+ // https://docs.github.com/rest/repos/repos#check-if-vulnerability-alerts-are-enabled-for-a-repository
18
+ await octokit.request(
19
+ 'GET /repos/{owner}/{repo}/vulnerability-alerts' satisfies EndpointKeys,
20
+ { owner: OWNER, repo: REPO, headers: octokitHeaders },
21
+ );
22
+
23
+ return VulnerabilityAlertsSettings.fill({ enabled: true });
24
+ } catch (error) {
25
+ if (isRecord(error) && hasKey(error, 'status') && error.status === 404) {
26
+ return VulnerabilityAlertsSettings.fill({ enabled: false });
27
+ }
28
+
29
+ throw error;
30
+ }
31
+ };
@@ -0,0 +1,2 @@
1
+ export * from './get-vulnerability-alerts.mjs';
2
+ export * from './set-vulnerability-alerts.mjs';
@@ -0,0 +1,31 @@
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 VulnerabilityAlertsSettings } from '../constants.mjs';
5
+
6
+ /**
7
+ * Dependabot alerts を JSON の内容に合わせる。
8
+ *
9
+ * 有効化と無効化で別メソッドなので、書き込みは 1 回で済むように分岐する。
10
+ * `enabled: false` はきちんと無効化する — 宣言が現状に合わせて黙るなら、
11
+ * ファイルを読んでも何が有効なのか分からなくなる。
12
+ */
13
+ export const setVulnerabilityAlerts = async (
14
+ settings: VulnerabilityAlertsSettings,
15
+ ): Promise<void> => {
16
+ if (settings.enabled) {
17
+ // https://docs.github.com/rest/repos/repos#enable-vulnerability-alerts
18
+ await octokit.request(
19
+ 'PUT /repos/{owner}/{repo}/vulnerability-alerts' satisfies EndpointKeys,
20
+ { owner: OWNER, repo: REPO, headers: octokitHeaders },
21
+ );
22
+
23
+ return;
24
+ }
25
+
26
+ // https://docs.github.com/rest/repos/repos#disable-vulnerability-alerts
27
+ await octokit.request(
28
+ 'DELETE /repos/{owner}/{repo}/vulnerability-alerts' satisfies EndpointKeys,
29
+ { owner: OWNER, repo: REPO, headers: octokitHeaders },
30
+ );
31
+ };
@@ -0,0 +1,77 @@
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 {
7
+ formatUncommittedFiles,
8
+ isDirectlyExecuted,
9
+ pathExists,
10
+ Result,
11
+ } from 'ts-repo-utils';
12
+ import { settingsJsonName, vulnerabilityAlertsDir } from '../constants.mjs';
13
+ import {
14
+ getVulnerabilityAlerts,
15
+ setVulnerabilityAlerts,
16
+ } from './api/index.mjs';
17
+ import { VulnerabilityAlertsSettings } from './constants.mjs';
18
+
19
+ /**
20
+ * `repo-settings/vulnerability-alerts/settings.json` を Dependabot alerts に反映する。
21
+ *
22
+ * 設定ファイルを置いていない repository では何もしない。 pages と同じ理由 —
23
+ * この package は他の repository からも使われるので、ファイルの不在を
24
+ * 「無効にせよ」と読むと、宣言していない設定を勝手に落としてしまう。
25
+ */
26
+ export const applyVulnerabilityAlerts = async (): Promise<void> => {
27
+ const settingsPath = path.resolve(vulnerabilityAlertsDir, settingsJsonName);
28
+
29
+ if (!(await pathExists(settingsPath))) {
30
+ return;
31
+ }
32
+
33
+ const settings = await readSettings(settingsPath);
34
+
35
+ await setVulnerabilityAlerts(settings);
36
+
37
+ // 反映後の実際の値でローカルのファイルを更新する
38
+ {
39
+ const applied = await getVulnerabilityAlerts();
40
+
41
+ const str = JSON.stringify(applied, undefined, 2);
42
+
43
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
44
+ await fs.writeFile(settingsPath, str);
45
+
46
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
47
+ await fs.writeFile(
48
+ path.resolve(vulnerabilityAlertsDir, 'bk', settingsJsonName),
49
+ str,
50
+ );
51
+
52
+ await formatUncommittedFiles();
53
+ }
54
+ };
55
+
56
+ const readSettings = async (
57
+ settingsPath: string,
58
+ ): Promise<VulnerabilityAlertsSettings> => {
59
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
60
+ const settingsText = await fs.readFile(settingsPath, { encoding: 'utf8' });
61
+
62
+ const validationResult = VulnerabilityAlertsSettings.validate(
63
+ JSON.parse(settingsText),
64
+ );
65
+
66
+ if (Result.isErr(validationResult)) {
67
+ throw new Error(
68
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
69
+ );
70
+ }
71
+
72
+ return validationResult.value;
73
+ };
74
+
75
+ if (isDirectlyExecuted(import.meta.url)) {
76
+ await applyVulnerabilityAlerts();
77
+ }
@@ -0,0 +1,36 @@
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 {
6
+ formatUncommittedFiles,
7
+ isDirectlyExecuted,
8
+ makeEmptyDir,
9
+ } from 'ts-repo-utils';
10
+ import { settingsJsonName, vulnerabilityAlertsDir } from '../constants.mjs';
11
+ import { getVulnerabilityAlerts } from './api/index.mjs';
12
+
13
+ const backupDir = path.resolve(vulnerabilityAlertsDir, './bk');
14
+
15
+ /** Dependabot alerts の現在値を `bk/` に保存する。 */
16
+ export const backupVulnerabilityAlerts = async (
17
+ fmt: boolean = true,
18
+ ): Promise<void> => {
19
+ await makeEmptyDir(backupDir);
20
+
21
+ const settings = await getVulnerabilityAlerts();
22
+
23
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
24
+ await fs.writeFile(
25
+ path.resolve(backupDir, settingsJsonName),
26
+ JSON.stringify(settings, undefined, 2),
27
+ );
28
+
29
+ if (fmt) {
30
+ await formatUncommittedFiles();
31
+ }
32
+ };
33
+
34
+ if (isDirectlyExecuted(import.meta.url)) {
35
+ await backupVulnerabilityAlerts();
36
+ }
@@ -0,0 +1,25 @@
1
+ import * as t from 'ts-fortress';
2
+
3
+ /**
4
+ * `repo-settings/vulnerability-alerts/settings.json` の型。
5
+ *
6
+ * Settings > Advanced Security > Dependabot alerts に対応する。
7
+ *
8
+ * `security_and_analysis` とは別の API
9
+ * (`GET|PUT|DELETE /repos/{owner}/{repo}/vulnerability-alerts`) で、
10
+ * 値も JSON body ではなく 204 / 404 で返ってくる。そのため
11
+ * `repository-settings/settings.json` には載せられず、 pages と同じく
12
+ * 独立した設定ファイルにしている。
13
+ *
14
+ * `repository-settings/settings.json` の `dependabot_security_updates` は
15
+ * これとは別物 — あちらは「脆弱な依存を直す PR を自動で作るか」で、
16
+ * この repository では `pnpm-update` が lockfile を持っているため無効のまま。
17
+ * ここで管理するのは「既知の脆弱性を知らせるか」だけ。
18
+ */
19
+ export const VulnerabilityAlertsSettings = t.record({
20
+ enabled: t.boolean(false),
21
+ });
22
+
23
+ export type VulnerabilityAlertsSettings = t.TypeOf<
24
+ typeof VulnerabilityAlertsSettings
25
+ >;
@@ -0,0 +1,4 @@
1
+ export * from './api/index.mjs';
2
+ export * from './apply.mjs';
3
+ export * from './backup.mjs';
4
+ export * from './constants.mjs';