@asagiri-design/labels-config 0.2.2 → 0.3.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 +62 -0
- package/dist/chunk-MM4GCVPE.mjs +549 -0
- package/dist/cli.js +522 -98
- package/dist/cli.mjs +256 -103
- package/dist/github/index.d.mts +49 -1
- package/dist/github/index.d.ts +49 -1
- package/dist/github/index.js +212 -0
- package/dist/github/index.mjs +3 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.mjs +1 -1
- package/docs/BATCH_SYNC.md +396 -0
- package/docs/NPM_SETUP.md +200 -0
- package/docs/QUICK_START.md +457 -0
- package/docs/RELEASE_FLOW.md +436 -0
- package/package.json +2 -6
package/README.md
CHANGED
|
@@ -281,6 +281,66 @@ jobs:
|
|
|
281
281
|
|
|
282
282
|
---
|
|
283
283
|
|
|
284
|
+
## 🔐 Security & Publishing
|
|
285
|
+
|
|
286
|
+
### Automated npm Publishing with Trusted Publishers (OIDC)
|
|
287
|
+
|
|
288
|
+
This package uses **npm Trusted Publishers (OIDC)** for secure, automated publishing. No long-lived tokens required!
|
|
289
|
+
|
|
290
|
+
**Key Features:**
|
|
291
|
+
- ✅ Zero token management - GitHub Actions authenticates automatically
|
|
292
|
+
- ✅ Enhanced security - No risk of token leakage
|
|
293
|
+
- ✅ Automatic provenance - Package authenticity verification
|
|
294
|
+
- ✅ Auto-release on merge - Patch version published when merging to main
|
|
295
|
+
|
|
296
|
+
### How It Works
|
|
297
|
+
|
|
298
|
+
**Automatic Release (main branch):**
|
|
299
|
+
```bash
|
|
300
|
+
# When you merge to main, the system automatically:
|
|
301
|
+
# 1. Bumps the patch version (e.g., 0.2.0 → 0.2.1)
|
|
302
|
+
# 2. Builds the package
|
|
303
|
+
# 3. Publishes to npm with provenance
|
|
304
|
+
# 4. Pushes the version tag
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**Manual Release:**
|
|
308
|
+
Use GitHub Actions UI to trigger manual releases with custom version bumps:
|
|
309
|
+
- Navigate to **Actions** → **Manual Release**
|
|
310
|
+
- Choose version type: `patch` / `minor` / `major` / `prerelease`
|
|
311
|
+
- Run the workflow
|
|
312
|
+
|
|
313
|
+
**Skip Auto-Release:**
|
|
314
|
+
Include `[skip ci]` or `[no release]` in your commit message:
|
|
315
|
+
```bash
|
|
316
|
+
git commit -m "docs: update README [skip ci]"
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Setup Required
|
|
320
|
+
|
|
321
|
+
For maintainers: OIDC publishing requires one-time setup in npm. See the detailed guide:
|
|
322
|
+
|
|
323
|
+
📖 **[Complete OIDC Setup Guide (in Japanese)](./docs/NPM_SETUP.md)**
|
|
324
|
+
|
|
325
|
+
> **Note:** npm Trusted Publisher setup must be done via [npmjs.com](https://www.npmjs.com/) Web UI.
|
|
326
|
+
> CLI/terminal configuration is not currently supported by npm.
|
|
327
|
+
|
|
328
|
+
### Migration from Local Release Scripts
|
|
329
|
+
|
|
330
|
+
The following npm scripts have been **removed** (now automated via GitHub Actions):
|
|
331
|
+
|
|
332
|
+
```bash
|
|
333
|
+
# 🚫 No longer available
|
|
334
|
+
npm run release:patch
|
|
335
|
+
npm run release:minor
|
|
336
|
+
npm run release:major
|
|
337
|
+
npm run release:beta
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
All releases are now handled automatically through GitHub Actions workflows.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
284
344
|
## Troubleshooting
|
|
285
345
|
|
|
286
346
|
### Authentication failed
|
|
@@ -381,6 +441,8 @@ MIT
|
|
|
381
441
|
## Related
|
|
382
442
|
|
|
383
443
|
- [日本語 README](./README.ja.md)
|
|
444
|
+
- [npm Publish Release Flow (in Japanese)](./docs/RELEASE_FLOW.md)
|
|
445
|
+
- [npm OIDC Setup Guide (in Japanese)](./docs/NPM_SETUP.md)
|
|
384
446
|
|
|
385
447
|
---
|
|
386
448
|
|
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__publicField
|
|
3
|
+
} from "./chunk-QZ7TP4HQ.mjs";
|
|
4
|
+
|
|
5
|
+
// src/github/client.ts
|
|
6
|
+
import { execSync } from "child_process";
|
|
7
|
+
var GitHubClient = class {
|
|
8
|
+
constructor(options) {
|
|
9
|
+
__publicField(this, "owner");
|
|
10
|
+
__publicField(this, "repo");
|
|
11
|
+
__publicField(this, "repoPath");
|
|
12
|
+
this.owner = options.owner;
|
|
13
|
+
this.repo = options.repo;
|
|
14
|
+
this.repoPath = `${this.owner}/${this.repo}`;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Execute gh CLI command
|
|
18
|
+
*/
|
|
19
|
+
exec(command) {
|
|
20
|
+
try {
|
|
21
|
+
return execSync(command, {
|
|
22
|
+
encoding: "utf-8",
|
|
23
|
+
stdio: ["pipe", "pipe", "pipe"]
|
|
24
|
+
}).trim();
|
|
25
|
+
} catch (error2) {
|
|
26
|
+
throw new Error(`gh CLI command failed: ${error2.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Fetch all labels from repository
|
|
31
|
+
*/
|
|
32
|
+
async fetchLabels() {
|
|
33
|
+
try {
|
|
34
|
+
const output = this.exec(
|
|
35
|
+
`gh label list --repo ${this.repoPath} --json name,color,description --limit 1000`
|
|
36
|
+
);
|
|
37
|
+
if (!output) {
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
const labels = JSON.parse(output);
|
|
41
|
+
return labels.map((label) => ({
|
|
42
|
+
name: label.name,
|
|
43
|
+
color: label.color,
|
|
44
|
+
description: label.description || ""
|
|
45
|
+
}));
|
|
46
|
+
} catch (error2) {
|
|
47
|
+
throw new Error(`Failed to fetch labels from ${this.repoPath}: ${error2}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Create a new label
|
|
52
|
+
*/
|
|
53
|
+
async createLabel(label) {
|
|
54
|
+
try {
|
|
55
|
+
const name = label.name.replace(/"/g, '\\"');
|
|
56
|
+
const description = label.description.replace(/"/g, '\\"');
|
|
57
|
+
const color = label.color.replace("#", "");
|
|
58
|
+
this.exec(
|
|
59
|
+
`gh label create "${name}" --color "${color}" --description "${description}" --repo ${this.repoPath}`
|
|
60
|
+
);
|
|
61
|
+
return {
|
|
62
|
+
name: label.name,
|
|
63
|
+
color: label.color,
|
|
64
|
+
description: label.description
|
|
65
|
+
};
|
|
66
|
+
} catch (error2) {
|
|
67
|
+
throw new Error(`Failed to create label "${label.name}": ${error2}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Update an existing label
|
|
72
|
+
*/
|
|
73
|
+
async updateLabel(currentName, label) {
|
|
74
|
+
try {
|
|
75
|
+
const escapedCurrentName = currentName.replace(/"/g, '\\"');
|
|
76
|
+
const args = [];
|
|
77
|
+
if (label.name && label.name !== currentName) {
|
|
78
|
+
args.push(`--name "${label.name.replace(/"/g, '\\"')}"`);
|
|
79
|
+
}
|
|
80
|
+
if (label.color) {
|
|
81
|
+
args.push(`--color "${label.color.replace("#", "")}"`);
|
|
82
|
+
}
|
|
83
|
+
if (label.description !== void 0) {
|
|
84
|
+
args.push(`--description "${label.description.replace(/"/g, '\\"')}"`);
|
|
85
|
+
}
|
|
86
|
+
if (args.length === 0) {
|
|
87
|
+
return {
|
|
88
|
+
name: currentName,
|
|
89
|
+
color: label.color || "",
|
|
90
|
+
description: label.description || ""
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
this.exec(
|
|
94
|
+
`gh label edit "${escapedCurrentName}" ${args.join(" ")} --repo ${this.repoPath}`
|
|
95
|
+
);
|
|
96
|
+
return {
|
|
97
|
+
name: label.name || currentName,
|
|
98
|
+
color: label.color || "",
|
|
99
|
+
description: label.description || ""
|
|
100
|
+
};
|
|
101
|
+
} catch (error2) {
|
|
102
|
+
throw new Error(`Failed to update label "${currentName}": ${error2}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Delete a label
|
|
107
|
+
*/
|
|
108
|
+
async deleteLabel(name) {
|
|
109
|
+
try {
|
|
110
|
+
const escapedName = name.replace(/"/g, '\\"');
|
|
111
|
+
this.exec(`gh label delete "${escapedName}" --repo ${this.repoPath} --yes`);
|
|
112
|
+
} catch (error2) {
|
|
113
|
+
throw new Error(`Failed to delete label "${name}": ${error2}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* Check if label exists
|
|
118
|
+
*/
|
|
119
|
+
async hasLabel(name) {
|
|
120
|
+
try {
|
|
121
|
+
const labels = await this.fetchLabels();
|
|
122
|
+
return labels.some((label) => label.name.toLowerCase() === name.toLowerCase());
|
|
123
|
+
} catch (error2) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// src/github/sync.ts
|
|
130
|
+
var _GitHubLabelSync = class _GitHubLabelSync {
|
|
131
|
+
constructor(options) {
|
|
132
|
+
__publicField(this, "client");
|
|
133
|
+
__publicField(this, "options");
|
|
134
|
+
this.client = new GitHubClient(options);
|
|
135
|
+
this.options = options;
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Log message if verbose mode is enabled
|
|
139
|
+
*/
|
|
140
|
+
log(message) {
|
|
141
|
+
if (this.options.verbose) {
|
|
142
|
+
console.log(`[labels-config] ${message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Sync labels to GitHub repository with batch operations for better performance
|
|
147
|
+
*/
|
|
148
|
+
async syncLabels(localLabels) {
|
|
149
|
+
this.log("Fetching remote labels...");
|
|
150
|
+
const remoteLabels = await this.client.fetchLabels();
|
|
151
|
+
const result = {
|
|
152
|
+
created: [],
|
|
153
|
+
updated: [],
|
|
154
|
+
deleted: [],
|
|
155
|
+
unchanged: [],
|
|
156
|
+
errors: []
|
|
157
|
+
};
|
|
158
|
+
const remoteLabelMap = new Map(remoteLabels.map((label) => [label.name.toLowerCase(), label]));
|
|
159
|
+
const localLabelMap = new Map(localLabels.map((label) => [label.name.toLowerCase(), label]));
|
|
160
|
+
const toCreate = [];
|
|
161
|
+
const toUpdate = [];
|
|
162
|
+
const toDelete = [];
|
|
163
|
+
for (const label of localLabels) {
|
|
164
|
+
const remoteLabel = remoteLabelMap.get(label.name.toLowerCase());
|
|
165
|
+
if (!remoteLabel) {
|
|
166
|
+
toCreate.push(label);
|
|
167
|
+
} else if (this.hasChanges(label, remoteLabel)) {
|
|
168
|
+
toUpdate.push({ current: remoteLabel.name, updated: label });
|
|
169
|
+
} else {
|
|
170
|
+
result.unchanged.push(label);
|
|
171
|
+
this.log(`Unchanged label: ${label.name}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (this.options.deleteExtra) {
|
|
175
|
+
for (const remoteLabel of remoteLabels) {
|
|
176
|
+
if (!localLabelMap.has(remoteLabel.name.toLowerCase())) {
|
|
177
|
+
toDelete.push(remoteLabel.name);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (!this.options.dryRun) {
|
|
182
|
+
for (let i = 0; i < toCreate.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
183
|
+
const batch = toCreate.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
184
|
+
const promises = batch.map(async (label) => {
|
|
185
|
+
try {
|
|
186
|
+
await this.client.createLabel(label);
|
|
187
|
+
result.created.push(label);
|
|
188
|
+
this.log(`Created label: ${label.name}`);
|
|
189
|
+
} catch (error2) {
|
|
190
|
+
result.errors.push({
|
|
191
|
+
name: label.name,
|
|
192
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
193
|
+
});
|
|
194
|
+
this.log(`Error creating label "${label.name}": ${error2}`);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
await Promise.all(promises);
|
|
198
|
+
}
|
|
199
|
+
for (let i = 0; i < toUpdate.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
200
|
+
const batch = toUpdate.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
201
|
+
const promises = batch.map(async ({ current, updated }) => {
|
|
202
|
+
try {
|
|
203
|
+
await this.client.updateLabel(current, updated);
|
|
204
|
+
result.updated.push(updated);
|
|
205
|
+
this.log(`Updated label: ${updated.name}`);
|
|
206
|
+
} catch (error2) {
|
|
207
|
+
result.errors.push({
|
|
208
|
+
name: updated.name,
|
|
209
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
210
|
+
});
|
|
211
|
+
this.log(`Error updating label "${updated.name}": ${error2}`);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
await Promise.all(promises);
|
|
215
|
+
}
|
|
216
|
+
for (let i = 0; i < toDelete.length; i += _GitHubLabelSync.BATCH_SIZE) {
|
|
217
|
+
const batch = toDelete.slice(i, i + _GitHubLabelSync.BATCH_SIZE);
|
|
218
|
+
const promises = batch.map(async (name) => {
|
|
219
|
+
try {
|
|
220
|
+
await this.client.deleteLabel(name);
|
|
221
|
+
result.deleted.push(name);
|
|
222
|
+
this.log(`Deleted label: ${name}`);
|
|
223
|
+
} catch (error2) {
|
|
224
|
+
result.errors.push({
|
|
225
|
+
name,
|
|
226
|
+
error: error2 instanceof Error ? error2.message : String(error2)
|
|
227
|
+
});
|
|
228
|
+
this.log(`Error deleting label "${name}": ${error2}`);
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
await Promise.all(promises);
|
|
232
|
+
}
|
|
233
|
+
} else {
|
|
234
|
+
result.created.push(...toCreate);
|
|
235
|
+
result.updated.push(...toUpdate.map((op) => op.updated));
|
|
236
|
+
result.deleted.push(...toDelete);
|
|
237
|
+
toCreate.forEach((label) => this.log(`[DRY RUN] Would create label: ${label.name}`));
|
|
238
|
+
toUpdate.forEach(({ updated }) => this.log(`[DRY RUN] Would update label: ${updated.name}`));
|
|
239
|
+
toDelete.forEach((name) => this.log(`[DRY RUN] Would delete label: ${name}`));
|
|
240
|
+
}
|
|
241
|
+
return result;
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* Check if label has changes
|
|
245
|
+
*/
|
|
246
|
+
hasChanges(local, remote) {
|
|
247
|
+
return local.color.toLowerCase() !== remote.color.toLowerCase() || local.description !== (remote.description || "");
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Fetch labels from GitHub
|
|
251
|
+
*/
|
|
252
|
+
async fetchLabels() {
|
|
253
|
+
const labels = await this.client.fetchLabels();
|
|
254
|
+
return labels.map((label) => ({
|
|
255
|
+
name: label.name,
|
|
256
|
+
color: label.color,
|
|
257
|
+
description: label.description || ""
|
|
258
|
+
}));
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Delete a single label
|
|
262
|
+
*/
|
|
263
|
+
async deleteLabel(name) {
|
|
264
|
+
if (!this.options.dryRun) {
|
|
265
|
+
await this.client.deleteLabel(name);
|
|
266
|
+
}
|
|
267
|
+
this.log(`Deleted label: ${name}`);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Update a single label
|
|
271
|
+
*/
|
|
272
|
+
async updateLabel(name, updates) {
|
|
273
|
+
if (!this.options.dryRun) {
|
|
274
|
+
await this.client.updateLabel(name, updates);
|
|
275
|
+
}
|
|
276
|
+
this.log(`Updated label: ${name}`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
__publicField(_GitHubLabelSync, "BATCH_SIZE", 5);
|
|
280
|
+
var GitHubLabelSync = _GitHubLabelSync;
|
|
281
|
+
|
|
282
|
+
// src/utils/ui.ts
|
|
283
|
+
var colors = {
|
|
284
|
+
reset: "\x1B[0m",
|
|
285
|
+
bright: "\x1B[1m",
|
|
286
|
+
dim: "\x1B[2m",
|
|
287
|
+
// Foreground colors
|
|
288
|
+
red: "\x1B[31m",
|
|
289
|
+
green: "\x1B[32m",
|
|
290
|
+
yellow: "\x1B[33m",
|
|
291
|
+
blue: "\x1B[34m",
|
|
292
|
+
magenta: "\x1B[35m",
|
|
293
|
+
cyan: "\x1B[36m",
|
|
294
|
+
white: "\x1B[37m",
|
|
295
|
+
gray: "\x1B[90m",
|
|
296
|
+
// Background colors
|
|
297
|
+
bgRed: "\x1B[41m",
|
|
298
|
+
bgGreen: "\x1B[42m",
|
|
299
|
+
bgYellow: "\x1B[43m",
|
|
300
|
+
bgBlue: "\x1B[44m"
|
|
301
|
+
};
|
|
302
|
+
function supportsColor() {
|
|
303
|
+
if (process.env.NO_COLOR) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
if (process.env.FORCE_COLOR) {
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
if (!process.stdout.isTTY) {
|
|
310
|
+
return false;
|
|
311
|
+
}
|
|
312
|
+
if (process.platform === "win32") {
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
function colorize(text, color) {
|
|
318
|
+
if (!supportsColor()) {
|
|
319
|
+
return text;
|
|
320
|
+
}
|
|
321
|
+
return `${colors[color]}${text}${colors.reset}`;
|
|
322
|
+
}
|
|
323
|
+
function success(message) {
|
|
324
|
+
return colorize("\u2713", "green") + " " + message;
|
|
325
|
+
}
|
|
326
|
+
function error(message) {
|
|
327
|
+
return colorize("\u2717", "red") + " " + message;
|
|
328
|
+
}
|
|
329
|
+
function warning(message) {
|
|
330
|
+
return colorize("\u26A0", "yellow") + " " + message;
|
|
331
|
+
}
|
|
332
|
+
function info(message) {
|
|
333
|
+
return colorize("\u2139", "blue") + " " + message;
|
|
334
|
+
}
|
|
335
|
+
function header(text) {
|
|
336
|
+
return "\n" + colorize(text, "bright") + "\n" + "\u2500".repeat(Math.min(text.length, 50));
|
|
337
|
+
}
|
|
338
|
+
var Spinner = class {
|
|
339
|
+
constructor() {
|
|
340
|
+
__publicField(this, "frames", ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"]);
|
|
341
|
+
__publicField(this, "interval", null);
|
|
342
|
+
__publicField(this, "frameIndex", 0);
|
|
343
|
+
__publicField(this, "message", "");
|
|
344
|
+
}
|
|
345
|
+
start(message) {
|
|
346
|
+
this.message = message;
|
|
347
|
+
if (!process.stdout.isTTY || !supportsColor()) {
|
|
348
|
+
console.log(message + "...");
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
this.interval = setInterval(() => {
|
|
352
|
+
const frame = this.frames[this.frameIndex];
|
|
353
|
+
process.stdout.write(`\r${colorize(frame, "cyan")} ${this.message}`);
|
|
354
|
+
this.frameIndex = (this.frameIndex + 1) % this.frames.length;
|
|
355
|
+
}, 80);
|
|
356
|
+
}
|
|
357
|
+
succeed(message) {
|
|
358
|
+
this.stop();
|
|
359
|
+
console.log(success(message || this.message));
|
|
360
|
+
}
|
|
361
|
+
fail(message) {
|
|
362
|
+
this.stop();
|
|
363
|
+
console.log(error(message || this.message));
|
|
364
|
+
}
|
|
365
|
+
warn(message) {
|
|
366
|
+
this.stop();
|
|
367
|
+
console.log(warning(message || this.message));
|
|
368
|
+
}
|
|
369
|
+
stop() {
|
|
370
|
+
if (this.interval) {
|
|
371
|
+
clearInterval(this.interval);
|
|
372
|
+
this.interval = null;
|
|
373
|
+
if (process.stdout.isTTY) {
|
|
374
|
+
process.stdout.write("\r\x1B[K");
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// src/github/batch-sync.ts
|
|
381
|
+
var _BatchLabelSync = class _BatchLabelSync {
|
|
382
|
+
/**
|
|
383
|
+
* 複数リポジトリへのラベル一括同期
|
|
384
|
+
*/
|
|
385
|
+
async syncMultiple(labels, options) {
|
|
386
|
+
const repos = await this.getTargetRepositories(options);
|
|
387
|
+
const results = [];
|
|
388
|
+
console.log(colorize(`
|
|
389
|
+
\u{1F4CB} Target repositories: ${repos.length}`, "cyan"));
|
|
390
|
+
let completed = 0;
|
|
391
|
+
const parallel = options.parallel || _BatchLabelSync.DEFAULT_PARALLEL;
|
|
392
|
+
for (let i = 0; i < repos.length; i += parallel) {
|
|
393
|
+
const batch = repos.slice(i, i + parallel);
|
|
394
|
+
const batchResults = await Promise.allSettled(
|
|
395
|
+
batch.map((repo) => this.syncSingleRepo(repo, labels, options))
|
|
396
|
+
);
|
|
397
|
+
batchResults.forEach((result, index) => {
|
|
398
|
+
const repo = batch[index];
|
|
399
|
+
if (result.status === "fulfilled") {
|
|
400
|
+
results.push(result.value);
|
|
401
|
+
completed++;
|
|
402
|
+
console.log(colorize(`\u2705 [${completed}/${repos.length}] ${repo}`, "green"));
|
|
403
|
+
} else {
|
|
404
|
+
results.push({
|
|
405
|
+
repository: repo,
|
|
406
|
+
status: "failed",
|
|
407
|
+
error: result.reason?.message || "Unknown error"
|
|
408
|
+
});
|
|
409
|
+
completed++;
|
|
410
|
+
console.log(colorize(`\u274C [${completed}/${repos.length}] ${repo}: ${result.reason}`, "red"));
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
return results;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* 単一リポジトリへの同期
|
|
418
|
+
*/
|
|
419
|
+
async syncSingleRepo(repository, labels, options) {
|
|
420
|
+
try {
|
|
421
|
+
const [owner, repo] = repository.split("/");
|
|
422
|
+
if (!owner || !repo) {
|
|
423
|
+
throw new Error(`Invalid repository format: ${repository}. Expected format: owner/repo`);
|
|
424
|
+
}
|
|
425
|
+
const sync = new GitHubLabelSync({
|
|
426
|
+
owner,
|
|
427
|
+
repo,
|
|
428
|
+
deleteExtra: options.mode === "replace",
|
|
429
|
+
dryRun: options.dryRun || false
|
|
430
|
+
});
|
|
431
|
+
const result = await sync.syncLabels(labels);
|
|
432
|
+
return {
|
|
433
|
+
repository,
|
|
434
|
+
status: "success",
|
|
435
|
+
result
|
|
436
|
+
};
|
|
437
|
+
} catch (error2) {
|
|
438
|
+
return {
|
|
439
|
+
repository,
|
|
440
|
+
status: "failed",
|
|
441
|
+
error: error2 instanceof Error ? error2.message : "Unknown error"
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* 対象リポジトリリストの取得
|
|
447
|
+
*/
|
|
448
|
+
async getTargetRepositories(options) {
|
|
449
|
+
if (options.repositories && options.repositories.length > 0) {
|
|
450
|
+
return options.repositories;
|
|
451
|
+
}
|
|
452
|
+
if (options.organization) {
|
|
453
|
+
return this.getOrganizationRepos(options.organization, options.filter);
|
|
454
|
+
}
|
|
455
|
+
if (options.user) {
|
|
456
|
+
return this.getUserRepos(options.user, options.filter);
|
|
457
|
+
}
|
|
458
|
+
throw new Error("No target repositories specified");
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* 組織のリポジトリ一覧を取得
|
|
462
|
+
*/
|
|
463
|
+
async getOrganizationRepos(org, filter) {
|
|
464
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
465
|
+
try {
|
|
466
|
+
const command = `gh repo list ${org} --json nameWithOwner,visibility,language,isArchived --limit 1000`;
|
|
467
|
+
const output = execSync2(command, { encoding: "utf-8" });
|
|
468
|
+
const repos = JSON.parse(output);
|
|
469
|
+
return repos.filter((repo) => {
|
|
470
|
+
if (filter?.visibility && filter.visibility !== "all" && repo.visibility !== filter.visibility) {
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
if (filter?.language && repo.language !== filter.language) {
|
|
474
|
+
return false;
|
|
475
|
+
}
|
|
476
|
+
if (filter?.archived !== void 0 && repo.isArchived !== filter.archived) {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
return true;
|
|
480
|
+
}).map((repo) => repo.nameWithOwner);
|
|
481
|
+
} catch (error2) {
|
|
482
|
+
throw new Error(`Failed to fetch organization repos: ${error2}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* ユーザーのリポジトリ一覧を取得
|
|
487
|
+
*/
|
|
488
|
+
async getUserRepos(user, filter) {
|
|
489
|
+
const { execSync: execSync2 } = await import("child_process");
|
|
490
|
+
try {
|
|
491
|
+
const command = `gh repo list ${user} --json nameWithOwner,visibility,language,isArchived --limit 1000`;
|
|
492
|
+
const output = execSync2(command, { encoding: "utf-8" });
|
|
493
|
+
const repos = JSON.parse(output);
|
|
494
|
+
return repos.filter((repo) => {
|
|
495
|
+
if (filter?.visibility && filter.visibility !== "all" && repo.visibility !== filter.visibility) {
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
if (filter?.language && repo.language !== filter.language) {
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
if (filter?.archived !== void 0 && repo.isArchived !== filter.archived) {
|
|
502
|
+
return false;
|
|
503
|
+
}
|
|
504
|
+
return true;
|
|
505
|
+
}).map((repo) => repo.nameWithOwner);
|
|
506
|
+
} catch (error2) {
|
|
507
|
+
throw new Error(`Failed to fetch user repos: ${error2}`);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* 結果サマリーの生成
|
|
512
|
+
*/
|
|
513
|
+
generateSummary(results) {
|
|
514
|
+
const successful = results.filter((r) => r.status === "success").length;
|
|
515
|
+
const failed = results.filter((r) => r.status === "failed").length;
|
|
516
|
+
const skipped = results.filter((r) => r.status === "skipped").length;
|
|
517
|
+
let summary = "\n\u{1F4CA} Batch Sync Summary:\n";
|
|
518
|
+
summary += `\u2705 Successful: ${successful}
|
|
519
|
+
`;
|
|
520
|
+
if (failed > 0) summary += `\u274C Failed: ${failed}
|
|
521
|
+
`;
|
|
522
|
+
if (skipped > 0) summary += `\u23ED\uFE0F Skipped: ${skipped}
|
|
523
|
+
`;
|
|
524
|
+
const failedRepos = results.filter((r) => r.status === "failed");
|
|
525
|
+
if (failedRepos.length > 0) {
|
|
526
|
+
summary += "\n\u274C Failed repositories:\n";
|
|
527
|
+
failedRepos.forEach((repo) => {
|
|
528
|
+
summary += ` - ${repo.repository}: ${repo.error}
|
|
529
|
+
`;
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
return summary;
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
__publicField(_BatchLabelSync, "DEFAULT_PARALLEL", 3);
|
|
536
|
+
var BatchLabelSync = _BatchLabelSync;
|
|
537
|
+
|
|
538
|
+
export {
|
|
539
|
+
GitHubClient,
|
|
540
|
+
GitHubLabelSync,
|
|
541
|
+
colorize,
|
|
542
|
+
success,
|
|
543
|
+
error,
|
|
544
|
+
warning,
|
|
545
|
+
info,
|
|
546
|
+
header,
|
|
547
|
+
Spinner,
|
|
548
|
+
BatchLabelSync
|
|
549
|
+
};
|