github-settings-as-code 1.2.10 → 2.0.1

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 (135) hide show
  1. package/README.md +60 -9
  2. package/dist/cmd/{gh-apply-all.d.mts → repo-settings.d.mts} +1 -1
  3. package/dist/cmd/repo-settings.d.mts.map +1 -0
  4. package/dist/cmd/repo-settings.mjs +118 -0
  5. package/dist/cmd/repo-settings.mjs.map +1 -0
  6. package/dist/github/actions-settings/api/get-actions-settings.d.mts +8 -0
  7. package/dist/github/actions-settings/api/get-actions-settings.d.mts.map +1 -0
  8. package/dist/github/actions-settings/api/get-actions-settings.mjs +25 -0
  9. package/dist/github/actions-settings/api/get-actions-settings.mjs.map +1 -0
  10. package/dist/github/actions-settings/api/index.d.mts +3 -0
  11. package/dist/github/actions-settings/api/index.d.mts.map +1 -0
  12. package/dist/github/actions-settings/api/index.mjs +3 -0
  13. package/dist/github/actions-settings/api/index.mjs.map +1 -0
  14. package/dist/github/actions-settings/api/set-actions-settings.d.mts +4 -0
  15. package/dist/github/actions-settings/api/set-actions-settings.d.mts.map +1 -0
  16. package/dist/github/actions-settings/api/set-actions-settings.mjs +27 -0
  17. package/dist/github/actions-settings/api/set-actions-settings.mjs.map +1 -0
  18. package/dist/github/actions-settings/apply.d.mts +5 -0
  19. package/dist/github/actions-settings/apply.d.mts.map +1 -0
  20. package/dist/github/actions-settings/apply.mjs +39 -0
  21. package/dist/github/actions-settings/apply.mjs.map +1 -0
  22. package/dist/github/actions-settings/backup.d.mts +5 -0
  23. package/dist/github/actions-settings/backup.d.mts.map +1 -0
  24. package/dist/github/actions-settings/backup.mjs +22 -0
  25. package/dist/github/actions-settings/backup.mjs.map +1 -0
  26. package/dist/github/actions-settings/constants.d.mts +69 -0
  27. package/dist/github/actions-settings/constants.d.mts.map +1 -0
  28. package/dist/github/actions-settings/constants.mjs +42 -0
  29. package/dist/github/actions-settings/constants.mjs.map +1 -0
  30. package/dist/github/actions-settings/index.d.mts +5 -0
  31. package/dist/github/actions-settings/index.d.mts.map +1 -0
  32. package/dist/github/actions-settings/index.mjs +5 -0
  33. package/dist/github/actions-settings/index.mjs.map +1 -0
  34. package/dist/github/constants.d.mts +3 -1
  35. package/dist/github/constants.d.mts.map +1 -1
  36. package/dist/github/constants.mjs +7 -31
  37. package/dist/github/constants.mjs.map +1 -1
  38. package/dist/github/index.d.mts +4 -0
  39. package/dist/github/index.d.mts.map +1 -1
  40. package/dist/github/index.mjs +4 -0
  41. package/dist/github/index.mjs.map +1 -1
  42. package/dist/github/octokit.d.mts.map +1 -1
  43. package/dist/github/octokit.mjs +39 -3
  44. package/dist/github/octokit.mjs.map +1 -1
  45. package/dist/github/pages/api/get-pages-settings.d.mts +8 -0
  46. package/dist/github/pages/api/get-pages-settings.d.mts.map +1 -0
  47. package/dist/github/pages/api/get-pages-settings.mjs +26 -0
  48. package/dist/github/pages/api/get-pages-settings.mjs.map +1 -0
  49. package/dist/github/pages/api/index.d.mts +3 -0
  50. package/dist/github/pages/api/index.d.mts.map +1 -0
  51. package/dist/github/pages/api/index.mjs +3 -0
  52. package/dist/github/pages/api/index.mjs.map +1 -0
  53. package/dist/github/pages/api/set-pages-settings.d.mts +9 -0
  54. package/dist/github/pages/api/set-pages-settings.d.mts.map +1 -0
  55. package/dist/github/pages/api/set-pages-settings.mjs +33 -0
  56. package/dist/github/pages/api/set-pages-settings.mjs.map +1 -0
  57. package/dist/github/pages/apply.d.mts +10 -0
  58. package/dist/github/pages/apply.d.mts.map +1 -0
  59. package/dist/github/pages/apply.mjs +49 -0
  60. package/dist/github/pages/apply.mjs.map +1 -0
  61. package/dist/github/pages/backup.d.mts +5 -0
  62. package/dist/github/pages/backup.d.mts.map +1 -0
  63. package/dist/github/pages/backup.mjs +26 -0
  64. package/dist/github/pages/backup.mjs.map +1 -0
  65. package/dist/github/pages/constants.d.mts +42 -0
  66. package/dist/github/pages/constants.d.mts.map +1 -0
  67. package/dist/github/pages/constants.mjs +14 -0
  68. package/dist/github/pages/constants.mjs.map +1 -0
  69. package/dist/github/pages/index.d.mts +5 -0
  70. package/dist/github/pages/index.d.mts.map +1 -0
  71. package/dist/github/pages/index.mjs +5 -0
  72. package/dist/github/pages/index.mjs.map +1 -0
  73. package/dist/github/repository/apply.d.mts.map +1 -1
  74. package/dist/github/repository/apply.mjs +4 -4
  75. package/dist/github/repository/apply.mjs.map +1 -1
  76. package/dist/github/repository/backup.d.mts.map +1 -1
  77. package/dist/github/repository/backup.mjs +2 -2
  78. package/dist/github/repository/backup.mjs.map +1 -1
  79. package/dist/github/repository/constants.d.mts +3259 -2024
  80. package/dist/github/repository/constants.d.mts.map +1 -1
  81. package/dist/github/repository/constants.mjs +2 -1
  82. package/dist/github/repository/constants.mjs.map +1 -1
  83. package/dist/github/resolve-target-repo.d.mts +16 -0
  84. package/dist/github/resolve-target-repo.d.mts.map +1 -0
  85. package/dist/github/resolve-target-repo.mjs +113 -0
  86. package/dist/github/resolve-target-repo.mjs.map +1 -0
  87. package/dist/github/ruleset/api/get-all-rulesets.d.mts.map +1 -1
  88. package/dist/github/ruleset/api/get-all-rulesets.mjs.map +1 -1
  89. package/dist/github/ruleset/constants.d.mts +750 -267
  90. package/dist/github/ruleset/constants.d.mts.map +1 -1
  91. package/dist/github/ruleset/constants.mjs +2 -1
  92. package/dist/github/ruleset/constants.mjs.map +1 -1
  93. package/dist/github/upgrade-stale-codec.d.mts +19 -0
  94. package/dist/github/upgrade-stale-codec.d.mts.map +1 -0
  95. package/dist/github/upgrade-stale-codec.mjs +20 -0
  96. package/dist/github/upgrade-stale-codec.mjs.map +1 -0
  97. package/dist/github/variables/apply.d.mts.map +1 -1
  98. package/dist/github/variables/apply.mjs +17 -6
  99. package/dist/github/variables/apply.mjs.map +1 -1
  100. package/package.json +41 -50
  101. package/src/cmd/repo-settings.mts +153 -0
  102. package/src/github/actions-settings/api/get-actions-settings.mts +38 -0
  103. package/src/github/actions-settings/api/index.mts +2 -0
  104. package/src/github/actions-settings/api/set-actions-settings.mts +39 -0
  105. package/src/github/actions-settings/apply.mts +63 -0
  106. package/src/github/actions-settings/backup.mts +36 -0
  107. package/src/github/actions-settings/constants.mts +50 -0
  108. package/src/github/actions-settings/index.mts +4 -0
  109. package/src/github/constants.mts +9 -42
  110. package/src/github/index.mts +4 -0
  111. package/src/github/octokit.mts +47 -3
  112. package/src/github/pages/api/get-pages-settings.mts +35 -0
  113. package/src/github/pages/api/index.mts +2 -0
  114. package/src/github/pages/api/set-pages-settings.mts +47 -0
  115. package/src/github/pages/apply.mts +73 -0
  116. package/src/github/pages/backup.mts +44 -0
  117. package/src/github/pages/constants.mts +16 -0
  118. package/src/github/pages/index.mts +4 -0
  119. package/src/github/repository/apply.mts +4 -7
  120. package/src/github/repository/backup.mts +2 -5
  121. package/src/github/repository/constants.mts +5 -1
  122. package/src/github/resolve-target-repo.mts +144 -0
  123. package/src/github/ruleset/api/get-all-rulesets.mts +3 -6
  124. package/src/github/ruleset/constants.mts +5 -1
  125. package/src/github/upgrade-stale-codec.mts +29 -0
  126. package/src/github/variables/apply.mts +17 -6
  127. package/dist/cmd/gh-apply-all.d.mts.map +0 -1
  128. package/dist/cmd/gh-apply-all.mjs +0 -10
  129. package/dist/cmd/gh-apply-all.mjs.map +0 -1
  130. package/dist/cmd/gh-backup-all.d.mts +0 -3
  131. package/dist/cmd/gh-backup-all.d.mts.map +0 -1
  132. package/dist/cmd/gh-backup-all.mjs +0 -9
  133. package/dist/cmd/gh-backup-all.mjs.map +0 -1
  134. package/src/cmd/gh-apply-all.mts +0 -18
  135. package/src/cmd/gh-backup-all.mts +0 -12
