@lilpacy/setup-github-rules 0.1.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/LICENSE +21 -0
- package/README.md +160 -0
- package/bin/setup-github-rules.js +324 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @lilpacy/setup-github-rules
|
|
2
|
+
|
|
3
|
+
`npx` で実行できる、GitHub repository ruleset の one-shot setup CLI です。
|
|
4
|
+
|
|
5
|
+
Terraform は使いません。`terraform.tfstate` も生成しません。内部では GitHub CLI の `gh api` を使って、対象 repository に対して直接 GitHub API を実行します。
|
|
6
|
+
|
|
7
|
+
## できること
|
|
8
|
+
|
|
9
|
+
- 現在の git remote から `OWNER/REPO` を自動検出
|
|
10
|
+
- 対話的に default branch を選択
|
|
11
|
+
- `main`
|
|
12
|
+
- `develop`
|
|
13
|
+
- 任意の branch
|
|
14
|
+
- branch が存在しなければ、現在の GitHub default branch から作成
|
|
15
|
+
- repository の `default_branch` を選択した branch に変更
|
|
16
|
+
- 選択した branch に対して PR 必須 ruleset を作成または更新
|
|
17
|
+
- branch deletion を禁止
|
|
18
|
+
- non-fast-forward / force push 系を禁止
|
|
19
|
+
- 既存の同名 ruleset がある場合は更新するので、再実行しやすい
|
|
20
|
+
|
|
21
|
+
## 前提
|
|
22
|
+
|
|
23
|
+
- Node.js 18+
|
|
24
|
+
- git
|
|
25
|
+
- GitHub CLI `gh`
|
|
26
|
+
- `gh auth login` 済み
|
|
27
|
+
- 対象 repository に対する admin 権限
|
|
28
|
+
|
|
29
|
+
確認:
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
gh auth status
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## 使い方
|
|
36
|
+
|
|
37
|
+
### 対話式で実行
|
|
38
|
+
|
|
39
|
+
対象 repository の中で実行します。
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npx @lilpacy/setup-github-rules
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
実行すると、default branch を選べます。
|
|
46
|
+
|
|
47
|
+
```txt
|
|
48
|
+
Choose the repository default branch:
|
|
49
|
+
1) main
|
|
50
|
+
2) develop
|
|
51
|
+
3) other
|
|
52
|
+
Select [1/2/3]:
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### repository を明示する
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @lilpacy/setup-github-rules --repo lilpacy/repo-a
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### branch を非対話で指定する
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx @lilpacy/setup-github-rules --repo lilpacy/repo-a --branch develop --yes
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### required approvals を指定する
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
npx @lilpacy/setup-github-rules \
|
|
71
|
+
--repo lilpacy/repo-a \
|
|
72
|
+
--branch develop \
|
|
73
|
+
--required-approvals 1 \
|
|
74
|
+
--yes
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### dry-run
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
npx @lilpacy/setup-github-rules --repo lilpacy/repo-a --dry-run
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## オプション
|
|
84
|
+
|
|
85
|
+
| Option | Description | Default |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| `--repo OWNER/REPO` | 対象 repository | current git remote から検出 |
|
|
88
|
+
| `--branch BRANCH` | default branch / protected branch | 対話式で選択 |
|
|
89
|
+
| `--required-approvals N` | 必須 approval 数 | `1` |
|
|
90
|
+
| `--ruleset-name NAME` | ruleset 名 | `Require PR to <branch>` |
|
|
91
|
+
| `--yes`, `-y` | 最終確認をスキップ | `false` |
|
|
92
|
+
| `--dry-run` | 変更せず plan だけ表示 | `false` |
|
|
93
|
+
| `--help`, `-h` | help 表示 | - |
|
|
94
|
+
|
|
95
|
+
## 実行例
|
|
96
|
+
|
|
97
|
+
```txt
|
|
98
|
+
Plan:
|
|
99
|
+
Repository: lilpacy/repo-a
|
|
100
|
+
Current default: main
|
|
101
|
+
New default: develop
|
|
102
|
+
Protected branch: develop
|
|
103
|
+
Required approvals: 1
|
|
104
|
+
Ruleset name: Require PR to develop
|
|
105
|
+
|
|
106
|
+
Apply these changes? [y/N]: y
|
|
107
|
+
Creating branch 'develop' from 'main'...
|
|
108
|
+
Setting default branch to 'develop'...
|
|
109
|
+
Creating ruleset 'Require PR to develop'...
|
|
110
|
+
|
|
111
|
+
Done.
|
|
112
|
+
Default branch 'develop' now requires Pull Requests before changes can be merged.
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## ローカル確認
|
|
116
|
+
|
|
117
|
+
publish 前にローカルで試す場合:
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
cd setup-github-rules-cli
|
|
121
|
+
npm link
|
|
122
|
+
setup-github-rules --help
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
または package directory を直接指定して実行できます。
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npx /path/to/setup-github-rules-cli --help
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## npm に公開する
|
|
132
|
+
|
|
133
|
+
この repository は scoped package `@lilpacy/setup-github-rules` として公開する前提です。
|
|
134
|
+
`package.json` には `publishConfig.access=public` を入れているので、初回 publish でも `--access public` を毎回付ける必要はありません。
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
npm login
|
|
138
|
+
npm run prepublishOnly
|
|
139
|
+
npm publish
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
公開確認:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
npm view @lilpacy/setup-github-rules version
|
|
146
|
+
npx @lilpacy/setup-github-rules --help
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## 注意点
|
|
150
|
+
|
|
151
|
+
この CLI は Terraform ではありません。state や desired state 管理はありません。
|
|
152
|
+
|
|
153
|
+
その代わり、以下の用途に向いています。
|
|
154
|
+
|
|
155
|
+
- 新規 repository 作成直後に ruleset をすぐ反映したい
|
|
156
|
+
- `gh` 認証をそのまま使いたい
|
|
157
|
+
- repository 内に IaC ファイルや state を残したくない
|
|
158
|
+
- 小〜中規模で、ワンコマンド setup を優先したい
|
|
159
|
+
|
|
160
|
+
厳密な drift detection、PR review 付きの設定変更、org 全体の一元管理が必要な場合は Terraform / OpenTofu 方式のほうが向いています。
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import readline from "node:readline/promises";
|
|
8
|
+
import { stdin as input, stdout as output } from "node:process";
|
|
9
|
+
|
|
10
|
+
const API_VERSION = "2022-11-28";
|
|
11
|
+
const DEFAULT_RULESET_NAME_PREFIX = "Require PR to";
|
|
12
|
+
|
|
13
|
+
function parseArgs(argv) {
|
|
14
|
+
const args = {
|
|
15
|
+
repo: null,
|
|
16
|
+
branch: null,
|
|
17
|
+
approvals: null,
|
|
18
|
+
yes: false,
|
|
19
|
+
dryRun: false,
|
|
20
|
+
help: false,
|
|
21
|
+
rulesetName: null
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
25
|
+
const arg = argv[i];
|
|
26
|
+
if (arg === "--repo") args.repo = argv[++i];
|
|
27
|
+
else if (arg === "--branch") args.branch = argv[++i];
|
|
28
|
+
else if (arg === "--required-approvals") args.approvals = Number(argv[++i]);
|
|
29
|
+
else if (arg === "--ruleset-name") args.rulesetName = argv[++i];
|
|
30
|
+
else if (arg === "--yes" || arg === "-y") args.yes = true;
|
|
31
|
+
else if (arg === "--dry-run") args.dryRun = true;
|
|
32
|
+
else if (arg === "--help" || arg === "-h") args.help = true;
|
|
33
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (args.approvals !== null && (!Number.isInteger(args.approvals) || args.approvals < 0 || args.approvals > 6)) {
|
|
37
|
+
throw new Error("--required-approvals must be an integer between 0 and 6.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return args;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function printHelp() {
|
|
44
|
+
console.log(`setup-github-rules
|
|
45
|
+
|
|
46
|
+
One-shot GitHub repository setup using the GitHub CLI.
|
|
47
|
+
|
|
48
|
+
Usage:
|
|
49
|
+
npx @lilpacy/setup-github-rules
|
|
50
|
+
npx @lilpacy/setup-github-rules --repo OWNER/REPO
|
|
51
|
+
npx @lilpacy/setup-github-rules --repo OWNER/REPO --branch develop --yes
|
|
52
|
+
|
|
53
|
+
Options:
|
|
54
|
+
--repo OWNER/REPO Target repository. Defaults to current git remote.
|
|
55
|
+
--branch BRANCH Default branch to set and protect. Skips branch prompt.
|
|
56
|
+
--required-approvals N Required approving reviews. Default: 1.
|
|
57
|
+
--ruleset-name NAME Ruleset name. Default: "Require PR to <branch>".
|
|
58
|
+
--yes, -y Skip final confirmation.
|
|
59
|
+
--dry-run Print planned operations without changing GitHub.
|
|
60
|
+
--help, -h Show this help.
|
|
61
|
+
|
|
62
|
+
What it does:
|
|
63
|
+
1. Detects or accepts OWNER/REPO.
|
|
64
|
+
2. Lets you choose main, develop, or another default branch.
|
|
65
|
+
3. Creates the branch from the current GitHub default branch if missing.
|
|
66
|
+
4. Updates the repository default_branch.
|
|
67
|
+
5. Creates or updates a branch ruleset that requires Pull Requests.
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function run(command, args, options = {}) {
|
|
72
|
+
const result = spawnSync(command, args, {
|
|
73
|
+
encoding: "utf8",
|
|
74
|
+
stdio: options.stdio ?? ["ignore", "pipe", "pipe"],
|
|
75
|
+
input: options.input,
|
|
76
|
+
cwd: options.cwd ?? process.cwd()
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (result.error) {
|
|
80
|
+
throw new Error(`Failed to run ${command}: ${result.error.message}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (result.status !== 0) {
|
|
84
|
+
const stderr = result.stderr?.trim();
|
|
85
|
+
const stdout = result.stdout?.trim();
|
|
86
|
+
throw new Error([`Command failed: ${command} ${args.join(" ")}`, stderr, stdout].filter(Boolean).join("\n"));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return result.stdout?.trim() ?? "";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function commandExists(command) {
|
|
93
|
+
const result = spawnSync(command, ["--version"], { encoding: "utf8", stdio: "ignore" });
|
|
94
|
+
return !result.error && result.status === 0;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function detectRepoFromGitRemote() {
|
|
98
|
+
const remoteUrl = run("git", ["remote", "get-url", "origin"]);
|
|
99
|
+
|
|
100
|
+
const sshMatch = remoteUrl.match(/^git@github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
101
|
+
if (sshMatch) return `${sshMatch[1]}/${sshMatch[2]}`;
|
|
102
|
+
|
|
103
|
+
const httpsMatch = remoteUrl.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
|
104
|
+
if (httpsMatch) return `${httpsMatch[1]}/${httpsMatch[2]}`;
|
|
105
|
+
|
|
106
|
+
throw new Error(`Could not detect OWNER/REPO from origin remote: ${remoteUrl}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function splitRepo(repo) {
|
|
110
|
+
const match = repo?.match(/^([^/\s]+)\/([^/\s]+)$/);
|
|
111
|
+
if (!match) throw new Error("Repository must be in OWNER/REPO format.");
|
|
112
|
+
return { owner: match[1], repo: match[2] };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function ghApi(endpoint, { method = "GET", body = null, silent404 = false } = {}) {
|
|
116
|
+
const args = [
|
|
117
|
+
"api",
|
|
118
|
+
`--method=${method}`,
|
|
119
|
+
"-H",
|
|
120
|
+
"Accept: application/vnd.github+json",
|
|
121
|
+
"-H",
|
|
122
|
+
`X-GitHub-Api-Version: ${API_VERSION}`,
|
|
123
|
+
endpoint
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
let tempDir = null;
|
|
127
|
+
try {
|
|
128
|
+
if (body !== null) {
|
|
129
|
+
tempDir = mkdtempSync(path.join(tmpdir(), "setup-github-rules-"));
|
|
130
|
+
const bodyPath = path.join(tempDir, "body.json");
|
|
131
|
+
writeFileSync(bodyPath, JSON.stringify(body, null, 2));
|
|
132
|
+
args.push("--input", bodyPath);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const result = spawnSync("gh", args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
136
|
+
if (result.status === 0) {
|
|
137
|
+
const stdout = result.stdout.trim();
|
|
138
|
+
return stdout ? JSON.parse(stdout) : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (silent404 && result.stderr.includes("HTTP 404")) return null;
|
|
142
|
+
|
|
143
|
+
throw new Error([`gh api failed: gh ${args.join(" ")}`, result.stderr.trim(), result.stdout.trim()].filter(Boolean).join("\n"));
|
|
144
|
+
} finally {
|
|
145
|
+
if (tempDir) rmSync(tempDir, { recursive: true, force: true });
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function selectBranch(rl, preselectedBranch) {
|
|
150
|
+
if (preselectedBranch) return preselectedBranch;
|
|
151
|
+
|
|
152
|
+
console.log("\nChoose the repository default branch:");
|
|
153
|
+
console.log(" 1) main");
|
|
154
|
+
console.log(" 2) develop");
|
|
155
|
+
console.log(" 3) other");
|
|
156
|
+
|
|
157
|
+
while (true) {
|
|
158
|
+
const answer = (await rl.question("Select [1/2/3]: ")).trim();
|
|
159
|
+
if (answer === "1" || answer.toLowerCase() === "main") return "main";
|
|
160
|
+
if (answer === "2" || answer.toLowerCase() === "develop") return "develop";
|
|
161
|
+
if (answer === "3" || answer.toLowerCase() === "other") {
|
|
162
|
+
const branch = (await rl.question("Branch name: ")).trim();
|
|
163
|
+
if (isValidBranchName(branch)) return branch;
|
|
164
|
+
console.log("Please enter a valid branch name.");
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
console.log("Please choose 1, 2, or 3.");
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function isValidBranchName(branch) {
|
|
172
|
+
return Boolean(branch) &&
|
|
173
|
+
!branch.startsWith("/") &&
|
|
174
|
+
!branch.endsWith("/") &&
|
|
175
|
+
!branch.includes("..") &&
|
|
176
|
+
!branch.includes(" ") &&
|
|
177
|
+
!branch.includes("~") &&
|
|
178
|
+
!branch.includes("^") &&
|
|
179
|
+
!branch.includes(":") &&
|
|
180
|
+
!branch.includes("?") &&
|
|
181
|
+
!branch.includes("*") &&
|
|
182
|
+
!branch.includes("[") &&
|
|
183
|
+
!branch.includes("\\") &&
|
|
184
|
+
!branch.endsWith(".lock");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function makeRulesetPayload({ branch, rulesetName, approvals }) {
|
|
188
|
+
return {
|
|
189
|
+
name: rulesetName,
|
|
190
|
+
target: "branch",
|
|
191
|
+
enforcement: "active",
|
|
192
|
+
conditions: {
|
|
193
|
+
ref_name: {
|
|
194
|
+
include: [`refs/heads/${branch}`],
|
|
195
|
+
exclude: []
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
rules: [
|
|
199
|
+
{
|
|
200
|
+
type: "pull_request",
|
|
201
|
+
parameters: {
|
|
202
|
+
required_approving_review_count: approvals,
|
|
203
|
+
dismiss_stale_reviews_on_push: true,
|
|
204
|
+
require_code_owner_review: false,
|
|
205
|
+
require_last_push_approval: false,
|
|
206
|
+
required_review_thread_resolution: false
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
{ type: "deletion" },
|
|
210
|
+
{ type: "non_fast_forward" }
|
|
211
|
+
],
|
|
212
|
+
bypass_actors: []
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function branchExists(owner, repo, branch) {
|
|
217
|
+
return ghApi(`/repos/${owner}/${repo}/branches/${encodeURIComponent(branch)}`, { silent404: true }) !== null;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function createBranchFrom(owner, repo, newBranch, sourceBranch) {
|
|
221
|
+
const sourceRef = ghApi(`/repos/${owner}/${repo}/git/ref/heads/${encodeURIComponent(sourceBranch)}`);
|
|
222
|
+
ghApi(`/repos/${owner}/${repo}/git/refs`, {
|
|
223
|
+
method: "POST",
|
|
224
|
+
body: {
|
|
225
|
+
ref: `refs/heads/${newBranch}`,
|
|
226
|
+
sha: sourceRef.object.sha
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function findExistingRuleset(owner, repo, rulesetName) {
|
|
232
|
+
const rulesets = ghApi(`/repos/${owner}/${repo}/rulesets`);
|
|
233
|
+
return Array.isArray(rulesets) ? rulesets.find((ruleset) => ruleset.name === rulesetName) : null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function main() {
|
|
237
|
+
const args = parseArgs(process.argv.slice(2));
|
|
238
|
+
if (args.help) {
|
|
239
|
+
printHelp();
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!commandExists("git")) throw new Error("git is required.");
|
|
244
|
+
if (!commandExists("gh")) throw new Error("GitHub CLI is required. Install gh and run `gh auth login` first.");
|
|
245
|
+
|
|
246
|
+
run("gh", ["auth", "status"], { stdio: ["ignore", "ignore", "pipe"] });
|
|
247
|
+
|
|
248
|
+
const repoFullName = args.repo ?? detectRepoFromGitRemote();
|
|
249
|
+
const { owner, repo } = splitRepo(repoFullName);
|
|
250
|
+
|
|
251
|
+
const rl = readline.createInterface({ input, output });
|
|
252
|
+
try {
|
|
253
|
+
const repoInfo = ghApi(`/repos/${owner}/${repo}`);
|
|
254
|
+
const currentDefaultBranch = repoInfo.default_branch;
|
|
255
|
+
const selectedBranch = await selectBranch(rl, args.branch);
|
|
256
|
+
const approvals = args.approvals ?? 1;
|
|
257
|
+
const rulesetName = args.rulesetName ?? `${DEFAULT_RULESET_NAME_PREFIX} ${selectedBranch}`;
|
|
258
|
+
|
|
259
|
+
console.log("\nPlan:");
|
|
260
|
+
console.log(` Repository: ${owner}/${repo}`);
|
|
261
|
+
console.log(` Current default: ${currentDefaultBranch}`);
|
|
262
|
+
console.log(` New default: ${selectedBranch}`);
|
|
263
|
+
console.log(` Protected branch: ${selectedBranch}`);
|
|
264
|
+
console.log(` Required approvals: ${approvals}`);
|
|
265
|
+
console.log(` Ruleset name: ${rulesetName}`);
|
|
266
|
+
|
|
267
|
+
if (args.dryRun) {
|
|
268
|
+
console.log("\nDry run only. No changes were made.");
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (!args.yes) {
|
|
273
|
+
const confirm = (await rl.question("\nApply these changes? [y/N]: ")).trim().toLowerCase();
|
|
274
|
+
if (confirm !== "y" && confirm !== "yes") {
|
|
275
|
+
console.log("Cancelled. No changes were made.");
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
if (!branchExists(owner, repo, selectedBranch)) {
|
|
281
|
+
console.log(`Creating branch '${selectedBranch}' from '${currentDefaultBranch}'...`);
|
|
282
|
+
createBranchFrom(owner, repo, selectedBranch, currentDefaultBranch);
|
|
283
|
+
} else {
|
|
284
|
+
console.log(`Branch '${selectedBranch}' already exists.`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
if (currentDefaultBranch !== selectedBranch) {
|
|
288
|
+
console.log(`Setting default branch to '${selectedBranch}'...`);
|
|
289
|
+
ghApi(`/repos/${owner}/${repo}`, {
|
|
290
|
+
method: "PATCH",
|
|
291
|
+
body: { default_branch: selectedBranch }
|
|
292
|
+
});
|
|
293
|
+
} else {
|
|
294
|
+
console.log(`Default branch is already '${selectedBranch}'.`);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const payload = makeRulesetPayload({ branch: selectedBranch, rulesetName, approvals });
|
|
298
|
+
const existing = findExistingRuleset(owner, repo, rulesetName);
|
|
299
|
+
|
|
300
|
+
if (existing) {
|
|
301
|
+
console.log(`Updating existing ruleset '${rulesetName}'...`);
|
|
302
|
+
ghApi(`/repos/${owner}/${repo}/rulesets/${existing.id}`, {
|
|
303
|
+
method: "PUT",
|
|
304
|
+
body: payload
|
|
305
|
+
});
|
|
306
|
+
} else {
|
|
307
|
+
console.log(`Creating ruleset '${rulesetName}'...`);
|
|
308
|
+
ghApi(`/repos/${owner}/${repo}/rulesets`, {
|
|
309
|
+
method: "POST",
|
|
310
|
+
body: payload
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
console.log("\nDone.");
|
|
315
|
+
console.log(`Default branch '${selectedBranch}' now requires Pull Requests before changes can be merged.`);
|
|
316
|
+
} finally {
|
|
317
|
+
rl.close();
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
main().catch((error) => {
|
|
322
|
+
console.error(`\nError: ${error.message}`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lilpacy/setup-github-rules",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "One-shot interactive GitHub repository rules setup using gh CLI. No Terraform state, no generated files.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"setup-github-rules": "./bin/setup-github-rules.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"README.md",
|
|
12
|
+
"LICENSE"
|
|
13
|
+
],
|
|
14
|
+
"scripts": {
|
|
15
|
+
"check": "node --check ./bin/setup-github-rules.js",
|
|
16
|
+
"test": "node --test",
|
|
17
|
+
"prepublishOnly": "npm run check && npm test && npm pack --dry-run",
|
|
18
|
+
"start": "node ./bin/setup-github-rules.js"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"cli",
|
|
22
|
+
"github",
|
|
23
|
+
"gh",
|
|
24
|
+
"ruleset",
|
|
25
|
+
"branch-protection",
|
|
26
|
+
"pull-request"
|
|
27
|
+
],
|
|
28
|
+
"author": "lilpacy",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/lilpacy/setup-github-rules-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/lilpacy/setup-github-rules-cli/issues"
|
|
36
|
+
},
|
|
37
|
+
"homepage": "https://github.com/lilpacy/setup-github-rules-cli#readme",
|
|
38
|
+
"engines": {
|
|
39
|
+
"node": ">=18"
|
|
40
|
+
},
|
|
41
|
+
"publishConfig": {
|
|
42
|
+
"access": "public"
|
|
43
|
+
}
|
|
44
|
+
}
|