github-settings-as-code 3.1.0 → 4.0.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 (170) hide show
  1. package/README.md +61 -39
  2. package/dist/cmd/repo-settings.mjs +17 -2
  3. package/dist/cmd/repo-settings.mjs.map +1 -1
  4. package/dist/github/actions-settings/apply.d.mts.map +1 -1
  5. package/dist/github/actions-settings/apply.mjs +0 -4
  6. package/dist/github/actions-settings/apply.mjs.map +1 -1
  7. package/dist/github/actions-settings/backup.d.mts +1 -1
  8. package/dist/github/actions-settings/backup.d.mts.map +1 -1
  9. package/dist/github/actions-settings/backup.mjs +4 -4
  10. package/dist/github/actions-settings/backup.mjs.map +1 -1
  11. package/dist/github/actions-settings/constants.d.mts +1 -1
  12. package/dist/github/clear-json-files.d.mts +17 -0
  13. package/dist/github/clear-json-files.d.mts.map +1 -0
  14. package/dist/github/clear-json-files.mjs +29 -0
  15. package/dist/github/clear-json-files.mjs.map +1 -0
  16. package/dist/github/constants.d.mts +3 -0
  17. package/dist/github/constants.d.mts.map +1 -1
  18. package/dist/github/constants.mjs +3 -0
  19. package/dist/github/constants.mjs.map +1 -1
  20. package/dist/github/environments/api/get-environments.d.mts +16 -0
  21. package/dist/github/environments/api/get-environments.d.mts.map +1 -0
  22. package/dist/github/environments/api/get-environments.mjs +98 -0
  23. package/dist/github/environments/api/get-environments.mjs.map +1 -0
  24. package/dist/github/environments/api/index.d.mts +3 -0
  25. package/dist/github/environments/api/index.d.mts.map +1 -0
  26. package/dist/github/environments/api/index.mjs +3 -0
  27. package/dist/github/environments/api/index.mjs.map +1 -0
  28. package/dist/github/environments/api/set-environment.d.mts +10 -0
  29. package/dist/github/environments/api/set-environment.d.mts.map +1 -0
  30. package/dist/github/environments/api/set-environment.mjs +73 -0
  31. package/dist/github/environments/api/set-environment.mjs.map +1 -0
  32. package/dist/github/environments/apply.d.mts +11 -0
  33. package/dist/github/environments/apply.d.mts.map +1 -0
  34. package/dist/github/environments/apply.mjs +63 -0
  35. package/dist/github/environments/apply.mjs.map +1 -0
  36. package/dist/github/environments/backup.d.mts +10 -0
  37. package/dist/github/environments/backup.d.mts.map +1 -0
  38. package/dist/github/environments/backup.mjs +30 -0
  39. package/dist/github/environments/backup.mjs.map +1 -0
  40. package/dist/github/environments/constants.d.mts +354 -0
  41. package/dist/github/environments/constants.d.mts.map +1 -0
  42. package/dist/github/environments/constants.mjs +62 -0
  43. package/dist/github/environments/constants.mjs.map +1 -0
  44. package/dist/github/environments/index.d.mts +6 -0
  45. package/dist/github/environments/index.d.mts.map +1 -0
  46. package/dist/github/environments/index.mjs +6 -0
  47. package/dist/github/environments/index.mjs.map +1 -0
  48. package/dist/github/environments/read-environment-contents.d.mts +3 -0
  49. package/dist/github/environments/read-environment-contents.d.mts.map +1 -0
  50. package/dist/github/environments/read-environment-contents.mjs +31 -0
  51. package/dist/github/environments/read-environment-contents.mjs.map +1 -0
  52. package/dist/github/index.d.mts +4 -0
  53. package/dist/github/index.d.mts.map +1 -1
  54. package/dist/github/index.mjs +4 -0
  55. package/dist/github/index.mjs.map +1 -1
  56. package/dist/github/pages/apply.d.mts.map +1 -1
  57. package/dist/github/pages/apply.mjs +0 -2
  58. package/dist/github/pages/apply.mjs.map +1 -1
  59. package/dist/github/pages/backup.d.mts +11 -1
  60. package/dist/github/pages/backup.d.mts.map +1 -1
  61. package/dist/github/pages/backup.mjs +18 -5
  62. package/dist/github/pages/backup.mjs.map +1 -1
  63. package/dist/github/repository/apply.d.mts.map +1 -1
  64. package/dist/github/repository/apply.mjs +0 -4
  65. package/dist/github/repository/apply.mjs.map +1 -1
  66. package/dist/github/repository/backup.d.mts.map +1 -1
  67. package/dist/github/repository/backup.mjs +4 -4
  68. package/dist/github/repository/backup.mjs.map +1 -1
  69. package/dist/github/ruleset/apply.d.mts.map +1 -1
  70. package/dist/github/ruleset/apply.mjs +9 -9
  71. package/dist/github/ruleset/apply.mjs.map +1 -1
  72. package/dist/github/ruleset/backup.d.mts +7 -0
  73. package/dist/github/ruleset/backup.d.mts.map +1 -1
  74. package/dist/github/ruleset/backup.mjs +20 -7
  75. package/dist/github/ruleset/backup.mjs.map +1 -1
  76. package/dist/github/ruleset/read-rule-set-contents.d.mts +0 -1
  77. package/dist/github/ruleset/read-rule-set-contents.d.mts.map +1 -1
  78. package/dist/github/ruleset/read-rule-set-contents.mjs +0 -1
  79. package/dist/github/ruleset/read-rule-set-contents.mjs.map +1 -1
  80. package/dist/github/settings-file-path.d.mts +14 -0
  81. package/dist/github/settings-file-path.d.mts.map +1 -0
  82. package/dist/github/settings-file-path.mjs +41 -0
  83. package/dist/github/settings-file-path.mjs.map +1 -0
  84. package/dist/github/variables/api/list-repo-variables.d.mts.map +1 -1
  85. package/dist/github/variables/api/list-repo-variables.mjs +3 -1
  86. package/dist/github/variables/api/list-repo-variables.mjs.map +1 -1
  87. package/dist/github/variables/apply.d.mts +9 -0
  88. package/dist/github/variables/apply.d.mts.map +1 -1
  89. package/dist/github/variables/apply.mjs +31 -17
  90. package/dist/github/variables/apply.mjs.map +1 -1
  91. package/dist/github/variables/backup.d.mts +20 -0
  92. package/dist/github/variables/backup.d.mts.map +1 -0
  93. package/dist/github/variables/backup.mjs +35 -0
  94. package/dist/github/variables/backup.mjs.map +1 -0
  95. package/dist/github/variables/constants.d.mts +65 -0
  96. package/dist/github/variables/constants.d.mts.map +1 -0
  97. package/dist/github/variables/constants.mjs +48 -0
  98. package/dist/github/variables/constants.mjs.map +1 -0
  99. package/dist/github/variables/index.d.mts +2 -0
  100. package/dist/github/variables/index.d.mts.map +1 -1
  101. package/dist/github/variables/index.mjs +2 -0
  102. package/dist/github/variables/index.mjs.map +1 -1
  103. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts +10 -0
  104. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts.map +1 -0
  105. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs +25 -0
  106. package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs.map +1 -0
  107. package/dist/github/vulnerability-alerts/api/index.d.mts +3 -0
  108. package/dist/github/vulnerability-alerts/api/index.d.mts.map +1 -0
  109. package/dist/github/vulnerability-alerts/api/index.mjs +3 -0
  110. package/dist/github/vulnerability-alerts/api/index.mjs.map +1 -0
  111. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts +10 -0
  112. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts.map +1 -0
  113. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs +19 -0
  114. package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs.map +1 -0
  115. package/dist/github/vulnerability-alerts/apply.d.mts +11 -0
  116. package/dist/github/vulnerability-alerts/apply.d.mts.map +1 -0
  117. package/dist/github/vulnerability-alerts/apply.mjs +45 -0
  118. package/dist/github/vulnerability-alerts/apply.mjs.map +1 -0
  119. package/dist/github/vulnerability-alerts/backup.d.mts +5 -0
  120. package/dist/github/vulnerability-alerts/backup.d.mts.map +1 -0
  121. package/dist/github/vulnerability-alerts/backup.mjs +22 -0
  122. package/dist/github/vulnerability-alerts/backup.mjs.map +1 -0
  123. package/dist/github/vulnerability-alerts/constants.d.mts +72 -0
  124. package/dist/github/vulnerability-alerts/constants.d.mts.map +1 -0
  125. package/dist/github/vulnerability-alerts/constants.mjs +21 -0
  126. package/dist/github/vulnerability-alerts/constants.mjs.map +1 -0
  127. package/dist/github/vulnerability-alerts/index.d.mts +5 -0
  128. package/dist/github/vulnerability-alerts/index.d.mts.map +1 -0
  129. package/dist/github/vulnerability-alerts/index.mjs +5 -0
  130. package/dist/github/vulnerability-alerts/index.mjs.map +1 -0
  131. package/package.json +29 -28
  132. package/src/cmd/repo-settings.mts +22 -2
  133. package/src/github/actions-settings/apply.mts +0 -9
  134. package/src/github/actions-settings/backup.mts +6 -11
  135. package/src/github/actions-settings/constants.mts +1 -1
  136. package/src/github/clear-json-files.mts +31 -0
  137. package/src/github/clear-json-files.test.mts +78 -0
  138. package/src/github/constants.mts +9 -0
  139. package/src/github/environments/api/get-environments.mts +155 -0
  140. package/src/github/environments/api/index.mts +2 -0
  141. package/src/github/environments/api/set-environment.mts +106 -0
  142. package/src/github/environments/apply.mts +82 -0
  143. package/src/github/environments/backup.mts +39 -0
  144. package/src/github/environments/constants.mts +78 -0
  145. package/src/github/environments/index.mts +5 -0
  146. package/src/github/environments/read-environment-contents.mts +49 -0
  147. package/src/github/index.mts +4 -0
  148. package/src/github/pages/apply.mts +0 -6
  149. package/src/github/pages/backup.mts +21 -12
  150. package/src/github/repository/apply.mts +0 -9
  151. package/src/github/repository/backup.mts +5 -10
  152. package/src/github/ruleset/apply.mts +10 -18
  153. package/src/github/ruleset/backup.mts +23 -13
  154. package/src/github/ruleset/read-rule-set-contents.mts +0 -4
  155. package/src/github/ruleset/schema.test.mts +0 -13
  156. package/src/github/settings-file-path.mts +51 -0
  157. package/src/github/settings-file-path.test.mts +70 -0
  158. package/src/github/variables/api/list-repo-variables.mts +3 -1
  159. package/src/github/variables/apply.mts +54 -27
  160. package/src/github/variables/backup.mts +47 -0
  161. package/src/github/variables/constants.mts +61 -0
  162. package/src/github/variables/constants.test.mts +110 -0
  163. package/src/github/variables/index.mts +2 -0
  164. package/src/github/vulnerability-alerts/api/get-vulnerability-alerts.mts +31 -0
  165. package/src/github/vulnerability-alerts/api/index.mts +2 -0
  166. package/src/github/vulnerability-alerts/api/set-vulnerability-alerts.mts +31 -0
  167. package/src/github/vulnerability-alerts/apply.mts +71 -0
  168. package/src/github/vulnerability-alerts/backup.mts +31 -0
  169. package/src/github/vulnerability-alerts/constants.mts +25 -0
  170. package/src/github/vulnerability-alerts/index.mts +4 -0