@@ -0,0 +1,50 @@
1
+ import * as t from 'ts-fortress';
2
+
3
+ /**
4
+ * `github/actions-settings/settings.json` の型。
5
+ *
6
+ * Settings > Actions > General に対応する。 2 つの API をまとめて 1 ファイルで
7
+ * 管理する:
8
+ *
9
+ * - `GET|PUT /repos/{owner}/{repo}/actions/permissions`
10
+ * - `GET|PUT /repos/{owner}/{repo}/actions/permissions/fork-pr-contributor-approval`
11
+ */
12
+ export const ActionsSettings = t.record({
13
+ /** Actions permissions: この repository で Actions を有効にするか。 */
14
+ enabled: t.boolean(true),
15
+
16
+ /** Actions permissions: 実行を許可する action の範囲。 */
17
+ allowed_actions: t.enumType(['all', 'local_only', 'selected'], {
18
+ typeName: 'AllowedActions',
19
+ defaultValue: 'all',
20
+ }),
21
+
22
+ /**
23
+ * 「 Require actions to be pinned to a full-length commit SHA 」。
24
+ *
25
+ * NOTE: @octokit/types v16 時点でこのフィールドは型定義に入っていないため、
26
+ * リクエスト時にのみ手当てしている( set-actions-permissions.mts 参照)。
27
+ */
28
+ sha_pinning_required: t.boolean(false),
29
+
30
+ /**
31
+ * 「 Approval for running fork pull request workflows from contributors 」。
32
+ *
33
+ * - `first_time_contributors_new_to_github`: GitHub 自体に不慣れな初回貢献者のみ承認必須
34
+ * - `first_time_contributors`: この repository への初回貢献者のみ承認必須
35
+ * - `all_external_contributors`: 外部貢献者すべてに承認必須
36
+ */
37
+ fork_pr_contributor_approval_policy: t.enumType(
38
+ [
39
+ 'first_time_contributors_new_to_github',
40
+ 'first_time_contributors',
41
+ 'all_external_contributors',
42
+ ],
43
+ {
44
+ typeName: 'ForkPrContributorApprovalPolicy',
45
+ defaultValue: 'all_external_contributors',
46
+ },
47
+ ),
48
+ });
49
+
50
+ export type ActionsSettings = t.TypeOf<typeof ActionsSettings>;
@@ -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';
@@ -1,57 +1,24 @@
1
- import * as fs from 'node:fs/promises';
2
1
  import * as path from 'node:path';
