github-settings-as-code 3.0.0 → 3.0.2

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 (69) hide show
  1. package/dist/cmd/repo-settings.d.mts.map +1 -1
  2. package/dist/cmd/repo-settings.mjs +35 -34
  3. package/dist/cmd/repo-settings.mjs.map +1 -1
  4. package/dist/github/actions-settings/api/get-actions-settings.d.mts.map +1 -1
  5. package/dist/github/actions-settings/api/get-actions-settings.mjs +7 -8
  6. package/dist/github/actions-settings/api/get-actions-settings.mjs.map +1 -1
  7. package/dist/github/actions-settings/api/set-actions-settings.mjs +7 -7
  8. package/dist/github/actions-settings/apply.mjs +7 -7
  9. package/dist/github/actions-settings/apply.mjs.map +1 -1
  10. package/dist/github/actions-settings/backup.mjs +2 -2
  11. package/dist/github/actions-settings/constants.d.mts +86 -0
  12. package/dist/github/actions-settings/constants.d.mts.map +1 -1
  13. package/dist/github/actions-settings/constants.mjs +24 -24
  14. package/dist/github/constants.mjs +2 -2
  15. package/dist/github/octokit.mjs +12 -12
  16. package/dist/github/pages/api/get-pages-settings.d.mts.map +1 -1
  17. package/dist/github/pages/api/get-pages-settings.mjs +6 -7
  18. package/dist/github/pages/api/get-pages-settings.mjs.map +1 -1
  19. package/dist/github/pages/api/set-pages-settings.mjs +8 -8
  20. package/dist/github/pages/apply.mjs +12 -12
  21. package/dist/github/pages/apply.mjs.map +1 -1
  22. package/dist/github/pages/backup.mjs +2 -2
  23. package/dist/github/pages/constants.d.mts +20 -0
  24. package/dist/github/pages/constants.d.mts.map +1 -1
  25. package/dist/github/pages/constants.mjs +6 -6
  26. package/dist/github/repository/api/get-repository.mjs +3 -3
  27. package/dist/github/repository/api/get-repository.mjs.map +1 -1
  28. package/dist/github/repository/api/update-repository.d.mts +1 -1
  29. package/dist/github/repository/api/update-repository.d.mts.map +1 -1
  30. package/dist/github/repository/api/update-repository.mjs +6 -3
  31. package/dist/github/repository/api/update-repository.mjs.map +1 -1
  32. package/dist/github/repository/apply.mjs +16 -16
  33. package/dist/github/repository/apply.mjs.map +1 -1
  34. package/dist/github/repository/backup.mjs +1 -1
  35. package/dist/github/repository/constants.d.mts +176 -0
  36. package/dist/github/repository/constants.d.mts.map +1 -1
  37. package/dist/github/repository/constants.mjs +3 -3
  38. package/dist/github/resolve-target-repo.mjs +24 -24
  39. package/dist/github/ruleset/api/create-ruleset.mjs +7 -7
  40. package/dist/github/ruleset/api/get-all-rulesets.mjs +5 -5
  41. package/dist/github/ruleset/api/get-all-rulesets.mjs.map +1 -1
  42. package/dist/github/ruleset/api/get-ruleset.mjs +3 -3
  43. package/dist/github/ruleset/api/get-ruleset.mjs.map +1 -1
  44. package/dist/github/ruleset/api/update-ruleset.mjs +7 -7
  45. package/dist/github/ruleset/apply.mjs +3 -3
  46. package/dist/github/ruleset/backup.mjs +1 -1
  47. package/dist/github/ruleset/constants.d.mts +88 -8
  48. package/dist/github/ruleset/constants.d.mts.map +1 -1
  49. package/dist/github/ruleset/constants.mjs +3 -3
  50. package/dist/github/ruleset/read-rule-set-contents.mjs +2 -2
  51. package/dist/github/variables/api/create-repo-variable.mjs +1 -1
  52. package/dist/github/variables/api/list-repo-variables.mjs +1 -1
  53. package/dist/github/variables/api/update-repo-variable.mjs +1 -1
  54. package/dist/github/variables/apply.d.mts.map +1 -1
  55. package/dist/github/variables/apply.mjs +7 -6
  56. package/dist/github/variables/apply.mjs.map +1 -1
  57. package/package.json +22 -27
  58. package/src/cmd/repo-settings.mts +37 -36
  59. package/src/github/actions-settings/api/get-actions-settings.mts +0 -1
  60. package/src/github/actions-settings/apply.mts +2 -2
  61. package/src/github/pages/api/get-pages-settings.mts +0 -1
  62. package/src/github/pages/apply.mts +2 -2
  63. package/src/github/repository/api/get-repository.mts +2 -2
  64. package/src/github/repository/api/update-repository.mts +6 -1
  65. package/src/github/repository/apply.mts +2 -2
  66. package/src/github/ruleset/api/get-all-rulesets.mts +2 -2
  67. package/src/github/ruleset/api/get-ruleset.mts +2 -2
  68. package/src/github/ruleset/schema.test.mts +164 -0
  69. package/src/github/variables/apply.mts +2 -0