@@ -23,5 +23,7 @@ export const listRepoVariables = async (): Promise<
23
23
  },
24
24
  );
25
25
 
26
- return result.data.variables;
26
+ // `getAllEnvironments` と同じく名前順に整えて返す。宣言ファイルの中身が API
27
+ // の返す順に左右されると、値が変わっていない日にも diff が出る。
28
+ return result.data.variables.toSorted((a, b) => a.name.localeCompare(b.name));
27
29
  };
@@ -1,43 +1,70 @@
1
1
  #!/usr/bin/env node
2
-
3
- // cspell:ignore lilpi
4
2
  import 'dotenv/config';
5
- import { isDirectlyExecuted } from 'ts-repo-utils';
3
+ import * as fs from 'node:fs/promises';
4
+ import * as path from 'node:path';
5
+ import * as t from 'ts-fortress';
6
+ import { isDirectlyExecuted, Result } from 'ts-repo-utils';
7
+ import { settingsJsonName, variablesDir } from '../constants.mjs';
6
8
  import {
7
9
  createRepoVariable,
8
10
  listRepoVariables,
9
11
  updateRepoVariable,
10
12
  } from './api/index.mjs';
13
+ import {
14
+ assertVariableNamesAreValid,
15
+ RepositoryVariables,
16
+ } from './constants.mjs';
11
17
 