3
- import { hasKey, isRecord, isString, Json } from 'ts-data-forge';
4
- import { Result } from 'ts-repo-utils';
2
+ import { resolveTargetRepo } from './resolve-target-repo.mjs';
5
3
 
6
- const ownerNullable = process.env['OWNER'];
4
+ const targetRepo = await resolveTargetRepo();
7
5
 
8
- if (ownerNullable === undefined) {
9
- throw new Error('OWNER env var is not set');
10
- }
6
+ export const OWNER: string = targetRepo.owner;
11
7
 
12
- export const OWNER = ownerNullable;
13
-
14
- const getRepoName = async (): Promise<string> => {
15
- const repoNameEnv = process.env['REPO_NAME'];
16
-
17
- if (repoNameEnv !== undefined) {
18
- return repoNameEnv;
19
- }
20
-
21
- const packageJsonPath = path.resolve(process.cwd(), './package.json');
22
-
23
- // eslint-disable-next-line security/detect-non-literal-fs-filename
24
- const packageJsonStr = await fs.readFile(packageJsonPath, {
25
- encoding: 'utf8',
26
- });
27
-
28
- const packageJson = Json.parse(packageJsonStr);
29
-
30
- if (Result.isErr(packageJson)) {
31
- throw new Error(packageJson.value);
32
- }
33
-
34
- if (
35
- !isRecord(packageJson.value) ||
36
- !hasKey(packageJson.value, 'name') ||
37
- !isString(packageJson.value.name)
38
- ) {
39
- throw new Error('package.json not parsed correctly');
40
- }
41
-
42
- return packageJson.value.name;
43
- };
44
-
45
- export const REPO: string = await getRepoName();
8
+ export const REPO: string = targetRepo.repo;
46
9
 
