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.
- package/README.md +61 -39
- package/dist/cmd/repo-settings.mjs +17 -2
- package/dist/cmd/repo-settings.mjs.map +1 -1
- package/dist/github/actions-settings/apply.d.mts.map +1 -1
- package/dist/github/actions-settings/apply.mjs +0 -4
- package/dist/github/actions-settings/apply.mjs.map +1 -1
- package/dist/github/actions-settings/backup.d.mts +1 -1
- package/dist/github/actions-settings/backup.d.mts.map +1 -1
- package/dist/github/actions-settings/backup.mjs +4 -4
- package/dist/github/actions-settings/backup.mjs.map +1 -1
- package/dist/github/actions-settings/constants.d.mts +1 -1
- package/dist/github/clear-json-files.d.mts +17 -0
- package/dist/github/clear-json-files.d.mts.map +1 -0
- package/dist/github/clear-json-files.mjs +29 -0
- package/dist/github/clear-json-files.mjs.map +1 -0
- package/dist/github/constants.d.mts +3 -0
- package/dist/github/constants.d.mts.map +1 -1
- package/dist/github/constants.mjs +3 -0
- package/dist/github/constants.mjs.map +1 -1
- package/dist/github/environments/api/get-environments.d.mts +16 -0
- package/dist/github/environments/api/get-environments.d.mts.map +1 -0
- package/dist/github/environments/api/get-environments.mjs +98 -0
- package/dist/github/environments/api/get-environments.mjs.map +1 -0
- package/dist/github/environments/api/index.d.mts +3 -0
- package/dist/github/environments/api/index.d.mts.map +1 -0
- package/dist/github/environments/api/index.mjs +3 -0
- package/dist/github/environments/api/index.mjs.map +1 -0
- package/dist/github/environments/api/set-environment.d.mts +10 -0
- package/dist/github/environments/api/set-environment.d.mts.map +1 -0
- package/dist/github/environments/api/set-environment.mjs +73 -0
- package/dist/github/environments/api/set-environment.mjs.map +1 -0
- package/dist/github/environments/apply.d.mts +11 -0
- package/dist/github/environments/apply.d.mts.map +1 -0
- package/dist/github/environments/apply.mjs +63 -0
- package/dist/github/environments/apply.mjs.map +1 -0
- package/dist/github/environments/backup.d.mts +10 -0
- package/dist/github/environments/backup.d.mts.map +1 -0
- package/dist/github/environments/backup.mjs +30 -0
- package/dist/github/environments/backup.mjs.map +1 -0
- package/dist/github/environments/constants.d.mts +354 -0
- package/dist/github/environments/constants.d.mts.map +1 -0
- package/dist/github/environments/constants.mjs +62 -0
- package/dist/github/environments/constants.mjs.map +1 -0
- package/dist/github/environments/index.d.mts +6 -0
- package/dist/github/environments/index.d.mts.map +1 -0
- package/dist/github/environments/index.mjs +6 -0
- package/dist/github/environments/index.mjs.map +1 -0
- package/dist/github/environments/read-environment-contents.d.mts +3 -0
- package/dist/github/environments/read-environment-contents.d.mts.map +1 -0
- package/dist/github/environments/read-environment-contents.mjs +31 -0
- package/dist/github/environments/read-environment-contents.mjs.map +1 -0
- package/dist/github/index.d.mts +4 -0
- package/dist/github/index.d.mts.map +1 -1
- package/dist/github/index.mjs +4 -0
- package/dist/github/index.mjs.map +1 -1
- package/dist/github/pages/apply.d.mts.map +1 -1
- package/dist/github/pages/apply.mjs +0 -2
- package/dist/github/pages/apply.mjs.map +1 -1
- package/dist/github/pages/backup.d.mts +11 -1
- package/dist/github/pages/backup.d.mts.map +1 -1
- package/dist/github/pages/backup.mjs +18 -5
- package/dist/github/pages/backup.mjs.map +1 -1
- package/dist/github/repository/apply.d.mts.map +1 -1
- package/dist/github/repository/apply.mjs +0 -4
- package/dist/github/repository/apply.mjs.map +1 -1
- package/dist/github/repository/backup.d.mts.map +1 -1
- package/dist/github/repository/backup.mjs +4 -4
- package/dist/github/repository/backup.mjs.map +1 -1
- package/dist/github/ruleset/apply.d.mts.map +1 -1
- package/dist/github/ruleset/apply.mjs +9 -9
- package/dist/github/ruleset/apply.mjs.map +1 -1
- package/dist/github/ruleset/backup.d.mts +7 -0
- package/dist/github/ruleset/backup.d.mts.map +1 -1
- package/dist/github/ruleset/backup.mjs +20 -7
- package/dist/github/ruleset/backup.mjs.map +1 -1
- package/dist/github/ruleset/read-rule-set-contents.d.mts +0 -1
- package/dist/github/ruleset/read-rule-set-contents.d.mts.map +1 -1
- package/dist/github/ruleset/read-rule-set-contents.mjs +0 -1
- package/dist/github/ruleset/read-rule-set-contents.mjs.map +1 -1
- package/dist/github/settings-file-path.d.mts +14 -0
- package/dist/github/settings-file-path.d.mts.map +1 -0
- package/dist/github/settings-file-path.mjs +41 -0
- package/dist/github/settings-file-path.mjs.map +1 -0
- package/dist/github/variables/api/list-repo-variables.d.mts.map +1 -1
- package/dist/github/variables/api/list-repo-variables.mjs +3 -1
- package/dist/github/variables/api/list-repo-variables.mjs.map +1 -1
- package/dist/github/variables/apply.d.mts +9 -0
- package/dist/github/variables/apply.d.mts.map +1 -1
- package/dist/github/variables/apply.mjs +31 -17
- package/dist/github/variables/apply.mjs.map +1 -1
- package/dist/github/variables/backup.d.mts +20 -0
- package/dist/github/variables/backup.d.mts.map +1 -0
- package/dist/github/variables/backup.mjs +35 -0
- package/dist/github/variables/backup.mjs.map +1 -0
- package/dist/github/variables/constants.d.mts +65 -0
- package/dist/github/variables/constants.d.mts.map +1 -0
- package/dist/github/variables/constants.mjs +48 -0
- package/dist/github/variables/constants.mjs.map +1 -0
- package/dist/github/variables/index.d.mts +2 -0
- package/dist/github/variables/index.d.mts.map +1 -1
- package/dist/github/variables/index.mjs +2 -0
- package/dist/github/variables/index.mjs.map +1 -1
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts +10 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs +25 -0
- package/dist/github/vulnerability-alerts/api/get-vulnerability-alerts.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/api/index.d.mts +3 -0
- package/dist/github/vulnerability-alerts/api/index.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/index.mjs +3 -0
- package/dist/github/vulnerability-alerts/api/index.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts +10 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs +19 -0
- package/dist/github/vulnerability-alerts/api/set-vulnerability-alerts.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/apply.d.mts +11 -0
- package/dist/github/vulnerability-alerts/apply.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/apply.mjs +45 -0
- package/dist/github/vulnerability-alerts/apply.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/backup.d.mts +5 -0
- package/dist/github/vulnerability-alerts/backup.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/backup.mjs +22 -0
- package/dist/github/vulnerability-alerts/backup.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/constants.d.mts +72 -0
- package/dist/github/vulnerability-alerts/constants.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/constants.mjs +21 -0
- package/dist/github/vulnerability-alerts/constants.mjs.map +1 -0
- package/dist/github/vulnerability-alerts/index.d.mts +5 -0
- package/dist/github/vulnerability-alerts/index.d.mts.map +1 -0
- package/dist/github/vulnerability-alerts/index.mjs +5 -0
- package/dist/github/vulnerability-alerts/index.mjs.map +1 -0
- package/package.json +29 -28
- package/src/cmd/repo-settings.mts +22 -2
- package/src/github/actions-settings/apply.mts +0 -9
- package/src/github/actions-settings/backup.mts +6 -11
- package/src/github/actions-settings/constants.mts +1 -1
- package/src/github/clear-json-files.mts +31 -0
- package/src/github/clear-json-files.test.mts +78 -0
- package/src/github/constants.mts +9 -0
- package/src/github/environments/api/get-environments.mts +155 -0
- package/src/github/environments/api/index.mts +2 -0
- package/src/github/environments/api/set-environment.mts +106 -0
- package/src/github/environments/apply.mts +82 -0
- package/src/github/environments/backup.mts +39 -0
- package/src/github/environments/constants.mts +78 -0
- package/src/github/environments/index.mts +5 -0
- package/src/github/environments/read-environment-contents.mts +49 -0
- package/src/github/index.mts +4 -0
- package/src/github/pages/apply.mts +0 -6
- package/src/github/pages/backup.mts +21 -12
- package/src/github/repository/apply.mts +0 -9
- package/src/github/repository/backup.mts +5 -10
- package/src/github/ruleset/apply.mts +10 -18
- package/src/github/ruleset/backup.mts +23 -13
- package/src/github/ruleset/read-rule-set-contents.mts +0 -4
- package/src/github/ruleset/schema.test.mts +0 -13
- package/src/github/settings-file-path.mts +51 -0
- package/src/github/settings-file-path.test.mts +70 -0
- package/src/github/variables/api/list-repo-variables.mts +3 -1
- package/src/github/variables/apply.mts +54 -27
- package/src/github/variables/backup.mts +47 -0
- package/src/github/variables/constants.mts +61 -0
- package/src/github/variables/constants.test.mts +110 -0
- package/src/github/variables/index.mts +2 -0
- package/src/github/vulnerability-alerts/api/get-vulnerability-alerts.mts +31 -0
- package/src/github/vulnerability-alerts/api/index.mts +2 -0
- package/src/github/vulnerability-alerts/api/set-vulnerability-alerts.mts +31 -0
- package/src/github/vulnerability-alerts/apply.mts +71 -0
- package/src/github/vulnerability-alerts/backup.mts +31 -0
- package/src/github/vulnerability-alerts/constants.mts +25 -0
- package/src/github/vulnerability-alerts/index.mts +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "github-settings-as-code",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "4.0.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"keywords": [
|
|
6
6
|
"github",
|
|
@@ -31,19 +31,19 @@
|
|
|
31
31
|
"octokit-safe-types": "^1.2.28",
|
|
32
32
|
"ts-data-forge": "^14.7.1",
|
|
33
33
|
"ts-fortress": "^12.1.3",
|
|
34
|
-
"ts-repo-utils": "^10.
|
|
34
|
+
"ts-repo-utils": "^10.7.0",
|
|
35
35
|
"ts-type-forge": "^9.2.2"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@types/node": "26.5.
|
|
38
|
+
"@types/node": "26.5.1",
|
|
39
39
|
"@vitest/coverage-v8": "5.0.0",
|
|
40
40
|
"@vitest/ui": "5.0.0",
|
|
41
|
-
"cspell": "10.
|
|
42
|
-
"eslint": "10.
|
|
43
|
-
"eslint-config-typed": "5.11.
|
|
44
|
-
"eslint-plugin-ts-data-forge": "0.
|
|
45
|
-
"eslint-plugin-ts-fortress": "1.2.
|
|
46
|
-
"eslint-plugin-ts-type-forge": "0.8.
|
|
41
|
+
"cspell": "10.3.0",
|
|
42
|
+
"eslint": "10.10.0",
|
|
43
|
+
"eslint-config-typed": "5.11.10",
|
|
44
|
+
"eslint-plugin-ts-data-forge": "0.8.1",
|
|
45
|
+
"eslint-plugin-ts-fortress": "1.2.3",
|
|
46
|
+
"eslint-plugin-ts-type-forge": "0.8.4",
|
|
47
47
|
"jiti": "2.7.0",
|
|
48
48
|
"npm-run-all2": "9.0.3",
|
|
49
49
|
"prettier": "3.9.6",
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
"pnpm": ">=9.0.0"
|
|
60
60
|
},
|
|
61
61
|
"volta": {
|
|
62
|
-
"node": "26.8.
|
|
62
|
+
"node": "26.8.2"
|
|
63
63
|
},
|
|
64
64
|
"publishConfig": {
|
|
65
65
|
"access": "public",
|
|
@@ -67,29 +67,30 @@
|
|
|
67
67
|
},
|
|
68
68
|
"scripts": {
|
|
69
69
|
"build": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/build.mts",
|
|
70
|
-
"
|
|
70
|
+
"check:cspell": "cspell \"**\" --gitignore --gitignore-root ../../ --no-progress",
|
|
71
71
|
"check:ext": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-ext.mts",
|
|
72
|
+
"check:lint": "eslint .",
|
|
73
|
+
"check:test": "pnpm run z:vitest:node run",
|
|
74
|
+
"check:test:cov": "pnpm run z:vitest:node run --coverage",
|
|
75
|
+
"check:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
72
76
|
"check-all": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/check-all.mts",
|
|
73
|
-
"codemod": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --uncommitted --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fmt",
|
|
74
|
-
"codemod:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --diff-from origin/main --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fmt",
|
|
75
|
-
"codemod:full": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fmt",
|
|
76
|
-
"cspell": "cspell \"**\" --gitignore --gitignore-root ../../ --no-progress",
|
|
77
77
|
"doc": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ./scripts/cmd/gen-docs.mts",
|
|
78
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
81
|
-
"
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
"lint
|
|
85
|
-
"
|
|
86
|
-
"
|
|
78
|
+
"fix:codemod": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --uncommitted --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fix:fmt",
|
|
79
|
+
"fix:codemod:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --diff-from origin/main --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fix:fmt",
|
|
80
|
+
"fix:codemod:full": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-codemod-cli/src/cmd/ts-codemod.mts --transformer append-as-const --transformer convert-to-readonly '{src,scripts,samples}/**/*.mts' && pnpm run fix:fmt",
|
|
81
|
+
"fix:fmt": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-uncommitted.mts --cwd .",
|
|
82
|
+
"fix:fmt:diff": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/format-diff-from.mts origin/main --cwd .",
|
|
83
|
+
"fix:fmt:full": "prettier --write .",
|
|
84
|
+
"fix:lint": "eslint . --fix",
|
|
85
|
+
"fmt": "pnpm run fix:fmt",
|
|
86
|
+
"gen": "run-s gen:index",
|
|
87
|
+
"gen:index": "run-s gen:index:src fix:fmt",
|
|
88
|
+
"gen:index:src": "tsx --tsconfig ../../tools/configs/tsconfig.tsx.json ../../libs/ts-repo-utils/src/cmd/gen-index-ts.mts ./src/github --index-ext .mts --export-ext .mjs --target-ext .mts --exclude cmd --exclude octokit.mts",
|
|
89
|
+
"test": "pnpm run check:test",
|
|
87
90
|
"test:cov:ui": "vite preview --outDir ./coverage",
|
|
88
91
|
"test:ui": "pnpm run z:vitest:node --ui",
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
"tscw": "node ../../node_modules/typescript-native/bin/tsc --noEmit --watch -p ./tsconfig.json",
|
|
92
|
-
"type-check": "node ../../node_modules/typescript-native/bin/tsc --noEmit",
|
|
92
|
+
"watch:test": "pnpm run z:vitest:node watch",
|
|
93
|
+
"watch:types": "node ../../node_modules/typescript-native/bin/tsc --noEmit --watch -p ./tsconfig.json",
|
|
93
94
|
"z:vitest": "vitest --config ./configs/vitest.config.mts",
|
|
94
95
|
"z:vitest:node": "pnpm run z:vitest --project='Node.js'"
|
|
95
96
|
}
|
|
@@ -13,15 +13,18 @@ const HELP = dedent`
|
|
|
13
13
|
|
|
14
14
|
Commands:
|
|
15
15
|
apply [target] ローカルの設定ファイルを GitHub に反映する
|
|
16
|
-
backup [target] GitHub
|
|
16
|
+
backup [target] GitHub の現在値でローカルの設定ファイルを撮り直す
|
|
17
17
|
|
|
18
18
|
Targets:
|
|
19
19
|
all すべて(既定)
|
|
20
20
|
repository repo-settings/repository-settings/settings.json
|
|
21
21
|
rulesets repo-settings/rulesets/*.json
|
|
22
|
-
variables
|
|
22
|
+
variables repo-settings/variables/settings.json
|
|
23
23
|
actions repo-settings/actions-settings/settings.json
|
|
24
24
|
pages repo-settings/pages/settings.json
|
|
25
|
+
environments repo-settings/environments/*.json
|
|
26
|
+
vulnerability-alerts
|
|
27
|
+
repo-settings/vulnerability-alerts/settings.json
|
|
25
28
|
|
|
26
29
|
Options:
|
|
27
30
|
--owner <owner> 対象の owner
|
|
@@ -54,6 +57,8 @@ const TARGETS = [
|
|
|
54
57
|
'variables',
|
|
55
58
|
'actions',
|
|
56
59
|
'pages',
|
|
60
|
+
'environments',
|
|
61
|
+
'vulnerability-alerts',
|
|
57
62
|
] as const;
|
|
58
63
|
|
|
59
64
|
type Target = (typeof TARGETS)[number];
|
|
@@ -125,12 +130,18 @@ const runners: ReadonlyRecord<
|
|
|
125
130
|
await github.applyActionsSettings();
|
|
126
131
|
|
|
127
132
|
await github.applyPagesSettings();
|
|
133
|
+
|
|
134
|
+
await github.applyEnvironments();
|
|
135
|
+
|
|
136
|
+
await github.applyVulnerabilityAlerts();
|
|
128
137
|
},
|
|
129
138
|
repository: github.applyRepositorySettings,
|
|
130
139
|
rulesets: github.applyRulesets,
|
|
131
140
|
variables: github.applyVariables,
|
|
132
141
|
actions: github.applyActionsSettings,
|
|
133
142
|
pages: github.applyPagesSettings,
|
|
143
|
+
environments: github.applyEnvironments,
|
|
144
|
+
'vulnerability-alerts': github.applyVulnerabilityAlerts,
|
|
134
145
|
},
|
|
135
146
|
backup: {
|
|
136
147
|
all: async () => {
|
|
@@ -141,11 +152,20 @@ const runners: ReadonlyRecord<
|
|
|
141
152
|
await github.backupActionsSettings();
|
|
142
153
|
|
|
143
154
|
await github.backupPagesSettings();
|
|
155
|
+
|
|
156
|
+
await github.backupEnvironments();
|
|
157
|
+
|
|
158
|
+
await github.backupVulnerabilityAlerts();
|
|
159
|
+
|
|
160
|
+
await github.backupVariables();
|
|
144
161
|
},
|
|
145
162
|
repository: github.backupRepositorySettings,
|
|
146
163
|
rulesets: github.backupRulesets,
|
|
164
|
+
variables: github.backupVariables,
|
|
147
165
|
actions: github.backupActionsSettings,
|
|
148
166
|
pages: github.backupPagesSettings,
|
|
167
|
+
environments: github.backupEnvironments,
|
|
168
|
+
'vulnerability-alerts': github.backupVulnerabilityAlerts,
|
|
149
169
|
},
|
|
150
170
|
} as const;
|
|
151
171
|
|
|
@@ -10,13 +10,10 @@ import {
|
|
|
10
10
|
} from 'ts-repo-utils';
|
|
11
11
|
import { actionsSettingsDir, settingsJsonName } from '../constants.mjs';
|
|
12
12
|
import { getActionsSettings, setActionsSettings } from './api/index.mjs';
|
|
13
|
-
import { backupActionsSettings } from './backup.mjs';
|
|
14
13
|
import { ActionsSettings } from './constants.mjs';
|
|
15
14
|
|
|
16
15
|
/** `repo-settings/actions-settings/settings.json` を Settings > Actions > General に反映する。 */
|
|
17
16
|
export const applyActionsSettings = async (): Promise<void> => {
|
|
18
|
-
await backupActionsSettings(false);
|
|
19
|
-
|
|
20
17
|
const settings = await readSettings();
|
|
21
18
|
|
|
22
19
|
await setActionsSettings(settings);
|
|
@@ -30,12 +27,6 @@ export const applyActionsSettings = async (): Promise<void> => {
|
|
|
30
27
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
31
28
|
await fs.writeFile(path.resolve(actionsSettingsDir, settingsJsonName), str);
|
|
32
29
|
|
|
33
|
-
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
34
|
-
await fs.writeFile(
|
|
35
|
-
path.resolve(actionsSettingsDir, 'bk', settingsJsonName),
|
|
36
|
-
str,
|
|
37
|
-
);
|
|
38
|
-
|
|
39
30
|
await formatUncommittedFiles();
|
|
40
31
|
}
|
|
41
32
|
};
|
|
@@ -2,27 +2,22 @@
|
|
|
2
2
|
import 'dotenv/config';
|
|
3
3
|
import * as fs from 'node:fs/promises';
|
|
4
4
|
import * as path from 'node:path';
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
isDirectlyExecuted,
|
|
8
|
-
makeEmptyDir,
|
|
9
|
-
} from 'ts-repo-utils';
|
|
5
|
+
import { formatUncommittedFiles, isDirectlyExecuted } from 'ts-repo-utils';
|
|
6
|
+
import { clearJsonFilesIn } from '../clear-json-files.mjs';
|
|
10
7
|
import { actionsSettingsDir, settingsJsonName } from '../constants.mjs';
|
|
11
8
|
import { getActionsSettings } from './api/index.mjs';
|
|
12
9
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
/** Settings > Actions > General の現在値を `bk/` に保存する。 */
|
|
10
|
+
/** Settings > Actions > General の現在値を宣言ファイルへ撮り直す。 */
|
|
16
11
|
export const backupActionsSettings = async (
|
|
17
12
|
fmt: boolean = true,
|
|
18
13
|
): Promise<void> => {
|
|
19
|
-
await makeEmptyDir(backupDir);
|
|
20
|
-
|
|
21
14
|
const settings = await getActionsSettings();
|
|
22
15
|
|
|
16
|
+
await clearJsonFilesIn(actionsSettingsDir);
|
|
17
|
+
|
|
23
18
|
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
24
19
|
await fs.writeFile(
|
|
25
|
-
path.resolve(
|
|
20
|
+
path.resolve(actionsSettingsDir, settingsJsonName),
|
|
26
21
|
JSON.stringify(settings, undefined, 2),
|
|
27
22
|
);
|
|
28
23
|
|
|
@@ -35,7 +35,7 @@ export const ActionsSettings = t.record({
|
|
|
35
35
|
* `read` にできるのは、 workflow 側が全て明示的に `permissions` を宣言して
|
|
36
36
|
* いる場合だけ。宣言の無い job はこの既定値をそのまま受け取るため、
|
|
37
37
|
* `write` のままだと「 branch の中身が何であれ書き込めるトークン」が
|
|
38
|
-
* 渡る。 .github/workflows/
|
|
38
|
+
* 渡る。 .github/workflows/code-check.yml 冒頭の注記を参照。
|
|
39
39
|
*/
|
|
40
40
|
default_workflow_permissions: t.enumType(['read', 'write'], {
|
|
41
41
|
typeName: 'DefaultWorkflowPermissions',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* `dir` の直下の `*.json` を消す。ディレクトリが無ければ作る。
|
|
6
|
+
*
|
|
7
|
+
* backup が書き込む前にこれを呼ぶ。撮り直したあとにそこに在るものが、そのまま
|
|
8
|
+
* 「GitHub から取れたもの」になる — 取れなかった資源は `git status` に削除と
|
|
9
|
+
* して現れる。消さずに上書きするだけだと、取れなかった資源の古いファイルが
|
|
10
|
+
* 残り、取れたのと見分けがつかない。
|
|
11
|
+
*
|
|
12
|
+
* まだ apply していない宣言も同じように削除として現れるが、それも見えた方が
|
|
13
|
+
* よい情報で、コミットしなければ失われない。
|
|
14
|
+
*
|
|
15
|
+
* `rm -rf` ではなく直下の `*.json` に限る。宣言の隣に置いてある README
|
|
16
|
+
* (`repo-settings/variables/README.md`)のような、JSON でない同居ファイルを
|
|
17
|
+
* 巻き込まないため。
|
|
18
|
+
*/
|
|
19
|
+
export const clearJsonFilesIn = async (dir: string): Promise<void> => {
|
|
20
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
21
|
+
await fs.mkdir(dir, { recursive: true });
|
|
22
|
+
|
|
23
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
24
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
25
|
+
|
|
26
|
+
for (const entry of entries) {
|
|
27
|
+
if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
28
|
+
await fs.rm(path.resolve(dir, entry.name));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises';
|
|
2
|
+
import * as os from 'node:os';
|
|
3
|
+
import * as path from 'node:path';
|
|
4
|
+
import { clearJsonFilesIn } from './clear-json-files.mjs';
|
|
5
|
+
|
|
6
|
+
const makeDir = async (): Promise<string> =>
|
|
7
|
+
fs.mkdtemp(path.join(os.tmpdir(), 'clear-json-files-'));
|
|
8
|
+
|
|
9
|
+
/** 途中のディレクトリごと作って書く。入れ子の fixture もこれで用意する。 */
|
|
10
|
+
const write = async (filePath: string, content: string): Promise<void> => {
|
|
11
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
12
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true });
|
|
13
|
+
|
|
14
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
15
|
+
await fs.writeFile(filePath, content);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const namesIn = async (dir: string): Promise<readonly string[]> =>
|
|
19
|
+
// eslint-disable-next-line security/detect-non-literal-fs-filename
|
|
20
|
+
fs.readdir(dir).then((names) => names.toSorted());
|
|
21
|
+
|
|
22
|
+
describe(clearJsonFilesIn, () => {
|
|
23
|
+
test('removes the json files directly under the directory', async () => {
|
|
24
|
+
const dir = await makeDir();
|
|
25
|
+
|
|
26
|
+
await write(path.join(dir, 'main.json'), '{}');
|
|
27
|
+
|
|
28
|
+
await write(path.join(dir, 'release.json'), '{}');
|
|
29
|
+
|
|
30
|
+
await clearJsonFilesIn(dir);
|
|
31
|
+
|
|
32
|
+
assert.deepStrictEqual(await namesIn(dir), []);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
test('keeps a file that is not json, so a README beside the declarations survives', async () => {
|
|
36
|
+
const dir = await makeDir();
|
|
37
|
+
|
|
38
|
+
await write(path.join(dir, 'settings.json'), '{}');
|
|
39
|
+
|
|
40
|
+
await write(path.join(dir, 'README.md'), '# notes');
|
|
41
|
+
|
|
42
|
+
await clearJsonFilesIn(dir);
|
|
43
|
+
|
|
44
|
+
assert.deepStrictEqual(await namesIn(dir), ['README.md']);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test('does not descend into a subdirectory', async () => {
|
|
48
|
+
const dir = await makeDir();
|
|
49
|
+
|
|
50
|
+
await write(path.join(dir, 'nested', 'deep.json'), '{}');
|
|
51
|
+
|
|
52
|
+
await clearJsonFilesIn(dir);
|
|
53
|
+
|
|
54
|
+
assert.deepStrictEqual(await namesIn(dir), ['nested']);
|
|
55
|
+
|
|
56
|
+
assert.deepStrictEqual(await namesIn(path.join(dir, 'nested')), [
|
|
57
|
+
'deep.json',
|
|
58
|
+
]);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('creates the directory when it does not exist', async () => {
|
|
62
|
+
const dir = path.join(await makeDir(), 'not-yet');
|
|
63
|
+
|
|
64
|
+
await clearJsonFilesIn(dir);
|
|
65
|
+
|
|
66
|
+
assert.deepStrictEqual(await namesIn(dir), []);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('is a no-op on a directory holding no json', async () => {
|
|
70
|
+
const dir = await makeDir();
|
|
71
|
+
|
|
72
|
+
await write(path.join(dir, 'README.md'), '# notes');
|
|
73
|
+
|
|
74
|
+
await clearJsonFilesIn(dir);
|
|
75
|
+
|
|
76
|
+
assert.deepStrictEqual(await namesIn(dir), ['README.md']);
|
|
77
|
+
});
|
|
78
|
+
});
|
package/src/github/constants.mts
CHANGED
|
@@ -25,8 +25,17 @@ export const actionsSettingsDir = path.resolve(
|
|
|
25
25
|
|
|
26
26
|
export const pagesSettingsDir = path.resolve(settingsDir, './pages');
|
|
27
27
|
|
|
28
|
+
export const environmentsDir = path.resolve(settingsDir, './environments');
|
|
29
|
+
|
|
30
|
+
export const vulnerabilityAlertsDir = path.resolve(
|
|
31
|
+
settingsDir,
|
|
32
|
+
'./vulnerability-alerts',
|
|
33
|
+
);
|
|
34
|
+
|
|
28
35
|
export const rulesetsDir = path.resolve(settingsDir, './rulesets');
|
|
29
36
|
|
|
37
|
+
export const variablesDir = path.resolve(settingsDir, './variables');
|
|
38
|
+
|
|
30
39
|
export const octokitHeaders = {
|
|
31
40
|
'X-GitHub-Api-Version': '2022-11-28',
|
|
32
41
|
} as const;
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { type EndpointKeys } from 'octokit-safe-types';
|
|
2
|
+
import { Arr } from 'ts-data-forge';
|
|
3
|
+
import * as t from 'ts-fortress';
|
|
4
|
+
import { Result } from 'ts-repo-utils';
|
|
5
|
+
import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
|
|
6
|
+
import { octokit } from '../../octokit.mjs';
|
|
7
|
+
import { EnvironmentSettings } from '../constants.mjs';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Settings > Environments の現在値を、宣言ファイルと同じ形にして返す。
|
|
11
|
+
*
|
|
12
|
+
* 名前順に整える。 GitHub が返す順は作成順で、環境を 1 つ足しただけで
|
|
13
|
+
* 宣言ファイルの中身が並び替わると差分が読めなくなるため。
|
|
14
|
+
*/
|
|
15
|
+
export const getAllEnvironments = async (): Promise<
|
|
16
|
+
readonly EnvironmentSettings[]
|
|
17
|
+
> => {
|
|
18
|
+
// https://docs.github.com/rest/deployments/environments#list-environments
|
|
19
|
+
//
|
|
20
|
+
// per_page の上限。環境は数個しか無く、 2 ページ目が要る状況は
|
|
21
|
+
// 「 GUI で何かが大量に作られた」ときだけなので、ページングは張らない。
|
|
22
|
+
const response = await octokit.request(
|
|
23
|
+
'GET /repos/{owner}/{repo}/environments' satisfies EndpointKeys,
|
|
24
|
+
{ owner: OWNER, repo: REPO, headers: octokitHeaders, per_page: 100 },
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const environments = response.data.environments ?? [];
|
|
28
|
+
|
|
29
|
+
const settings = await Promise.all(
|
|
30
|
+
environments.map(async (environment) => {
|
|
31
|
+
const policy = environment.deployment_branch_policy ?? undefined;
|
|
32
|
+
|
|
33
|
+
return EnvironmentSettings.fill({
|
|
34
|
+
name: environment.name,
|
|
35
|
+
...parseProtectionRules(environment.protection_rules),
|
|
36
|
+
deployment_branch_policy:
|
|
37
|
+
policy === undefined
|
|
38
|
+
? null
|
|
39
|
+
: {
|
|
40
|
+
protected_branches: policy.protected_branches,
|
|
41
|
+
custom_branch_policies: policy.custom_branch_policies,
|
|
42
|
+
},
|
|
43
|
+
deployment_branch_policies:
|
|
44
|
+
policy?.custom_branch_policies === true
|
|
45
|
+
? await getDeploymentBranchPolicyNames(environment.name)
|
|
46
|
+
: [],
|
|
47
|
+
});
|
|
48
|
+
}),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return settings.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/** 宣言ファイルに載る形。 `id` は GitHub 側の都合なので落とす。 */
|
|
55
|
+
const getDeploymentBranchPolicyNames = async (
|
|
56
|
+
environmentName: string,
|
|
57
|
+
): Promise<readonly Readonly<{ name: string; type: 'branch' | 'tag' }>[]> => {
|
|
58
|
+
const policies = await getDeploymentBranchPolicies(environmentName);
|
|
59
|
+
|
|
60
|
+
return policies.map(({ name, type }) => ({ name, type }));
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
/** 「 Selected branches and tags 」に登録されている pattern を列挙する。 */
|
|
64
|
+
export const getDeploymentBranchPolicies = async (
|
|
65
|
+
environmentName: string,
|
|
66
|
+
): Promise<readonly DeploymentBranchPolicy[]> => {
|
|
67
|
+
// https://docs.github.com/rest/deployments/branch-policies#list-deployment-branch-policies
|
|
68
|
+
const response = await octokit.request(
|
|
69
|
+
'GET /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies' satisfies EndpointKeys,
|
|
70
|
+
{
|
|
71
|
+
owner: OWNER,
|
|
72
|
+
repo: REPO,
|
|
73
|
+
environment_name: environmentName,
|
|
74
|
+
headers: octokitHeaders,
|
|
75
|
+
per_page: 100,
|
|
76
|
+
},
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
return response.data.branch_policies.map((policy) => ({
|
|
80
|
+
id: policy.id ?? 0,
|
|
81
|
+
name: policy.name ?? '',
|
|
82
|
+
type: policy.type === 'tag' ? 'tag' : 'branch',
|
|
83
|
+
}));
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export type DeploymentBranchPolicy = Readonly<{
|
|
87
|
+
id: number;
|
|
88
|
+
name: string;
|
|
89
|
+
type: 'branch' | 'tag';
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* GET が返す `protection_rules` を PUT の body の形に戻す。
|
|
94
|
+
*
|
|
95
|
+
* GitHub は有効な保護ルールだけを配列で返し、その要素の `type` は openapi の
|
|
96
|
+
* 型定義では素の `string` — つまり判別可能 union になっていないので、絞り込みが
|
|
97
|
+
* 効かない。実データを ts-fortress で読み直すほうが、上流の型に合わせて
|
|
98
|
+
* キャストを書くより素直で、形が変わったときに黙って通り過ぎることもない。
|
|
99
|
+
*/
|
|
100
|
+
const parseProtectionRules = (
|
|
101
|
+
protectionRules: unknown,
|
|
102
|
+
): Readonly<{
|
|
103
|
+
wait_timer: number;
|
|
104
|
+
prevent_self_review: boolean;
|
|
105
|
+
reviewers: readonly Readonly<{ type: 'User' | 'Team'; id: number }>[];
|
|
106
|
+
}> => {
|
|
107
|
+
const validationResult = ProtectionRules.validate(protectionRules ?? []);
|
|
108
|
+
|
|
109
|
+
if (Result.isErr(validationResult)) {
|
|
110
|
+
throw new Error(
|
|
111
|
+
Arr.toUnshifted(
|
|
112
|
+
'GitHub が返した environment の protection_rules を読めませんでした。',
|
|
113
|
+
)(t.validationErrorsToMessages(validationResult.value)).join('\n'),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const rules = validationResult.value;
|
|
118
|
+
|
|
119
|
+
const waitTimer = rules.find((rule) => rule.type === 'wait_timer');
|
|
120
|
+
|
|
121
|
+
const requiredReviewers = rules.find(
|
|
122
|
+
(rule) => rule.type === 'required_reviewers',
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
return {
|
|
126
|
+
wait_timer: waitTimer?.wait_timer ?? 0,
|
|
127
|
+
prevent_self_review: requiredReviewers?.prevent_self_review ?? false,
|
|
128
|
+
reviewers: (requiredReviewers?.reviewers ?? []).map((entry) => ({
|
|
129
|
+
type: entry.type,
|
|
130
|
+
id: entry.reviewer.id,
|
|
131
|
+
})),
|
|
132
|
+
};
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const ProtectionRules = t.array(
|
|
136
|
+
t.record({
|
|
137
|
+
type: t.string(''),
|
|
138
|
+
wait_timer: t.optional(t.number(0)),
|
|
139
|
+
prevent_self_review: t.optional(t.boolean(false)),
|
|
140
|
+
reviewers: t.optional(
|
|
141
|
+
t.array(
|
|
142
|
+
t.record({
|
|
143
|
+
type: t.enumType(['User', 'Team'], {
|
|
144
|
+
typeName: 'EnvironmentReviewerType',
|
|
145
|
+
defaultValue: 'User',
|
|
146
|
+
}),
|
|
147
|
+
// `reviewer` は User / Team の完全な表現だが、書き戻しに要るのは id
|
|
148
|
+
// だけ。 record は既定で余剰キーを通すので、残りはそのまま無視される。
|
|
149
|
+
reviewer: t.record({ id: t.number(0) }),
|
|
150
|
+
}),
|
|
151
|
+
),
|
|
152
|
+
),
|
|
153
|
+
}),
|
|
154
|
+
{ typeName: 'EnvironmentProtectionRules' },
|
|
155
|
+
);
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type EndpointKeys } from 'octokit-safe-types';
|
|
2
|
+
import { octokitHeaders, OWNER, REPO } from '../../constants.mjs';
|
|
3
|
+
import { octokit } from '../../octokit.mjs';
|
|
4
|
+
import { type EnvironmentSettings } from '../constants.mjs';
|
|
5
|
+
import { getDeploymentBranchPolicies } from './get-environments.mjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 環境を JSON の内容に合わせる。無ければ作られる。
|
|
9
|
+
*
|
|
10
|
+
* `PUT` は保護ルールを丸ごと置き換えるので、省いたキーは「変更しない」ではなく
|
|
11
|
+
* 「無効にする」になる。宣言ファイルをそのまま渡しているのはそのため —
|
|
12
|
+
* 部分更新にすると、ファイルから消した設定が GitHub 側に残る。
|
|
13
|
+
*/
|
|
14
|
+
export const setEnvironment = async (
|
|
15
|
+
settings: EnvironmentSettings,
|
|
16
|
+
): Promise<void> => {
|
|
17
|
+
// https://docs.github.com/rest/deployments/environments#create-or-update-an-environment
|
|
18
|
+
await octokit.request(
|
|
19
|
+
'PUT /repos/{owner}/{repo}/environments/{environment_name}' satisfies EndpointKeys,
|
|
20
|
+
{
|
|
21
|
+
owner: OWNER,
|
|
22
|
+
repo: REPO,
|
|
23
|
+
environment_name: settings.name,
|
|
24
|
+
headers: octokitHeaders,
|
|
25
|
+
wait_timer: settings.wait_timer,
|
|
26
|
+
prevent_self_review: settings.prevent_self_review,
|
|
27
|
+
reviewers: settings.reviewers.map((reviewer) => ({
|
|
28
|
+
type: reviewer.type,
|
|
29
|
+
id: reviewer.id,
|
|
30
|
+
})),
|
|
31
|
+
deployment_branch_policy:
|
|
32
|
+
settings.deployment_branch_policy === null
|
|
33
|
+
? null
|
|
34
|
+
: {
|
|
35
|
+
protected_branches:
|
|
36
|
+
settings.deployment_branch_policy.protected_branches,
|
|
37
|
+
custom_branch_policies:
|
|
38
|
+
settings.deployment_branch_policy.custom_branch_policies,
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
await setDeploymentBranchPolicies(settings);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 「 Selected branches and tags 」の pattern を宣言に合わせる。
|
|
48
|
+
*
|
|
49
|
+
* 個別の追加・削除しか無いエンドポイントなので、現在値との差分を取って埋める。
|
|
50
|
+
* 名前と種別が一致するものは触らない — 消して作り直すと id が変わり、
|
|
51
|
+
* その間だけ deploy できない ref ができる。
|
|
52
|
+
*/
|
|
53
|
+
const keyOf = (policy: Readonly<{ name: string; type: string }>): string =>
|
|
54
|
+
`${policy.type}:${policy.name}` as const;
|
|
55
|
+
|
|
56
|
+
const setDeploymentBranchPolicies = async (
|
|
57
|
+
settings: EnvironmentSettings,
|
|
58
|
+
): Promise<void> => {
|
|
59
|
+
if (settings.deployment_branch_policy?.custom_branch_policies !== true) {
|
|
60
|
+
// 選択されていないなら pattern は存在し得ない。 PUT 側が既に消している。
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const current = await getDeploymentBranchPolicies(settings.name);
|
|
65
|
+
|
|
66
|
+
const desiredKeys: ReadonlySet<string> = new Set(
|
|
67
|
+
settings.deployment_branch_policies.map(keyOf),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
const currentKeys: ReadonlySet<string> = new Set(current.map(keyOf));
|
|
71
|
+
|
|
72
|
+
const toDelete = current.filter((p) => !desiredKeys.has(keyOf(p)));
|
|
73
|
+
|
|
74
|
+
const toCreate = settings.deployment_branch_policies.filter(
|
|
75
|
+
(p) => !currentKeys.has(keyOf(p)),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
for (const policy of toDelete) {
|
|
79
|
+
// https://docs.github.com/rest/deployments/branch-policies#delete-a-deployment-branch-policy
|
|
80
|
+
await octokit.request(
|
|
81
|
+
'DELETE /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies/{branch_policy_id}' satisfies EndpointKeys,
|
|
82
|
+
{
|
|
83
|
+
owner: OWNER,
|
|
84
|
+
repo: REPO,
|
|
85
|
+
environment_name: settings.name,
|
|
86
|
+
branch_policy_id: policy.id,
|
|
87
|
+
headers: octokitHeaders,
|
|
88
|
+
},
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const policy of toCreate) {
|
|
93
|
+
// https://docs.github.com/rest/deployments/branch-policies#create-a-deployment-branch-policy
|
|
94
|
+
await octokit.request(
|
|
95
|
+
'POST /repos/{owner}/{repo}/environments/{environment_name}/deployment-branch-policies' satisfies EndpointKeys,
|
|
96
|
+
{
|
|
97
|
+
owner: OWNER,
|
|
98
|
+
repo: REPO,
|
|
99
|
+
environment_name: settings.name,
|
|
100
|
+
headers: octokitHeaders,
|
|
101
|
+
name: policy.name,
|
|
102
|
+
type: policy.type,
|
|
103
|
+
},
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
};
|