18
+ /**
19
+ * `repo-settings/variables/settings.json` を Settings > Secrets and variables >
20
+ * Actions > Variables に反映する。
21
+ *
22
+ * ruleset や environment と同じく、**宣言に無い変数は消さない。** 消したい
23
+ * ときは宣言から外したうえで GitHub 側でも消す。 apply が片付けてしまうと、
24
+ * 誰かが GUI で足したものが「いつの間にか無くなる」ことになり、drift 検査が
25
+ * 報せるという見え方が働かなくなる。
26
+ */
12
27
  export const applyVariables = async (): Promise<void> => {
13
- const variables = [
14
- {
15
- // noshiro-repo-settings-bot (App ID: 4476573)
16
- // Administration を含む強い権限を持つ。設定の backup / apply 専用。
17
- name: 'REPO_SETTINGS_BOT_CLIENT_ID',
18
- value: 'Iv23ctr4Lq9H3wOYRQrD',
19
- },
20
- {
21
- // noshiro-repo-automation-bot (App ID: 4478721)
22
- // Contents / Pull requests / Issues 権限。PR を作る自動化( pnpm-update )と
23
- // release で共用する。実際に使う権限は各ワークフローの permission-* 入力で
24
- // 絞る。
25
- name: 'REPO_AUTOMATION_BOT_CLIENT_ID',
26
- value: 'Iv23lilpiDh0twq2PErv',
27
- },
28
- ] as const;
29
-
30
- const variableListSaved = await listRepoVariables();
31
-
32
- const variablesSaved: ReadonlySet<string> = new Set(
33
- variableListSaved.map((v) => v.name),
28
+ const variables = await readSettings();
29
+
30
+ // 1 本目を送る前に、全部の名前を見る。
31
+ assertVariableNamesAreValid(variables);
32
+
33
+ const existing: ReadonlySet<string> = await listRepoVariables().then(
34
+ (vs) => new Set(vs.map((v) => v.name)),
35
+ );
36
+
37
+ for (const [name, value] of Object.entries(variables)) {
38
+ await (existing.has(name)
39
+ ? updateRepoVariable({ name, value })
40
+ : createRepoVariable({ name, value }));
41
+ }
42
+
43
+ // ここで live を読み直して書き戻すことはしない。 backup は live をそのまま
44
+ // 宣言に写す操作で、 live は宣言の上位集合になりうる — apply の締めに呼ぶと、
45
+ // 宣言していない変数まで黙って宣言に入る。送った値は宣言そのものなので、
46
+ // 書き戻して得られるものも無い。現在値を取り込みたいときは backup を明示的に
47
+ // 実行する。
48
+ };
49
+
50
+ const readSettings = async (): Promise<RepositoryVariables> => {
51
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
52
+ const settingsText = await fs.readFile(
53
+ path.resolve(variablesDir, settingsJsonName),
54
+ { encoding: 'utf8' },
34
55
  );
35
56
 
36
- for (const variable of variables) {
37
- const found = variablesSaved.has(variable.name);
57
+ const validationResult = RepositoryVariables.validate(
58
+ JSON.parse(settingsText),
59
+ );
38
60
 
39
- await (found ? updateRepoVariable(variable) : createRepoVariable(variable));
61
+ if (Result.isErr(validationResult)) {
62
+ throw new Error(
63
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
64
+ );
40
65
  }
66
+
67
+ return validationResult.value;
41
68
  };
42
69
 
43
70
  if (isDirectlyExecuted(import.meta.url)) {
@@ -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 { formatUncommittedFiles, isDirectlyExecuted } from 'ts-repo-utils';
6
+ import { clearJsonFilesIn } from '../clear-json-files.mjs';
7
+ import { settingsJsonName, variablesDir } from '../constants.mjs';
8
+ import { listRepoVariables } from './api/index.mjs';
9
+ import { type RepositoryVariables } from './constants.mjs';
10
+
11
+ /**
12
+ * repository variable の現在値を宣言ファイルへ撮り直す。
13
+ *
14
+ * 宣言していないものも含めて live にあるものを全部書く。 backup は「いま何が
15
+ * あるか」を写す操作なので、 GUI で足された変数もここで宣言に入る。要らない
16
+ * ものは撮り直したあとに宣言から外し、 GitHub 側でも消す。
17
+ */
18
+ export const backupVariables = async (fmt: boolean = true): Promise<void> => {
19
+ const variables = await listRepoVariables();
20
+
21
+ await clearJsonFilesIn(variablesDir);
22
+
23
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
24
+ await fs.writeFile(
25
+ path.resolve(variablesDir, settingsJsonName),
26
+ JSON.stringify(toDeclaration(variables), undefined, 2),
27
+ );
28
+
29
+ if (fmt) {
30
+ await formatUncommittedFiles();
31
+ }
32
+ };
33
+
34
+ /**
35
+ * API の配列を `settings.json` と同じ record に畳む。
36
+ *
37
+ * `created_at` / `updated_at` はここで落ちる。値が変わっていなくても動くので、
38
+ * 残すと宣言ファイルが毎日変わり、ドリフト検査が本当の変更を報せなくなる。
39
+ */
40
+ export const toDeclaration = (
41
+ variables: Awaited<ReturnType<typeof listRepoVariables>>,
42
+ ): RepositoryVariables =>
43
+ Object.fromEntries(variables.map(({ name, value }) => [name, value]));
44
+
45
+ if (isDirectlyExecuted(import.meta.url)) {
46
+ await backupVariables();
47
+ }
@@ -0,0 +1,61 @@
1
+ import { Arr } from 'ts-data-forge';
2
+ import * as t from 'ts-fortress';
3
+
4
+ /**
5
+ * `repo-settings/variables/settings.json` の型。
6
+ *
7
+ * Settings > Secrets and variables > Actions > Variables に対応する
8
+ * (`GET|POST /repos/{owner}/{repo}/actions/variables`,
9
+ * `PATCH /repos/{owner}/{repo}/actions/variables/{name}`)。
10
+ *
11
+ * 変数名をキー、値を値にした素の record にしてある。 API が返す配列
12
+ * (`{ name, value, created_at, updated_at }[]`) をそのまま写さないのは、
13
+ * `created_at` / `updated_at` が値の変化なしに動くためで、宣言に入れると
14
+ * 毎日ドリフトとして出てしまう。ここで持つのは宣言できるものだけ。
15
+ *
16
+ * **ここに秘密を書かないこと。** repository variable は API でも Actions の
17
+ * ログでも平文で読める。秘密は secret 側で、そちらはこのツールの管理外にある。
18
+ */
19
+ export const RepositoryVariables = t.keyValueRecord(t.string(), t.string(), {
20
+ typeName: 'RepositoryVariables',
21
+ });
22
+
23
+ export type RepositoryVariables = t.TypeOf<typeof RepositoryVariables>;
24
+
25
+ /**
26
+ * GitHub が受け付けない変数名を、送る前にすべて挙げて落とす。
27
+ *
28
+ * 名前は `PATCH .../actions/variables/{name}` のパスに入る。 octokit が URL
29
+ * エンコードするので注入にはならないが、弾かれる名前は 422 になり、それが
30
+ * 起きるのは**何本か書き込んだ後**である。 environments の `assertConsistent`
31
+ * と同じ理由でここに置いてある — 1 本目を送る前に全部の名前を見ておけば、
32
+ * この種の間違いで中途半端に適用された状態にはならない。
33
+ *
34
+ * 規則は GitHub の documentation のとおり: 英数字と `_` のみ、先頭に数字は
35
+ * 不可、`GITHUB_` 始まりは予約。名前の大文字小文字は区別されないので、
36
+ * 予約接頭辞の判定も区別しない。
37
+ */
38
+ export const assertVariableNamesAreValid = (
39
+ variables: RepositoryVariables,
40
+ ): void => {
41
+ const offenders = Object.keys(variables).filter(
42
+ (name) => !isValidVariableName(name),
43
+ );
44
+
45
+ if (Arr.isNonEmpty(offenders)) {
46
+ throw new Error(
47
+ [
48
+ `repository variable の名前として使えないものがあります: ${offenders
49
+ .map((name) => JSON.stringify(name))
50
+ .join(', ')}。`,
51
+ '英数字と "_" のみが使え、先頭に数字は置けず、"GITHUB_" 始まりは予約されています。',
52
+ ].join('\n'),
53
+ );
54
+ }
55
+ };
56
+
57
+ /** 先頭は英字か `_`、以降は英数字か `_`。入れ子の量化子は無いので線形。 */
58
+ const variableNamePattern = /^[A-Za-z_][A-Za-z0-9_]*$/u;
59
+
60
+ const isValidVariableName = (name: string): boolean =>
61
+ variableNamePattern.test(name) && !name.toUpperCase().startsWith('GITHUB_');
@@ -0,0 +1,110 @@
1
+ import { Result } from 'ts-data-forge';
2
+ import {
3
+ assertVariableNamesAreValid,
4
+ RepositoryVariables,
5
+ } from './constants.mjs';
6
+
7
+ /** `'rejected'` when the names were refused, `'accepted'` when they were not. */
8
+ const outcomeOf = (variables: RepositoryVariables): string =>
9
+ Result.isErr(
10
+ Result.fromThrowable(() => {
11
+ assertVariableNamesAreValid(variables);
12
+ }),
13
+ )
14
+ ? 'rejected'
15
+ : 'accepted';
16
+
17
+ describe('the repository-variables schema', () => {
18
+ test('accepts a declaration of names to values', () => {
19
+ const result = RepositoryVariables.validate({
20
+ REPO_AUTOMATION_BOT_CLIENT_ID: 'Iv1example0client0id',
21
+ });
22
+
23
+ assert.isTrue(Result.isOk(result));
24
+
25
+ if (Result.isOk(result)) {
26
+ assert.deepStrictEqual(result.value, {
27
+ REPO_AUTOMATION_BOT_CLIENT_ID: 'Iv1example0client0id',
28
+ });
29
+ }
30
+ });
31
+
32
+ test('accepts an empty declaration', () => {
33
+ assert.isTrue(Result.isOk(RepositoryVariables.validate({})));
34
+ });
35
+
36
+ test('rejects a value that is not a string', () => {
37
+ // A variable's value is always a string on GitHub's side. A number here
38
+ // would be sent as one and read back as `"4476573"`, which reads as drift
39
+ // on the next backup.
40
+ assert.isTrue(
41
+ Result.isErr(RepositoryVariables.validate({ A_NUMBER: 123 })),
42
+ );
43
+
44
+ assert.isTrue(Result.isErr(RepositoryVariables.validate({ A_NULL: null })));
45
+ });
46
+
47
+ test('rejects a declaration that is not a record', () => {
48
+ assert.isTrue(Result.isErr(RepositoryVariables.validate([])));
49
+
50
+ assert.isTrue(Result.isErr(RepositoryVariables.validate('')));
51
+
52
+ assert.isTrue(Result.isErr(RepositoryVariables.validate(null)));
53
+ });
54
+ });
55
+
56
+ describe(assertVariableNamesAreValid, () => {
57
+ test('accepts the names GitHub accepts', () => {
58
+ assert.deepStrictEqual(
59
+ outcomeOf({
60
+ REPO_AUTOMATION_BOT_CLIENT_ID: 'x',
61
+ lowercase_is_allowed: 'x',
62
+ _LEADING_UNDERSCORE: 'x',
63
+ MIXED_Case_9: 'x',
64
+ }),
65
+ 'accepted',
66
+ );
67
+ });
68
+
69
+ test('rejects a name starting with a digit', () => {
70
+ assert.deepStrictEqual(outcomeOf({ '1ST_PLACE': 'x' }), 'rejected');
71
+ });
72
+
73
+ test('rejects a name carrying a character GitHub does not allow', () => {
74
+ assert.deepStrictEqual(outcomeOf({ 'WITH-HYPHEN': 'x' }), 'rejected');
75
+
76
+ assert.deepStrictEqual(outcomeOf({ 'WITH SPACE': 'x' }), 'rejected');
77
+
78
+ assert.deepStrictEqual(outcomeOf({ 'WITH.DOT': 'x' }), 'rejected');
79
+ });
80
+
81
+ test('rejects the reserved GITHUB_ prefix, in any case', () => {
82
+ assert.deepStrictEqual(outcomeOf({ GITHUB_TOKEN: 'x' }), 'rejected');
83
+
84
+ assert.deepStrictEqual(outcomeOf({ github_token: 'x' }), 'rejected');
85
+ });
86
+
87
+ test('rejects an empty name', () => {
88
+ assert.deepStrictEqual(outcomeOf({ '': 'x' }), 'rejected');
89
+ });
90
+
91
+ test('names every offender, so one run fixes the file', () => {
92
+ const result = Result.fromThrowable(() => {
93
+ assertVariableNamesAreValid({
94
+ 'WITH-HYPHEN': 'x',
95
+ GITHUB_TOKEN: 'x',
96
+ FINE: 'x',
97
+ });
98
+ });
99
+
100
+ assert.isTrue(Result.isErr(result));
101
+
102
+ if (Result.isErr(result)) {
103
+ assert.isTrue(result.value.message.includes('WITH-HYPHEN'));
104
+
105
+ assert.isTrue(result.value.message.includes('GITHUB_TOKEN'));
106
+
107
+ assert.isFalse(result.value.message.includes('FINE'));
108
+ }
109
+ });
110
+ });
@@ -1,2 +1,4 @@
1
1
  export * from './api/index.mjs';
2
2
  export * from './apply.mjs';
3
+ export * from './backup.mjs';
4
+ export * from './constants.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,71 @@
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
+ await formatUncommittedFiles();
47
+ }
48
+ };
49
+
50
+ const readSettings = async (
51
+ settingsPath: string,
52
+ ): Promise<VulnerabilityAlertsSettings> => {
53
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
54
+ const settingsText = await fs.readFile(settingsPath, { encoding: 'utf8' });
55
+
56
+ const validationResult = VulnerabilityAlertsSettings.validate(
57
+ JSON.parse(settingsText),
58
+ );
59
+
60
+ if (Result.isErr(validationResult)) {
61
+ throw new Error(
62
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
63
+ );
64
+ }
65
+
66
+ return validationResult.value;
67
+ };
68
+
69
+ if (isDirectlyExecuted(import.meta.url)) {
70
+ await applyVulnerabilityAlerts();
71
+ }
@@ -0,0 +1,31 @@
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 } from 'ts-repo-utils';
6
+ import { clearJsonFilesIn } from '../clear-json-files.mjs';
7
+ import { settingsJsonName, vulnerabilityAlertsDir } from '../constants.mjs';
8
+ import { getVulnerabilityAlerts } from './api/index.mjs';
9
+
10
+ /** Dependabot alerts の現在値を宣言ファイルへ撮り直す。 */
11
+ export const backupVulnerabilityAlerts = async (
12
+ fmt: boolean = true,
13
+ ): Promise<void> => {
14
+ const settings = await getVulnerabilityAlerts();
15
+
16
+ await clearJsonFilesIn(vulnerabilityAlertsDir);
17
+
18
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
19
+ await fs.writeFile(
20
+ path.resolve(vulnerabilityAlertsDir, settingsJsonName),
21
+ JSON.stringify(settings, undefined, 2),
22
+ );
23
+
24
+ if (fmt) {
25
+ await formatUncommittedFiles();
26
+ }
27
+ };
28
+
29
+ if (isDirectlyExecuted(import.meta.url)) {
30
+ await backupVulnerabilityAlerts();
31
+ }
@@ -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';