47
10
  const githubDir = path.resolve(process.cwd(), './github');
48
11
 
12
+ export const settingsJsonName = 'settings.json';
13
+
49
14
  export const repositorySettingsDir = path.resolve(
50
15
  githubDir,
51
16
  './repository-settings',
52
17
  );
53
18
 
54
- export const repositorySettingsJsonName = 'settings.json';
19
+ export const actionsSettingsDir = path.resolve(githubDir, './actions-settings');
20
+
21
+ export const pagesSettingsDir = path.resolve(githubDir, './pages');
55
22
 
56
23
  export const rulesetsDir = path.resolve(githubDir, './rulesets');
57
24
 
@@ -1,4 +1,8 @@
1
+ export * from './actions-settings/index.mjs';
1
2
  export * from './constants.mjs';
3
+ export * from './pages/index.mjs';
2
4
  export * from './repository/index.mjs';
5
+ export * from './resolve-target-repo.mjs';
3
6
  export * from './ruleset/index.mjs';
7
+ export * from './upgrade-stale-codec.mjs';
4
8
  export * from './variables/index.mjs';
@@ -1,5 +1,49 @@
1
1
  import { Octokit } from '@octokit/core';
2
+ import { execFileSync } from 'node:child_process';
2
3
 
3
- export const octokit = new Octokit({
4
- auth: process.env['PERSONAL_ACCESS_TOKEN'],
5
- });
4
+ /**
5
+ * GitHub API の認証トークンを解決する。
6
+ *
7
+ * 1. 明示的な環境変数 … CI では GitHub App の installation token を渡す。
8
+ * 2. `gh auth token` … ローカルでは gh CLI のログイン情報をそのまま使うため、 PAT
9
+ * を .env などに平文で置く必要がない。
10
+ *
11
+ * `GITHUB_TOKEN` は候補に含めない。 Actions では既定の( administration 権限を
12
+ * 持たない)トークンが同名で入っていることがあり、黙って弱い方を掴むため。
13
+ */
14
+ const resolveAuthToken = (): string => {
15
+ const fromEnv =
16
+ process.env['GITHUB_APP_TOKEN'] ??
17
+ process.env['GH_TOKEN'] ??
18
+ process.env['PERSONAL_ACCESS_TOKEN'];
19
+
20
+ if (fromEnv !== undefined && fromEnv !== '') {
21
+ return fromEnv;
22
+ }
23
+
24
+ try {
25
+ // シェルを経由させない。 stderr も捨てて、失敗時にトークンが混ざらないようにする。
26
+ const stdout = execFileSync('gh', ['auth', 'token'], {
27
+ encoding: 'utf8',
28
+ stdio: ['ignore', 'pipe', 'ignore'],
29
+ });
30
+
31
+ const token = stdout.trim();
32
+
33
+ if (token.length > 0) {
34
+ return token;
35
+ }
36
+ } catch {
37
+ // gh が未インストール、または未ログイン。下のエラーにまとめる。
38
+ }
39
+
40
+ throw new Error(
41
+ [
42
+ 'GitHub のトークンが見つかりませんでした。',
43
+ 'CI では GH_TOKEN( GitHub App の installation token )を設定してください。',
44
+ 'ローカルでは `gh auth login` を実行すれば、その認証がそのまま使われます。',
45
+ ].join('\n'),
46
+ );
47
+ };
48
+
49
+ export const octokit = new Octokit({ auth: resolveAuthToken() });
@@ -0,0 +1,35 @@
1
+ import { type EndpointKeys } from 'octokit-safe-types';
2
+ import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
3
+ import { octokit } from '../../octokit.mjs';
4
+ import { PagesSettings } from '../constants.mjs';
5
+
6
+ /**
7
+ * GitHub Pages の現在の設定を取得する。
8
+ *
9
+ * Pages が未設定の repository では 404 が返るため、その場合は `undefined` を返す。
10
+ */
11
+ export const getPagesSettings = async (): Promise<
12
+ PagesSettings | undefined
13
+ > => {
14
+ try {
15
+ // https://docs.github.com/rest/pages/pages#get-a-apiname-pages-site
16
+ const response = await octokit.request(
17
+ 'GET /repos/{owner}/{repo}/pages' satisfies EndpointKeys,
18
+ { owner: OWNER, repo: REPO, headers: octokitHeaders },
19
+ );
20
+
21
+ // eslint-disable-next-line unicorn/no-array-fill-with-reference-type
22
+ return PagesSettings.fill({ build_type: response.data.build_type });
23
+ } catch (error) {
24
+ if (
25
+ typeof error === 'object' &&
26
+ error !== null &&
27
+ Object.hasOwn(error, 'status') &&
28
+ (error as Readonly<{ status?: unknown }>).status === 404
29
+ ) {
30
+ return undefined;
31
+ }
32
+
33
+ throw error;
34
+ }
35
+ };
@@ -0,0 +1,2 @@
1
+ export * from './get-pages-settings.mjs';
2
+ export * from './set-pages-settings.mjs';
@@ -0,0 +1,47 @@
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 PagesSettings } from '../constants.mjs';
5
+ import { getPagesSettings } from './get-pages-settings.mjs';
6
+
7
+ /**
8
+ * GitHub Pages の Source を JSON の内容に合わせる。
9
+ *
10
+ * Pages 未設定なら POST で作成し、設定済みなら PUT で更新する。 これにより
11
+ * 「 Settings > Pages で Source を GitHub Actions に変更する 」という手作業が不要になる。
12
+ */
13
+ export const setPagesSettings = async (
14
+ settings: PagesSettings,
15
+ ): Promise<void> => {
16
+ const current = await getPagesSettings();
17
+
18
+ if (current === undefined) {
19
+ // https://docs.github.com/rest/pages/pages#create-a-apiname-pages-site
20
+ await octokit.request(
21
+ 'POST /repos/{owner}/{repo}/pages' satisfies EndpointKeys,
22
+ {
23
+ owner: OWNER,
24
+ repo: REPO,
25
+ headers: octokitHeaders,
26
+ build_type: settings.build_type,
27
+ },
28
+ );
29
+
30
+ return;
31
+ }
32
+
33
+ if (current.build_type === settings.build_type) {
34
+ return;
35
+ }
36
+
37
+ // https://docs.github.com/rest/pages/pages#update-information-about-a-apiname-pages-site
38
+ await octokit.request(
39
+ 'PUT /repos/{owner}/{repo}/pages' satisfies EndpointKeys,
40
+ {
41
+ owner: OWNER,
42
+ repo: REPO,
43
+ headers: octokitHeaders,
44
+ build_type: settings.build_type,
45
+ },
46
+ );
47
+ };
@@ -0,0 +1,73 @@
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 { validationErrorsToMessages } from 'ts-fortress';
6
+ import {
7
+ formatUncommittedFiles,
8
+ isDirectlyExecuted,
9
+ pathExists,
10
+ Result,
11
+ } from 'ts-repo-utils';
12
+ import { pagesSettingsDir, settingsJsonName } from '../constants.mjs';
13
+ import { getPagesSettings, setPagesSettings } from './api/index.mjs';
14
+ import { PagesSettings } from './constants.mjs';
15
+
16
+ /**
17
+ * `github/pages/settings.json` を Settings > Pages に反映する。
18
+ *
19
+ * 設定ファイルを置いていない repository では何もしない( Pages を使わない
20
+ * repository で誤って Pages を有効化しないため)。
21
+ */
22
+ export const applyPagesSettings = async (): Promise<void> => {
23
+ const settingsPath = path.resolve(pagesSettingsDir, settingsJsonName);
24
+
25
+ if (!(await pathExists(settingsPath))) {
26
+ return;
27
+ }
28
+
29
+ const settings = await readSettings(settingsPath);
30
+
31
+ await setPagesSettings(settings);
32
+
33
+ // 反映後の実際の値でローカルのファイルを更新する
34
+ {
35
+ const applied = await getPagesSettings();
36
+
37
+ if (applied === undefined) {
38
+ return;
39
+ }
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(pagesSettingsDir, 'bk', settingsJsonName),
49
+ str,
50
+ );
51
+
52
+ await formatUncommittedFiles();
53
+ }
54
+ };
55
+
56
+ const readSettings = async (settingsPath: string): Promise<PagesSettings> => {
57
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
58
+ const settingsText = await fs.readFile(settingsPath, { encoding: 'utf8' });
59
+
60
+ const validationResult = PagesSettings.validate(JSON.parse(settingsText));
61
+
62
+ if (Result.isErr(validationResult)) {
63
+ throw new Error(
64
+ validationErrorsToMessages(validationResult.value).join('\n'),
65
+ );
66
+ }
67
+
68
+ return validationResult.value;
69
+ };
70
+
71
+ if (isDirectlyExecuted(import.meta.url)) {
72
+ await applyPagesSettings();
73
+ }
@@ -0,0 +1,44 @@
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 { pagesSettingsDir, settingsJsonName } from '../constants.mjs';
11
+ import { getPagesSettings } from './api/index.mjs';
12
+
13
+ const backupDir = path.resolve(pagesSettingsDir, './bk');
14
+
15
+ /** Settings > Pages の現在値を `bk/` に保存する。 */
16
+ export const backupPagesSettings = async (
17
+ fmt: boolean = true,
18
+ ): Promise<void> => {
19
+ await makeEmptyDir(backupDir);
20
+
21
+ const settings = await getPagesSettings();
22
+
23
+ if (settings === undefined) {
24
+ console.warn(
25
+ 'GitHub Pages is not enabled for this repository; skipping backup.',
26
+ );
27
+
28
+ return;
29
+ }
30
+
31
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
32
+ await fs.writeFile(
33
+ path.resolve(backupDir, settingsJsonName),
34
+ JSON.stringify(settings, undefined, 2),
35
+ );
36
+
37
+ if (fmt) {
38
+ await formatUncommittedFiles();
39
+ }
40
+ };
41
+
42
+ if (isDirectlyExecuted(import.meta.url)) {
43
+ await backupPagesSettings();
44
+ }
@@ -0,0 +1,16 @@
1
+ import * as t from 'ts-fortress';
2
+
3
+ /**
4
+ * `github/pages/settings.json` の型。
5
+ *
6
+ * Settings > Pages > Build and deployment > Source に対応する。
7
+ * `workflow` が GUI 上の「 GitHub Actions 」、 `legacy` が「 Deploy from a branch 」。
8
+ */
9
+ export const PagesSettings = t.record({
10
+ build_type: t.enumType(['workflow', 'legacy'], {
11
+ typeName: 'PagesBuildType',
12
+ defaultValue: 'workflow',
13
+ }),
14
+ });
15
+
16
+ export type PagesSettings = t.TypeOf<typeof PagesSettings>;
@@ -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';
@@ -10,10 +10,7 @@ import {
10
10
  isDirectlyExecuted,
11
11
  Result,
12
12
  } from 'ts-repo-utils';