@@ -1,46 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ import dedent from 'dedent';
3
4
  import 'dotenv/config';
4
5
  import * as util from 'node:util';
5
6
  import { Arr } from 'ts-data-forge';
6
7
  import { type ReadonlyRecord } from 'ts-type-forge';
7
8
 
8
- const HELP = `
9
- Usage: repo-settings <command> [target] [options]
10
-
11
- GitHub の repository 設定を JSON ファイルで管理する。
12
-
13
- Commands:
14
- apply [target] ローカルの設定ファイルを GitHub に反映する
15
- backup [target] GitHub の現在値をローカルの bk/ に保存する
16
-
17
- Targets:
18
- all すべて(既定)
19
- repository repo-settings/repository-settings/settings.json
20
- rulesets repo-settings/rulesets/*.json
21
- variables repository variables ( apply のみ )
22
- actions repo-settings/actions-settings/settings.json
23
- pages repo-settings/pages/settings.json
24
-
25
- Options:
26
- --owner <owner> 対象の owner
27
- --repo <repo> 対象の repository 名
28
- -h, --help このヘルプを表示する
29
-
30
- owner / repo は「コマンドライン引数 → 環境変数 OWNER / REPO_NAME →
31
- git remote origin → package.json の name 」の順で解決される。
32
- 通常は git remote から決まるため、どちらの指定も不要。
33
-
34
- 認証は「環境変数 GITHUB_APP_TOKEN / GH_TOKEN / PERSONAL_ACCESS_TOKEN →
35
- gh CLI ( gh auth token )」の順で解決される。ローカルでは gh auth login
36
- だけで動く。
37
-
38
- Examples:
39
- repo-settings apply
40
- repo-settings apply rulesets
41
- repo-settings backup
42
- repo-settings apply --owner noshiro-pf --repo ts-repo-utils
43
- `.trim();
9
+ const HELP = dedent`
10
+ Usage: repo-settings <command> [target] [options]
11
+
12
+ GitHub の repository 設定を JSON ファイルで管理する。
13
+
14
+ Commands:
15
+ apply [target] ローカルの設定ファイルを GitHub に反映する
16
+ backup [target] GitHub の現在値をローカルの bk/ に保存する
17
+
18
+ Targets:
19
+ all すべて(既定)
20
+ repository repo-settings/repository-settings/settings.json
21
+ rulesets repo-settings/rulesets/*.json
22
+ variables repository variables ( apply のみ )
23
+ actions repo-settings/actions-settings/settings.json
24
+ pages repo-settings/pages/settings.json
25
+
26
+ Options:
27
+ --owner <owner> 対象の owner
28
+ --repo <repo> 対象の repository 名
29
+ -h, --help このヘルプを表示する
30
+
31
+ owner / repo は「コマンドライン引数 → 環境変数 OWNER / REPO_NAME →
32
+ git remote origin → package.json の name 」の順で解決される。
33
+ 通常は git remote から決まるため、どちらの指定も不要。
34
+
35
+ 認証は「環境変数 GITHUB_APP_TOKEN / GH_TOKEN / PERSONAL_ACCESS_TOKEN →
36
+ gh CLI ( gh auth token )」の順で解決される。ローカルでは gh auth login
37
+ だけで動く。
38
+
39
+ Examples:
40
+ repo-settings apply
41
+ repo-settings apply rulesets
42
+ repo-settings backup
43
+ repo-settings apply --owner noshiro-pf --repo ts-repo-utils
44
+ `;
44
45
 
45
46
  const COMMANDS = ['apply', 'backup'] as const;
46
47
 
@@ -21,7 +21,6 @@ export const getActionsSettings = async (): Promise<ActionsSettings> => {
21
21
  { owner: OWNER, repo: REPO, headers: octokitHeaders },
22
22
  );
