@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 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
+ };