@justanarthur/just-github-actions-n-workflows-cli 0.0.0-beta.10 → 0.0.0-beta.12
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/dist/commands/init.js +198 -0
- package/dist/github.js +90 -0
- package/dist/index.js +6 -0
- package/{src/init.ts → dist/init.js} +2 -1
- package/package.json +9 -5
- package/src/commands/init.ts +0 -265
- package/src/github.ts +0 -113
- /package/{src/index.ts → bin/run.js} +0 -0
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { Args, Command, Flags, ux } from "@oclif/core";
|
|
2
|
+
import { checkbox, select } from "@inquirer/prompts";
|
|
3
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { REPO, enrichWorkflows, fetchTags, fetchWorkflowContent, fetchWorkflowList, } from "../github.js";
|
|
6
|
+
export default class Init extends Command {
|
|
7
|
+
static description = "Scaffold workflow files into .github/workflows/ of the current repo";
|
|
8
|
+
static examples = [
|
|
9
|
+
"<%= config.bin %> init",
|
|
10
|
+
"<%= config.bin %> init bump-version",
|
|
11
|
+
"<%= config.bin %> init --ref v1.0.0",
|
|
12
|
+
"<%= config.bin %> init --list",
|
|
13
|
+
"<%= config.bin %> init --yes --force",
|
|
14
|
+
];
|
|
15
|
+
static args = {
|
|
16
|
+
workflows: Args.string({
|
|
17
|
+
description: "Specific workflow names to install (space-separated)",
|
|
18
|
+
required: false,
|
|
19
|
+
}),
|
|
20
|
+
};
|
|
21
|
+
static strict = false;
|
|
22
|
+
static flags = {
|
|
23
|
+
list: Flags.boolean({
|
|
24
|
+
char: "l",
|
|
25
|
+
description: "Show available workflows",
|
|
26
|
+
default: false,
|
|
27
|
+
}),
|
|
28
|
+
force: Flags.boolean({
|
|
29
|
+
char: "f",
|
|
30
|
+
description: "Overwrite existing workflow files",
|
|
31
|
+
default: false,
|
|
32
|
+
}),
|
|
33
|
+
yes: Flags.boolean({
|
|
34
|
+
char: "y",
|
|
35
|
+
description: "Skip interactive prompts, install all workflows",
|
|
36
|
+
default: false,
|
|
37
|
+
}),
|
|
38
|
+
ref: Flags.string({
|
|
39
|
+
description: "Git ref to fetch from (branch, tag, or sha)",
|
|
40
|
+
default: "main",
|
|
41
|
+
}),
|
|
42
|
+
};
|
|
43
|
+
async run() {
|
|
44
|
+
const { argv, flags } = await this.parse(Init);
|
|
45
|
+
const positional = argv;
|
|
46
|
+
// --- list mode ---
|
|
47
|
+
if (flags.list) {
|
|
48
|
+
await this.listWorkflows(flags.ref);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
// --- interactive / install mode ---
|
|
52
|
+
this.log();
|
|
53
|
+
this.log(ux.colorize("bold", " just-github-actions-n-workflows"));
|
|
54
|
+
this.log(ux.colorize("dim", " release automation toolkit\n"));
|
|
55
|
+
const ref = await this.resolveRef(flags, positional);
|
|
56
|
+
const selected = await this.selectWorkflows(ref, flags, positional);
|
|
57
|
+
if (selected.length === 0) {
|
|
58
|
+
this.log(ux.colorize("yellow", "\n no workflows selected — nothing to do.\n"));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const { created, skipped } = await this.installWorkflows(selected, ref, flags);
|
|
62
|
+
this.log(`\n done — ${ux.colorize("green", `${created} created`)}, ${skipped} skipped\n`);
|
|
63
|
+
this.printSecretsReminder(selected);
|
|
64
|
+
this.printNextSteps(created, selected);
|
|
65
|
+
}
|
|
66
|
+
// ── step 1: resolve ref ────────────────────────────────
|
|
67
|
+
async resolveRef(flags, positional) {
|
|
68
|
+
if (flags.ref !== "main" || positional.length > 0 || flags.yes) {
|
|
69
|
+
return flags.ref;
|
|
70
|
+
}
|
|
71
|
+
this.log(ux.colorize("bold", " step 1 — select version\n"));
|
|
72
|
+
const tags = await fetchTags();
|
|
73
|
+
if (tags.length === 0)
|
|
74
|
+
return "main";
|
|
75
|
+
const choices = [
|
|
76
|
+
{ name: `main ${ux.colorize("dim", "(latest)")}`, value: "main" },
|
|
77
|
+
...tags.slice(0, 9).map((tag) => ({ name: tag, value: tag })),
|
|
78
|
+
];
|
|
79
|
+
const ref = await select({
|
|
80
|
+
message: "Pick a version",
|
|
81
|
+
choices,
|
|
82
|
+
default: "main",
|
|
83
|
+
});
|
|
84
|
+
this.log(ux.colorize("green", `\n → using ${ref}\n`));
|
|
85
|
+
return ref;
|
|
86
|
+
}
|
|
87
|
+
// ── step 2: select workflows ───────────────────────────
|
|
88
|
+
async selectWorkflows(ref, flags, positional) {
|
|
89
|
+
let available = await fetchWorkflowList(ref);
|
|
90
|
+
available = await enrichWorkflows(available, ref);
|
|
91
|
+
if (positional.length > 0) {
|
|
92
|
+
return positional.map((name) => {
|
|
93
|
+
const key = name.replace(/\.yml$/, "");
|
|
94
|
+
const wf = available.find((w) => w.name === key);
|
|
95
|
+
if (!wf) {
|
|
96
|
+
this.error(`Unknown workflow "${name}". Available: ${available.map((w) => w.name).join(", ")}`);
|
|
97
|
+
}
|
|
98
|
+
return wf;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
if (flags.yes)
|
|
102
|
+
return available;
|
|
103
|
+
this.log(ux.colorize("bold", " step 2 — select workflows\n"));
|
|
104
|
+
const selected = await checkbox({
|
|
105
|
+
message: "Select workflows to install",
|
|
106
|
+
choices: available.map((wf) => {
|
|
107
|
+
const secretNames = wf.secrets?.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ");
|
|
108
|
+
const hint = [
|
|
109
|
+
wf.description,
|
|
110
|
+
secretNames ? `requires: ${secretNames}` : null,
|
|
111
|
+
].filter(Boolean).join(" · ");
|
|
112
|
+
return {
|
|
113
|
+
name: `${wf.name.padEnd(26)} ${ux.colorize("dim", hint || wf.file)}`,
|
|
114
|
+
value: wf.name,
|
|
115
|
+
checked: true,
|
|
116
|
+
};
|
|
117
|
+
}),
|
|
118
|
+
});
|
|
119
|
+
return available.filter((w) => selected.includes(w.name));
|
|
120
|
+
}
|
|
121
|
+
// ── step 3: install ────────────────────────────────────
|
|
122
|
+
async installWorkflows(selected, ref, flags) {
|
|
123
|
+
const targetDir = join(process.cwd(), ".github", "workflows");
|
|
124
|
+
mkdirSync(targetDir, { recursive: true });
|
|
125
|
+
if (!flags.yes) {
|
|
126
|
+
this.log(ux.colorize("bold", " step 3 — install\n"));
|
|
127
|
+
}
|
|
128
|
+
this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${ref}\n`));
|
|
129
|
+
let created = 0;
|
|
130
|
+
let skipped = 0;
|
|
131
|
+
for (const workflow of selected) {
|
|
132
|
+
const targetPath = join(targetDir, workflow.file);
|
|
133
|
+
if (!flags.force && existsSync(targetPath)) {
|
|
134
|
+
this.log(` ${ux.colorize("yellow", "skip")} ${workflow.file} ${ux.colorize("dim", "(already exists, use --force)")}`);
|
|
135
|
+
skipped++;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
try {
|
|
139
|
+
const content = await fetchWorkflowContent(workflow.file, ref);
|
|
140
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
141
|
+
this.log(` ${ux.colorize("green", "create")} .github/workflows/${workflow.file}`);
|
|
142
|
+
created++;
|
|
143
|
+
}
|
|
144
|
+
catch (error) {
|
|
145
|
+
this.log(` ${ux.colorize("red", "error")} ${workflow.file}: ${error.message}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return { created, skipped };
|
|
149
|
+
}
|
|
150
|
+
// ── step 4: secrets reminder ───────────────────────────
|
|
151
|
+
printSecretsReminder(selected) {
|
|
152
|
+
const allSecrets = new Map();
|
|
153
|
+
for (const wf of selected) {
|
|
154
|
+
for (const s of wf.secrets || []) {
|
|
155
|
+
const [name, ...descParts] = s.split(/\s+[—–]\s*/);
|
|
156
|
+
if (name && !allSecrets.has(name.trim())) {
|
|
157
|
+
allSecrets.set(name.trim(), descParts.join(" — ").trim());
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (allSecrets.size === 0)
|
|
162
|
+
return;
|
|
163
|
+
this.log(ux.colorize("bold", " required secrets:\n"));
|
|
164
|
+
for (const [name, desc] of allSecrets) {
|
|
165
|
+
this.log(` • ${ux.colorize("cyan", name.padEnd(18))} ${ux.colorize("dim", desc)}`);
|
|
166
|
+
}
|
|
167
|
+
this.log(ux.colorize("dim", `\n set these in your repo → Settings → Secrets → Actions\n`));
|
|
168
|
+
}
|
|
169
|
+
// ── next steps ─────────────────────────────────────────
|
|
170
|
+
printNextSteps(created, selected) {
|
|
171
|
+
if (created === 0)
|
|
172
|
+
return;
|
|
173
|
+
const hasSecrets = selected.some((w) => w.secrets && w.secrets.length > 0);
|
|
174
|
+
this.log(ux.colorize("bold", " next steps:\n"));
|
|
175
|
+
this.log(` 1. ${hasSecrets ? "set the secrets listed above" : "set the GH_TOKEN secret in your repo settings"}`);
|
|
176
|
+
this.log(` 2. adjust push.branches / push.tags triggers for your repo`);
|
|
177
|
+
this.log(` 3. commit and push:`);
|
|
178
|
+
this.log(ux.colorize("dim", ` git add .github/ && git commit -m "ci: add workflows" && git push`));
|
|
179
|
+
this.log();
|
|
180
|
+
}
|
|
181
|
+
// ── list mode ──────────────────────────────────────────
|
|
182
|
+
async listWorkflows(ref) {
|
|
183
|
+
const workflows = await enrichWorkflows(await fetchWorkflowList(ref), ref);
|
|
184
|
+
this.log(`\n${ux.colorize("bold", "available workflows")} ${ux.colorize("dim", `(${REPO} @ ${ref})`)}\n`);
|
|
185
|
+
for (const w of workflows) {
|
|
186
|
+
this.log(` ${ux.colorize("cyan", w.name.padEnd(28))} → .github/workflows/${w.file}`);
|
|
187
|
+
if (w.description) {
|
|
188
|
+
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", w.description)}`);
|
|
189
|
+
}
|
|
190
|
+
if (w.secrets?.length) {
|
|
191
|
+
const names = w.secrets.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ");
|
|
192
|
+
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", `secrets: ${names}`)}`);
|
|
193
|
+
}
|
|
194
|
+
this.log();
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
//# sourceMappingURL=init.js.map
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const pkg = require("../package.json");
|
|
4
|
+
const REPO = pkg.repository.url
|
|
5
|
+
.replace(/^https?:\/\/github\.com\//, "")
|
|
6
|
+
.replace(/\.git$/, "");
|
|
7
|
+
const API_BASE = `https://api.github.com/repos/${REPO}`;
|
|
8
|
+
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}`;
|
|
9
|
+
export { REPO };
|
|
10
|
+
// --- parse workflow header ---
|
|
11
|
+
// extracts description and required secrets from the yaml header comment block.
|
|
12
|
+
export function parseWorkflowHeader(content) {
|
|
13
|
+
const lines = content.split("\n");
|
|
14
|
+
let description = "";
|
|
15
|
+
const secrets = [];
|
|
16
|
+
let inHeader = false;
|
|
17
|
+
for (const line of lines) {
|
|
18
|
+
if (line.startsWith("# ---")) {
|
|
19
|
+
if (inHeader)
|
|
20
|
+
break;
|
|
21
|
+
inHeader = true;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (!inHeader)
|
|
25
|
+
continue;
|
|
26
|
+
if (!line.startsWith("#"))
|
|
27
|
+
break;
|
|
28
|
+
const text = line.replace(/^#\s?/, "").trim();
|
|
29
|
+
if (/^required secrets:/i.test(text))
|
|
30
|
+
continue;
|
|
31
|
+
if (/^\w+\s+[—–]/.test(text)) {
|
|
32
|
+
secrets.push(text);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (!description && text && !/^copy this|^can also|^actions used|^paste this|^this one/i.test(text)) {
|
|
36
|
+
description = text;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return { description, secrets };
|
|
40
|
+
}
|
|
41
|
+
// --- api ---
|
|
42
|
+
export async function fetchTags() {
|
|
43
|
+
const url = `${API_BASE}/tags?per_page=20`;
|
|
44
|
+
const res = await fetch(url, {
|
|
45
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
46
|
+
});
|
|
47
|
+
if (!res.ok)
|
|
48
|
+
return [];
|
|
49
|
+
const tags = await res.json();
|
|
50
|
+
return tags.map((t) => t.name);
|
|
51
|
+
}
|
|
52
|
+
export async function fetchWorkflowList(gitRef) {
|
|
53
|
+
const url = `${API_BASE}/contents/workflows?ref=${gitRef}`;
|
|
54
|
+
const res = await fetch(url, {
|
|
55
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
56
|
+
});
|
|
57
|
+
if (!res.ok) {
|
|
58
|
+
throw new Error(`failed to list workflows from ${REPO} @ ${gitRef} (${res.status})`);
|
|
59
|
+
}
|
|
60
|
+
const entries = await res.json();
|
|
61
|
+
return entries
|
|
62
|
+
.filter((e) => e.type === "file" && e.name.endsWith(".yml"))
|
|
63
|
+
.map((e) => ({
|
|
64
|
+
name: e.name.replace(/\.yml$/, ""),
|
|
65
|
+
file: e.name
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
export async function fetchWorkflowContent(file, gitRef) {
|
|
69
|
+
const url = `${RAW_BASE}/${gitRef}/workflows/${file}`;
|
|
70
|
+
const res = await fetch(url);
|
|
71
|
+
if (!res.ok) {
|
|
72
|
+
throw new Error(`failed to fetch workflows/${file} from ${REPO} @ ${gitRef} (${res.status})`);
|
|
73
|
+
}
|
|
74
|
+
return await res.text();
|
|
75
|
+
}
|
|
76
|
+
export async function enrichWorkflows(workflows, gitRef) {
|
|
77
|
+
const enriched = [];
|
|
78
|
+
for (const wf of workflows) {
|
|
79
|
+
try {
|
|
80
|
+
const content = await fetchWorkflowContent(wf.file, gitRef);
|
|
81
|
+
const { description, secrets } = parseWorkflowHeader(content);
|
|
82
|
+
enriched.push({ ...wf, description, secrets });
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
enriched.push(wf);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return enriched;
|
|
89
|
+
}
|
|
90
|
+
//# sourceMappingURL=github.js.map
|
package/dist/index.js
ADDED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
2
|
+
"use strict";
|
|
3
3
|
// This file has been replaced by:
|
|
4
4
|
// src/index.ts — oclif entry point
|
|
5
5
|
// src/commands/init.ts — init command
|
|
6
6
|
// src/github.ts — github api helpers
|
|
7
7
|
//
|
|
8
8
|
// You can safely delete this file.
|
|
9
|
+
//# sourceMappingURL=init.js.map
|
package/package.json
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justanarthur/just-github-actions-n-workflows-cli",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.12",
|
|
4
4
|
"description": "Release automation toolkit — version bumping, npm publishing, docker publishing, VPS deployment via GitHub Actions composite actions",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"just-github-actions-n-workflows": "./
|
|
7
|
+
"just-github-actions-n-workflows": "./bin/run.js"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
10
|
+
"build": "tsc"
|
|
11
11
|
},
|
|
12
12
|
"files": [
|
|
13
|
-
"
|
|
13
|
+
"bin/**/*.js",
|
|
14
|
+
"dist/**/*.js",
|
|
14
15
|
"package.json",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
|
@@ -25,8 +26,11 @@
|
|
|
25
26
|
"@oclif/core": "^4",
|
|
26
27
|
"@inquirer/prompts": "^7"
|
|
27
28
|
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
},
|
|
28
32
|
"oclif": {
|
|
29
|
-
"commands": "./
|
|
33
|
+
"commands": "./dist/commands",
|
|
30
34
|
"default": "init"
|
|
31
35
|
},
|
|
32
36
|
"properties": {
|
package/src/commands/init.ts
DELETED
|
@@ -1,265 +0,0 @@
|
|
|
1
|
-
import { Args, Command, Flags, ux } from "@oclif/core"
|
|
2
|
-
import { checkbox, select } from "@inquirer/prompts"
|
|
3
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs"
|
|
4
|
-
import { join } from "node:path"
|
|
5
|
-
|
|
6
|
-
import {
|
|
7
|
-
REPO,
|
|
8
|
-
enrichWorkflows,
|
|
9
|
-
fetchTags,
|
|
10
|
-
fetchWorkflowContent,
|
|
11
|
-
fetchWorkflowList,
|
|
12
|
-
type WorkflowEntry,
|
|
13
|
-
} from "../github.js"
|
|
14
|
-
|
|
15
|
-
export default class Init extends Command {
|
|
16
|
-
static override description = "Scaffold workflow files into .github/workflows/ of the current repo"
|
|
17
|
-
|
|
18
|
-
static override examples = [
|
|
19
|
-
"<%= config.bin %> init",
|
|
20
|
-
"<%= config.bin %> init bump-version",
|
|
21
|
-
"<%= config.bin %> init --ref v1.0.0",
|
|
22
|
-
"<%= config.bin %> init --list",
|
|
23
|
-
"<%= config.bin %> init --yes --force",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
static override args = {
|
|
27
|
-
workflows: Args.string({
|
|
28
|
-
description: "Specific workflow names to install (space-separated)",
|
|
29
|
-
required: false,
|
|
30
|
-
}),
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static override strict = false
|
|
34
|
-
|
|
35
|
-
static override flags = {
|
|
36
|
-
list: Flags.boolean({
|
|
37
|
-
char: "l",
|
|
38
|
-
description: "Show available workflows",
|
|
39
|
-
default: false,
|
|
40
|
-
}),
|
|
41
|
-
force: Flags.boolean({
|
|
42
|
-
char: "f",
|
|
43
|
-
description: "Overwrite existing workflow files",
|
|
44
|
-
default: false,
|
|
45
|
-
}),
|
|
46
|
-
yes: Flags.boolean({
|
|
47
|
-
char: "y",
|
|
48
|
-
description: "Skip interactive prompts, install all workflows",
|
|
49
|
-
default: false,
|
|
50
|
-
}),
|
|
51
|
-
ref: Flags.string({
|
|
52
|
-
description: "Git ref to fetch from (branch, tag, or sha)",
|
|
53
|
-
default: "main",
|
|
54
|
-
}),
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
async run(): Promise<void> {
|
|
58
|
-
const { argv, flags } = await this.parse(Init)
|
|
59
|
-
const positional = argv as string[]
|
|
60
|
-
|
|
61
|
-
// --- list mode ---
|
|
62
|
-
|
|
63
|
-
if (flags.list) {
|
|
64
|
-
await this.listWorkflows(flags.ref)
|
|
65
|
-
return
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// --- interactive / install mode ---
|
|
69
|
-
|
|
70
|
-
this.log()
|
|
71
|
-
this.log(ux.colorize("bold", " just-github-actions-n-workflows"))
|
|
72
|
-
this.log(ux.colorize("dim", " release automation toolkit\n"))
|
|
73
|
-
|
|
74
|
-
const ref = await this.resolveRef(flags, positional)
|
|
75
|
-
const selected = await this.selectWorkflows(ref, flags, positional)
|
|
76
|
-
|
|
77
|
-
if (selected.length === 0) {
|
|
78
|
-
this.log(ux.colorize("yellow", "\n no workflows selected — nothing to do.\n"))
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const { created, skipped } = await this.installWorkflows(selected, ref, flags)
|
|
83
|
-
|
|
84
|
-
this.log(`\n done — ${ux.colorize("green", `${created} created`)}, ${skipped} skipped\n`)
|
|
85
|
-
|
|
86
|
-
this.printSecretsReminder(selected)
|
|
87
|
-
this.printNextSteps(created, selected)
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// ── step 1: resolve ref ────────────────────────────────
|
|
91
|
-
|
|
92
|
-
private async resolveRef(
|
|
93
|
-
flags: { ref: string; yes: boolean },
|
|
94
|
-
positional: string[]
|
|
95
|
-
): Promise<string> {
|
|
96
|
-
if (flags.ref !== "main" || positional.length > 0 || flags.yes) {
|
|
97
|
-
return flags.ref
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
this.log(ux.colorize("bold", " step 1 — select version\n"))
|
|
101
|
-
|
|
102
|
-
const tags = await fetchTags()
|
|
103
|
-
|
|
104
|
-
if (tags.length === 0) return "main"
|
|
105
|
-
|
|
106
|
-
const choices = [
|
|
107
|
-
{ name: `main ${ux.colorize("dim", "(latest)")}`, value: "main" },
|
|
108
|
-
...tags.slice(0, 9).map((tag) => ({ name: tag, value: tag })),
|
|
109
|
-
]
|
|
110
|
-
|
|
111
|
-
const ref = await select({
|
|
112
|
-
message: "Pick a version",
|
|
113
|
-
choices,
|
|
114
|
-
default: "main",
|
|
115
|
-
})
|
|
116
|
-
|
|
117
|
-
this.log(ux.colorize("green", `\n → using ${ref}\n`))
|
|
118
|
-
return ref
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// ── step 2: select workflows ───────────────────────────
|
|
122
|
-
|
|
123
|
-
private async selectWorkflows(
|
|
124
|
-
ref: string,
|
|
125
|
-
flags: { yes: boolean },
|
|
126
|
-
positional: string[]
|
|
127
|
-
): Promise<WorkflowEntry[]> {
|
|
128
|
-
let available = await fetchWorkflowList(ref)
|
|
129
|
-
available = await enrichWorkflows(available, ref)
|
|
130
|
-
|
|
131
|
-
if (positional.length > 0) {
|
|
132
|
-
return positional.map((name) => {
|
|
133
|
-
const key = name.replace(/\.yml$/, "")
|
|
134
|
-
const wf = available.find((w) => w.name === key)
|
|
135
|
-
if (!wf) {
|
|
136
|
-
this.error(`Unknown workflow "${name}". Available: ${available.map((w) => w.name).join(", ")}`)
|
|
137
|
-
}
|
|
138
|
-
return wf!
|
|
139
|
-
})
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (flags.yes) return available
|
|
143
|
-
|
|
144
|
-
this.log(ux.colorize("bold", " step 2 — select workflows\n"))
|
|
145
|
-
|
|
146
|
-
const selected = await checkbox({
|
|
147
|
-
message: "Select workflows to install",
|
|
148
|
-
choices: available.map((wf) => {
|
|
149
|
-
const secretNames = wf.secrets?.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ")
|
|
150
|
-
const hint = [
|
|
151
|
-
wf.description,
|
|
152
|
-
secretNames ? `requires: ${secretNames}` : null,
|
|
153
|
-
].filter(Boolean).join(" · ")
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
name: `${wf.name.padEnd(26)} ${ux.colorize("dim", hint || wf.file)}`,
|
|
157
|
-
value: wf.name,
|
|
158
|
-
checked: true,
|
|
159
|
-
}
|
|
160
|
-
}),
|
|
161
|
-
})
|
|
162
|
-
|
|
163
|
-
return available.filter((w) => selected.includes(w.name))
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// ── step 3: install ────────────────────────────────────
|
|
167
|
-
|
|
168
|
-
private async installWorkflows(
|
|
169
|
-
selected: WorkflowEntry[],
|
|
170
|
-
ref: string,
|
|
171
|
-
flags: { force: boolean; yes: boolean }
|
|
172
|
-
): Promise<{ created: number; skipped: number }> {
|
|
173
|
-
const targetDir = join(process.cwd(), ".github", "workflows")
|
|
174
|
-
mkdirSync(targetDir, { recursive: true })
|
|
175
|
-
|
|
176
|
-
if (!flags.yes) {
|
|
177
|
-
this.log(ux.colorize("bold", " step 3 — install\n"))
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${ref}\n`))
|
|
181
|
-
|
|
182
|
-
let created = 0
|
|
183
|
-
let skipped = 0
|
|
184
|
-
|
|
185
|
-
for (const workflow of selected) {
|
|
186
|
-
const targetPath = join(targetDir, workflow.file)
|
|
187
|
-
|
|
188
|
-
if (!flags.force && existsSync(targetPath)) {
|
|
189
|
-
this.log(` ${ux.colorize("yellow", "skip")} ${workflow.file} ${ux.colorize("dim", "(already exists, use --force)")}`)
|
|
190
|
-
skipped++
|
|
191
|
-
continue
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
const content = await fetchWorkflowContent(workflow.file, ref)
|
|
196
|
-
writeFileSync(targetPath, content, "utf-8")
|
|
197
|
-
this.log(` ${ux.colorize("green", "create")} .github/workflows/${workflow.file}`)
|
|
198
|
-
created++
|
|
199
|
-
} catch (error: any) {
|
|
200
|
-
this.log(` ${ux.colorize("red", "error")} ${workflow.file}: ${error.message}`)
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
return { created, skipped }
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
// ── step 4: secrets reminder ───────────────────────────
|
|
208
|
-
|
|
209
|
-
private printSecretsReminder(selected: WorkflowEntry[]): void {
|
|
210
|
-
const allSecrets = new Map<string, string>()
|
|
211
|
-
for (const wf of selected) {
|
|
212
|
-
for (const s of wf.secrets || []) {
|
|
213
|
-
const [name, ...descParts] = s.split(/\s+[—–]\s*/)
|
|
214
|
-
if (name && !allSecrets.has(name.trim())) {
|
|
215
|
-
allSecrets.set(name.trim(), descParts.join(" — ").trim())
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (allSecrets.size === 0) return
|
|
221
|
-
|
|
222
|
-
this.log(ux.colorize("bold", " required secrets:\n"))
|
|
223
|
-
for (const [name, desc] of allSecrets) {
|
|
224
|
-
this.log(` • ${ux.colorize("cyan", name.padEnd(18))} ${ux.colorize("dim", desc)}`)
|
|
225
|
-
}
|
|
226
|
-
this.log(ux.colorize("dim", `\n set these in your repo → Settings → Secrets → Actions\n`))
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ── next steps ─────────────────────────────────────────
|
|
230
|
-
|
|
231
|
-
private printNextSteps(created: number, selected: WorkflowEntry[]): void {
|
|
232
|
-
if (created === 0) return
|
|
233
|
-
|
|
234
|
-
const hasSecrets = selected.some((w) => w.secrets && w.secrets.length > 0)
|
|
235
|
-
|
|
236
|
-
this.log(ux.colorize("bold", " next steps:\n"))
|
|
237
|
-
this.log(` 1. ${hasSecrets ? "set the secrets listed above" : "set the GH_TOKEN secret in your repo settings"}`)
|
|
238
|
-
this.log(` 2. adjust push.branches / push.tags triggers for your repo`)
|
|
239
|
-
this.log(` 3. commit and push:`)
|
|
240
|
-
this.log(ux.colorize("dim", ` git add .github/ && git commit -m "ci: add workflows" && git push`))
|
|
241
|
-
this.log()
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
// ── list mode ──────────────────────────────────────────
|
|
245
|
-
|
|
246
|
-
private async listWorkflows(ref: string): Promise<void> {
|
|
247
|
-
const workflows = await enrichWorkflows(await fetchWorkflowList(ref), ref)
|
|
248
|
-
|
|
249
|
-
this.log(`\n${ux.colorize("bold", "available workflows")} ${ux.colorize("dim", `(${REPO} @ ${ref})`)}\n`)
|
|
250
|
-
|
|
251
|
-
for (const w of workflows) {
|
|
252
|
-
this.log(` ${ux.colorize("cyan", w.name.padEnd(28))} → .github/workflows/${w.file}`)
|
|
253
|
-
if (w.description) {
|
|
254
|
-
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", w.description)}`)
|
|
255
|
-
}
|
|
256
|
-
if (w.secrets?.length) {
|
|
257
|
-
const names = w.secrets.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ")
|
|
258
|
-
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", `secrets: ${names}`)}`)
|
|
259
|
-
}
|
|
260
|
-
this.log()
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
|
package/src/github.ts
DELETED
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
import pkg from "../package.json"
|
|
2
|
-
|
|
3
|
-
const REPO = pkg.repository.url
|
|
4
|
-
.replace(/^https?:\/\/github\.com\//, "")
|
|
5
|
-
.replace(/\.git$/, "")
|
|
6
|
-
|
|
7
|
-
const API_BASE = `https://api.github.com/repos/${REPO}`
|
|
8
|
-
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}`
|
|
9
|
-
|
|
10
|
-
export { REPO }
|
|
11
|
-
|
|
12
|
-
// --- types ---
|
|
13
|
-
|
|
14
|
-
export type WorkflowEntry = {
|
|
15
|
-
name: string
|
|
16
|
-
file: string
|
|
17
|
-
description?: string
|
|
18
|
-
secrets?: string[]
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// --- parse workflow header ---
|
|
22
|
-
// extracts description and required secrets from the yaml header comment block.
|
|
23
|
-
|
|
24
|
-
export function parseWorkflowHeader(content: string): { description: string; secrets: string[] } {
|
|
25
|
-
const lines = content.split("\n")
|
|
26
|
-
let description = ""
|
|
27
|
-
const secrets: string[] = []
|
|
28
|
-
let inHeader = false
|
|
29
|
-
|
|
30
|
-
for (const line of lines) {
|
|
31
|
-
if (line.startsWith("# ---")) {
|
|
32
|
-
if (inHeader) break
|
|
33
|
-
inHeader = true
|
|
34
|
-
continue
|
|
35
|
-
}
|
|
36
|
-
if (!inHeader) continue
|
|
37
|
-
if (!line.startsWith("#")) break
|
|
38
|
-
|
|
39
|
-
const text = line.replace(/^#\s?/, "").trim()
|
|
40
|
-
|
|
41
|
-
if (/^required secrets:/i.test(text)) continue
|
|
42
|
-
if (/^\w+\s+[—–]/.test(text)) {
|
|
43
|
-
secrets.push(text)
|
|
44
|
-
continue
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
if (!description && text && !/^copy this|^can also|^actions used|^paste this|^this one/i.test(text)) {
|
|
48
|
-
description = text
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return { description, secrets }
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// --- api ---
|
|
56
|
-
|
|
57
|
-
export async function fetchTags(): Promise<string[]> {
|
|
58
|
-
const url = `${API_BASE}/tags?per_page=20`
|
|
59
|
-
const res = await fetch(url, {
|
|
60
|
-
headers: { Accept: "application/vnd.github.v3+json" }
|
|
61
|
-
})
|
|
62
|
-
if (!res.ok) return []
|
|
63
|
-
const tags: any[] = await res.json()
|
|
64
|
-
return tags.map((t) => t.name)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export async function fetchWorkflowList(gitRef: string): Promise<WorkflowEntry[]> {
|
|
68
|
-
const url = `${API_BASE}/contents/workflows?ref=${gitRef}`
|
|
69
|
-
const res = await fetch(url, {
|
|
70
|
-
headers: { Accept: "application/vnd.github.v3+json" }
|
|
71
|
-
})
|
|
72
|
-
|
|
73
|
-
if (!res.ok) {
|
|
74
|
-
throw new Error(`failed to list workflows from ${REPO} @ ${gitRef} (${res.status})`)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const entries: any[] = await res.json()
|
|
78
|
-
|
|
79
|
-
return entries
|
|
80
|
-
.filter((e) => e.type === "file" && e.name.endsWith(".yml"))
|
|
81
|
-
.map((e) => ({
|
|
82
|
-
name: e.name.replace(/\.yml$/, ""),
|
|
83
|
-
file: e.name
|
|
84
|
-
}))
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export async function fetchWorkflowContent(file: string, gitRef: string): Promise<string> {
|
|
88
|
-
const url = `${RAW_BASE}/${gitRef}/workflows/${file}`
|
|
89
|
-
const res = await fetch(url)
|
|
90
|
-
|
|
91
|
-
if (!res.ok) {
|
|
92
|
-
throw new Error(`failed to fetch workflows/${file} from ${REPO} @ ${gitRef} (${res.status})`)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
return await res.text()
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
export async function enrichWorkflows(workflows: WorkflowEntry[], gitRef: string): Promise<WorkflowEntry[]> {
|
|
99
|
-
const enriched: WorkflowEntry[] = []
|
|
100
|
-
|
|
101
|
-
for (const wf of workflows) {
|
|
102
|
-
try {
|
|
103
|
-
const content = await fetchWorkflowContent(wf.file, gitRef)
|
|
104
|
-
const { description, secrets } = parseWorkflowHeader(content)
|
|
105
|
-
enriched.push({ ...wf, description, secrets })
|
|
106
|
-
} catch {
|
|
107
|
-
enriched.push(wf)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
return enriched
|
|
112
|
-
}
|
|
113
|
-
|
|
File without changes
|