23
23
 
24
- // eslint-disable-next-line unicorn/no-array-fill-with-reference-type
25
24
  return ActionsSettings.fill({
26
25
  enabled: permissions.data.enabled,
27
26
  allowed_actions: permissions.data.allowed_actions,
@@ -2,7 +2,7 @@
2
2
  import 'dotenv/config';
3
3
  import * as fs from 'node:fs/promises';
4
4
  import * as path from 'node:path';
5
- import { validationErrorsToMessages } from 'ts-fortress';
5
+ import * as t from 'ts-fortress';
6
6
  import {
7
7
  formatUncommittedFiles,
8
8
  isDirectlyExecuted,
@@ -51,7 +51,7 @@ const readSettings = async (): Promise<ActionsSettings> => {
51
51
 
52
52
  if (Result.isErr(validationResult)) {
53
53
  throw new Error(
54
- validationErrorsToMessages(validationResult.value).join('\n'),
54
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
55
55
  );
56
56
  }
57
57
 
@@ -19,7 +19,6 @@ export const getPagesSettings = async (): Promise<
19
19
  { owner: OWNER, repo: REPO, headers: octokitHeaders },
20
20
  );
21
21
 
22
- // eslint-disable-next-line unicorn/no-array-fill-with-reference-type
23
22
  return PagesSettings.fill({ build_type: response.data.build_type });
24
23
  } catch (error) {
25
24
  if (isRecord(error) && hasKey(error, 'status') && error.status === 404) {
@@ -2,7 +2,7 @@
2
2
  import 'dotenv/config';
3
3
  import * as fs from 'node:fs/promises';
4
4
  import * as path from 'node:path';
5
- import { validationErrorsToMessages } from 'ts-fortress';
5
+ import * as t from 'ts-fortress';
6
6
  import {
7
7
  formatUncommittedFiles,
8
8
  isDirectlyExecuted,
@@ -61,7 +61,7 @@ const readSettings = async (settingsPath: string): Promise<PagesSettings> => {
61
61
 
62
62
  if (Result.isErr(validationResult)) {
63
63
  throw new Error(
64
- validationErrorsToMessages(validationResult.value).join('\n'),
64
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
65
65
  );
66
66
  }
67
67
 
@@ -1,5 +1,5 @@
1
1
  import { GetRepositoryResponse, type EndpointKeys } from 'octokit-safe-types';
2
- import { validationErrorsToMessages } from 'ts-fortress';
2
+ import * as t from 'ts-fortress';
3
3
  import { Result } from 'ts-repo-utils';
4
4
  import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
5
5
  import { octokit } from '../../octokit.mjs';
@@ -20,7 +20,7 @@ export const getRepositorySettings =
20
20
  const res = GetRepositoryResponse.validate(getRepositoryResult.data);
21
21
 
22
22
  if (Result.isErr(res)) {
23
- console.warn(validationErrorsToMessages(res.value));
23
+ console.warn(t.validationErrorsToMessages(res.value));
24
24
  }
25
25
  }
26
26
 
@@ -2,6 +2,7 @@ import {
2
2
  type EndpointKeys,
3
3
  type UpdateRepositoryRequest,
4
4
  } from 'octokit-safe-types';
5
+ import { castDeepMutable } from 'ts-data-forge';
5
6
  import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
6
7
  import { octokit } from '../../octokit.mjs';
7
8
 
@@ -16,6 +17,8 @@ export const updateRepository = async ({
16
17
  has_issues,
17
18
  has_projects,
18
19
  has_wiki,
20
+ has_pull_requests,
21
+ pull_request_creation_policy,
19
22
  is_template,
20
23
  default_branch,
21
24
  allow_squash_merge,
@@ -47,10 +50,12 @@ export const updateRepository = async ({
47
50
  homepage,
48
51
  private: private_,
49
52
  visibility,
50
- security_and_analysis,
53
+ security_and_analysis: castDeepMutable(security_and_analysis),
51
54
  has_issues,
52
55
  has_projects,
53
56
  has_wiki,
57
+ has_pull_requests,
58
+ pull_request_creation_policy,
54
59
  is_template,
55
60
  default_branch,
56
61
  allow_squash_merge,
@@ -4,7 +4,7 @@ import * as fs from 'node:fs/promises';
4
4
  import * as path from 'node:path';
5
5
  import { type UpdateRepositoryRequest } from 'octokit-safe-types';
6
6
  import { Obj } from 'ts-data-forge';
7
- import { validationErrorsToMessages } from 'ts-fortress';
7
+ import * as t from 'ts-fortress';
8
8
  import {
9
9
  formatUncommittedFiles,
10
10
  isDirectlyExecuted,
@@ -95,7 +95,7 @@ const readSettings = async (): Promise<RepositoryPicked> => {
95
95
 
96
96
  if (Result.isErr(validationResult)) {
97
97
  throw new Error(
98
- validationErrorsToMessages(validationResult.value).join('\n'),
98
+ t.validationErrorsToMessages(validationResult.value).join('\n'),
99
99
  );
100
100
  }
101
101
 
@@ -5,7 +5,7 @@ import {
5
5
  type OctokitResponse,
6
6
  } from 'octokit-safe-types';
7
7
  import { expectType } from 'ts-data-forge';
8
- import { validationErrorsToMessages } from 'ts-fortress';
8
+ import * as t from 'ts-fortress';
9
9
  import { Result } from 'ts-repo-utils';
10
10
  import { type DeepReadonly } from 'ts-type-forge';
11
11
  import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
@@ -64,7 +64,7 @@ export const getAllRulesets = async (): Promise<GetAllRulesetsResponse> => {
64
64
  const res = GetAllRulesetsResponse.validate(getAllRulesetsResponse.data);
65
65
 
66
66
  if (Result.isErr(res)) {
67
- console.warn(validationErrorsToMessages(res.value));
67
+ console.warn(t.validationErrorsToMessages(res.value));
68
68
  }
69
69
  }
70
70
 
@@ -1,5 +1,5 @@
1
1
  import { GetRulesetResponse, type EndpointKeys } from 'octokit-safe-types';
2
- import { validationErrorsToMessages } from 'ts-fortress';
2
+ import * as t from 'ts-fortress';
3
3
  import { Result } from 'ts-repo-utils';
4
4
  import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
5
5
  import { octokit } from '../../octokit.mjs';
@@ -22,7 +22,7 @@ export const getRuleset = async (
22
22
  const res = GetRulesetResponse.validate(getRulesetResult.data);
23
23
 
24
24
  if (Result.isErr(res)) {
25
- console.warn(validationErrorsToMessages(res.value));
25
+ console.warn(t.validationErrorsToMessages(res.value));
26
26
  }
27
27
  }
28
28
 
@@ -0,0 +1,164 @@
1
+ import * as fs from 'node:fs/promises';
2
+ import * as path from 'node:path';
3
+ import { Arr, Obj, Result } from 'ts-data-forge';
4
+ import * as t from 'ts-fortress';
5
+ import { RulesetPicked, rulesetKeysToPick } from './constants.mjs';
6
+
7
+ /**
8
+ * The rulesets this repository declares, read from where `repo-settings apply`
9
+ * reads them.
10
+ *
11
+ * The real files rather than copies of them: what is worth knowing is that the
12
+ * payloads actually committed here parse. `apply` throws outright when one does
13
+ * not, and by then it is running against GitHub.
14
+ */
15
+ const rulesetsDir = path.resolve(
16
+ import.meta.dirname,
17
+ '../../../../../repo-settings/rulesets',
18
+ );
19
+
20
+ const readJsonFilesIn = async (
21
+ dir: string,
22
+ ): Promise<readonly (readonly [string, unknown])[]> => {
23
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
24
+ const entries = await fs.readdir(dir, { withFileTypes: true });
25
+
26
+ return Promise.all(
27
+ entries
28
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
29
+ .map(async (entry) => {
30
+ // eslint-disable-next-line security/detect-non-literal-fs-filename
31
+ const content = await fs.readFile(
32
+ path.resolve(dir, entry.name),
33
+ 'utf8',
34
+ );
35
+
36
+ return [entry.name, JSON.parse(content)] as const;
37
+ }),
38
+ );
39
+ };
40
+
41
+ /**
42
+ * The file name on success, and the name with what went wrong on failure.
43
+ *
44
+ * Compared against the bare name rather than asserted on directly, because
45
+ * `assert.isTrue` here takes no message argument — this way a failure still
46
+ * prints which file it was and what the validator objected to.
47
+ */
48
+ const outcomeOf = (
49
+ name: string,
50
+ result: ReturnType<typeof RulesetPicked.validate>,
51
+ ): string =>
52
+ Result.isErr(result)
53
+ ? (`${name}: ${t.validationErrorsToMessages(result.value).join('\n')}` as const)
54
+ : name;
55
+
56
+ const validRuleset = {
57
+ id: 1,
58
+ name: 'x',
59
+ target: 'branch',
60
+ enforcement: 'active',
61
+ bypass_actors: [],
62
+ conditions: { ref_name: { include: [], exclude: [] } },
63
+ rules: [{ type: 'deletion' }],
64
+ } as const;
65
+
66
+ describe('the ruleset schema', () => {
67
+ test('accepts every ruleset this repository declares', async () => {
68
+ const files = await readJsonFilesIn(rulesetsDir);
69
+
70
+ assert.isTrue(Arr.isNonEmpty(files));
71
+
72
+ for (const [name, content] of files) {
73
+ assert.deepStrictEqual(
74
+ outcomeOf(name, RulesetPicked.validate(content)),
75
+ name,
76
+ );
77
+ }
78
+ });
79
+
80
+ test('accepts every backed-up ruleset', async () => {
81
+ const files = await readJsonFilesIn(path.resolve(rulesetsDir, './bk'));
82
+
83
+ assert.isTrue(Arr.isNonEmpty(files));
84
+
85
+ for (const [name, content] of files) {
86
+ assert.deepStrictEqual(
87
+ outcomeOf(name, RulesetPicked.validate(content)),
88
+ name,
89
+ );
90
+ }
91
+ });
92
+
93
+ test('accepts the fields GitHub adds to what apply sends back', async () => {
94
+ const files = await readJsonFilesIn(rulesetsDir);
95
+
96
+ assert.isTrue(Arr.isNonEmpty(files));
97
+
98
+ const declared = RulesetPicked.validate(files[0][1]);
99
+
100
+ assert.deepStrictEqual(outcomeOf(files[0][0], declared), files[0][0]);
101
+
102
+ if (Result.isOk(declared)) {
103
+ // `getRuleset` hands the whole API response to this type, and a response
104
+ // carries more than the seven fields that go back out again.
105
+ const result = RulesetPicked.validate({
106
+ ...declared.value,
107
+ source: 'noshiro-pf/mono',
108
+ source_type: 'Repository',
109
+ node_id: 'RRS_lACqUmVwb3NpdG9yeQ',
110
+ });
111
+
112
+ assert.isTrue(Result.isOk(result));
113
+ }
114
+ });
115
+
116
+ test('accepts a ruleset built from values GitHub does use', () => {
117
+ // The control for the two rejections below: without it they could be
118
+ // passing for some reason other than the field under test.
119
+ assert.isTrue(Result.isOk(RulesetPicked.validate(validRuleset)));
120
+ });
121
+
122
+ test('rejects an enforcement value GitHub does not use', () => {
123
+ const result = RulesetPicked.validate({
124
+ ...validRuleset,
125
+ enforcement: 'enabled',
126
+ });
127
+
128
+ assert.isTrue(Result.isErr(result));
129
+ });
130
+
131
+ test('rejects a rule type GitHub does not define', () => {
132
+ const result = RulesetPicked.validate({
133
+ ...validRuleset,
134
+ rules: [{ type: 'no_such_rule' }],
135
+ });
136
+
137
+ assert.isTrue(Result.isErr(result));
138
+ });
139
+
140
+ test('narrows to the seven fields apply sends, which is what backup writes', () => {
141
+ // Validation is a check rather than a filter, so `Obj.pick` is the step
142
+ // that narrows — this mirrors what `backupRulesets` does to a response
143
+ // before writing it.
144
+ const picked = Obj.pick(
145
+ {
146
+ ...validRuleset,
147
+ source: 'noshiro-pf/mono',
148
+ source_type: 'Repository',
149
+ node_id: 'RRS_lACqUmVwb3NpdG9yeQ',
150
+ },
151
+ rulesetKeysToPick,
152
+ );
153
+
154
+ assert.deepStrictEqual(Object.keys(picked).toSorted(), [
155
+ 'bypass_actors',
156
+ 'conditions',
157
+ 'enforcement',
158
+ 'id',
159
+ 'name',
160
+ 'rules',
161
+ 'target',
162
+ ]);
163
+ });
164
+ });
@@ -1,4 +1,6 @@
1
1
  #!/usr/bin/env node
2
+
3
+ // cspell:ignore lilpi
2
4
  import 'dotenv/config';
3
5
  import { isDirectlyExecuted } from 'ts-repo-utils';
4
6
  import {