13
- import {
14
- repositorySettingsDir,
15
- repositorySettingsJsonName,
16
- } from '../constants.mjs';
13
+ import { repositorySettingsDir, settingsJsonName } from '../constants.mjs';
17
14
  import { getRepositorySettings, updateRepository } from './api/index.mjs';
18
15
  import { backupRepositorySettings } from './backup.mjs';
19
16
  import {
@@ -70,13 +67,13 @@ export const applyRepositorySettings = async (): Promise<void> => {
70
67
 
71
68
  // eslint-disable-next-line security/detect-non-literal-fs-filename
72
69
  await fs.writeFile(
73
- path.resolve(repositorySettingsDir, repositorySettingsJsonName),
70
+ path.resolve(repositorySettingsDir, settingsJsonName),
74
71
  str,
75
72
  );
76
73
 
77
74
  // eslint-disable-next-line security/detect-non-literal-fs-filename
78
75
  await fs.writeFile(
79
- path.resolve(repositorySettingsDir, 'bk', repositorySettingsJsonName),
76
+ path.resolve(repositorySettingsDir, 'bk', settingsJsonName),
80
77
  str,
81
78
  );
82
79
 
@@ -87,7 +84,7 @@ export const applyRepositorySettings = async (): Promise<void> => {
87
84
  const readSettings = async (): Promise<RepositoryPicked> => {
88
85
  // eslint-disable-next-line security/detect-non-literal-fs-filename
89
86
  const settingsText = await fs.readFile(
90
- path.resolve(repositorySettingsDir, repositorySettingsJsonName),
87
+ path.resolve(repositorySettingsDir, settingsJsonName),
91
88
  {
92
89
  encoding: 'utf8',
93
90
  },
@@ -8,10 +8,7 @@ import {
8
8
  isDirectlyExecuted,
9
9
  makeEmptyDir,
10
10
  } from 'ts-repo-utils';
11
- import {
12
- repositorySettingsDir,
13
- repositorySettingsJsonName,
14
- } from '../constants.mjs';
11
+ import { repositorySettingsDir, settingsJsonName } from '../constants.mjs';
15
12
  import { getRepositorySettings } from './api/index.mjs';
16
13
  import { repositoryKeysToPick } from './constants.mjs';
17
14
 
@@ -26,7 +23,7 @@ export const backupRepositorySettings = async (
26
23
 
27
24
  // eslint-disable-next-line security/detect-non-literal-fs-filename
28
25
  await fs.writeFile(
29
- path.resolve(backupDir, repositorySettingsJsonName),
26
+ path.resolve(backupDir, settingsJsonName),
30
27
  JSON.stringify(
31
28
  Obj.pick(repositorySettings, repositoryKeysToPick),
32
29
  undefined,
@@ -5,6 +5,7 @@ import {
5
5
  import { expectType } from 'ts-data-forge';
6
6
  import * as t from 'ts-fortress';
7
7
  import { type StrictExclude } from 'ts-type-forge';
8
+ import { upgradeStaleCodec } from '../upgrade-stale-codec.mjs';
8
9
 
9
10
  const keysToPickBase = [
10
11
  'allow_auto_merge',
@@ -63,6 +64,9 @@ export type RepositoryKeysToPick = StrictExclude<
63
64
 
64
65
  expectType<keyof UpdateRepositoryRequest, RepositoryKeysToPick>('>=');
65
66
 
66
- export const RepositoryPicked = t.pick(FullRepository, repositoryKeysToPick);
67
+ export const RepositoryPicked = t.pick(
68
+ upgradeStaleCodec(FullRepository),
69
+ repositoryKeysToPick,
70
+ );
67
71
 
68
72
  export type RepositoryPicked = t.TypeOf<typeof RepositoryPicked>;
@@ -0,0 +1,144 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as util from 'node:util';
5
+ import { hasKey, isRecord, isString, Json } from 'ts-data-forge';
6
+ import { Result } from 'ts-repo-utils';
7
+
8
+ export type TargetRepo = Readonly<{
9
+ owner: string;
10
+ repo: string;
11
+ }>;
12
+
13
+ /** `strict: false` の解析結果は string | boolean | 配列 になりうるため絞り込む。 */
14
+ const pickString = (value: unknown): string | undefined =>
15
+ isString(value) && value !== '' ? value : undefined;
16
+
17
+ /**
18
+ * コマンドライン引数から `--owner` / `--repo` を読む。
19
+ *
20
+ * サブコマンドや他のフラグと共存できるよう `strict: false` で解析する。
21
+ */
22
+ const fromCliArgs = (): Partial<TargetRepo> => {
23
+ const { values } = util.parseArgs({
24
+ args: process.argv.slice(2),
25
+ options: {
26
+ owner: { type: 'string' },
27
+ repo: { type: 'string' },
28
+ },
29
+ strict: false,
30
+ allowPositionals: true,
31
+ });
32
+
33
+ return {
34
+ owner: pickString(values.owner),
35
+ repo: pickString(values.repo),
36
+ };
37
+ };
38
+
39
+ const fromEnv = (): Partial<TargetRepo> =>
40
+ ({
41
+ owner: process.env['OWNER'],
42
+ repo: process.env['REPO_NAME'],
43
+ }) as const;
44
+
45
+ /**
46
+ * `git remote get-url origin` から owner / repo を推測する。
47
+ *
48
+ * SSH 形式( `git@github.com:owner/repo.git` )と HTTPS 形式
49
+ * ( `https://github.com/owner/repo.git` )の両方に対応する。
50
+ */
51
+ const fromGitRemote = (): Partial<TargetRepo> => {
52
+ try {
53
+ const url = execFileSync('git', ['remote', 'get-url', 'origin'], {
54
+ encoding: 'utf8',
55
+ stdio: ['ignore', 'pipe', 'ignore'],
56
+ }).trim();
57
+
58
+ const matched =
59
+ /^(?:git@[^:]+:|(?:https?|ssh):\/\/[^/]+\/)(?<owner>[^/]+)\/(?<repo>[^/]+?)(?:\.git)?$/u.exec(
60
+ url,
61
+ );
62
+
63
+ return {
64
+ owner: matched?.groups?.['owner'],
65
+ repo: matched?.groups?.['repo'],
66
+ };
67
+ } catch {
68
+ return {};
69
+ }
70
+ };
71
+
72
+ /** `package.json` の `name` を repository 名として使う(最後の手段)。 */
73
+ const fromPackageJson = async (): Promise<Partial<TargetRepo>> => {
74
+ const packageJsonPath = path.resolve(process.cwd(), './package.json');
75
+
76
+ try {
77
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
78
+ const packageJsonStr = await fs.readFile(packageJsonPath, {
79
+ encoding: 'utf8',
80
+ });
81
+
82
+ const packageJson = Json.parse(packageJsonStr);
83
+
84
+ if (
85
+ Result.isErr(packageJson) ||
86
+ !isRecord(packageJson.value) ||
87
+ !hasKey(packageJson.value, 'name') ||
88
+ !isString(packageJson.value.name)
89
+ ) {
90
+ return {};
91
+ }
92
+
93
+ return { repo: packageJson.value.name };
94
+ } catch {
95
+ return {};
96
+ }
97
+ };
98
+
99
+ /**
100
+ * 操作対象の repository を決める。
101
+ *
102
+ * 優先順位は以下の通り。
103
+ *
104
+ * 1. コマンドライン引数 `--owner` / `--repo`
105
+ * 2. 環境変数 `OWNER` / `REPO_NAME`
106
+ * 3. `git remote get-url origin`
107
+ * 4. `package.json` の `name` ( repo のみ )
108
+ */
109
+ export const resolveTargetRepo = async (): Promise<TargetRepo> => {
110
+ const sources: readonly Partial<TargetRepo>[] = [
111
+ fromCliArgs(),
112
+ fromEnv(),
113
+ fromGitRemote(),
114
+ await fromPackageJson(),
115
+ ] as const;
116
+
117
+ const owner = sources.map((s) => s.owner).find((v) => v !== undefined);
118
+
119
+ if (owner === undefined) {
120
+ throw new Error(
121
+ [
122
+ 'owner を特定できませんでした。次のいずれかで指定してください:',
123
+ ' --owner <owner> (コマンドライン引数)',
124
+ ' OWNER=<owner> (環境変数)',
125
+ ' git remote add origin <URL> ( origin の URL から推測されます)',
126
+ ].join('\n'),
127
+ );
128
+ }
129
+
130
+ const repo = sources.map((s) => s.repo).find((v) => v !== undefined);
131
+
132
+ if (repo === undefined) {
133
+ throw new Error(
134
+ [
135
+ 'repository 名を特定できませんでした。次のいずれかで指定してください:',
136
+ ' --repo <repo> (コマンドライン引数)',
137
+ ' REPO_NAME=<repo> (環境変数)',
138
+ ' git remote add origin <URL> ( origin の URL から推測されます)',
139
+ ].join('\n'),
140
+ );
141
+ }
142
+
143
+ return { owner, repo };
144
+ };
@@ -35,18 +35,15 @@ export const getAllRulesets = async (): Promise<GetAllRulesetsResponse> => {
35
35
  enforcement: components['schemas']['repository-rule-enforcement'];
36
36
  bypass_actors?: components['schemas']['repository-ruleset-bypass-actor'][];
37
37
  current_user_can_bypass?:
38
- | 'always'
39
- | 'pull_requests_only'
40
- | 'never'
41
- | 'exempt';
38
+ 'always' | 'pull_requests_only' | 'never' | 'exempt';
42
39
  node_id?: string;
43
40
  _links?: {
44
41
  self?: {
45
42
  href?: string;
46
43
  };
47
- html?: {
44
+ html?: null | {
48
45
  href?: string;
49
- } | null;
46
+ };
50
47
  };
51
48
  conditions?:
52
49
  | (
@@ -5,6 +5,7 @@ import {
5
5
  } from 'octokit-safe-types';
6
6
  import { expectType } from 'ts-data-forge';
7
7
  import * as t from 'ts-fortress';
8
+ import { upgradeStaleCodec } from '../upgrade-stale-codec.mjs';
8
9
 
9
10
  export const rulesetKeysToPick = [
10
11
  'id',
@@ -22,6 +23,9 @@ expectType<keyof UpdateRulesetRequest | 'id', KeysToPick>('=');
22
23
 
23
24
  expectType<keyof CreateRulesetRequest, keyof UpdateRulesetRequest>('=');
24
25
 
25
- export const RulesetPicked = t.pick(RepositoryRuleset, rulesetKeysToPick);
26
+ export const RulesetPicked = t.pick(
27
+ upgradeStaleCodec(RepositoryRuleset),
28
+ rulesetKeysToPick,
29
+ );
26
30
 
27
31
  export type RulesetPicked = t.TypeOf<typeof RulesetPicked>;