@justanarthur/just-github-actions-n-workflows-cli 0.0.0-beta.11 → 0.0.0-beta.13
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 +209 -0
- package/dist/commands/status.js +53 -0
- package/dist/commands/update.js +108 -0
- package/dist/github.js +96 -0
- package/dist/index.js +6 -0
- package/{src/init.ts → dist/init.js} +2 -1
- package/dist/lockfile.js +72 -0
- package/package.json +11 -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,209 @@
|
|
|
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
|
+
import { injectRefComment, mergeLockfile, readLockfile, writeLockfile, } from "../lockfile.js";
|
|
7
|
+
export default class Init extends Command {
|
|
8
|
+
static description = "Scaffold workflow files into .github/workflows/ of the current repo";
|
|
9
|
+
static examples = [
|
|
10
|
+
"<%= config.bin %> init",
|
|
11
|
+
"<%= config.bin %> init bump-version",
|
|
12
|
+
"<%= config.bin %> init --ref v1.0.0",
|
|
13
|
+
"<%= config.bin %> init --list",
|
|
14
|
+
"<%= config.bin %> init --yes --force",
|
|
15
|
+
];
|
|
16
|
+
static args = {
|
|
17
|
+
workflows: Args.string({
|
|
18
|
+
description: "Specific workflow names to install (space-separated)",
|
|
19
|
+
required: false,
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
22
|
+
static strict = false;
|
|
23
|
+
static flags = {
|
|
24
|
+
list: Flags.boolean({
|
|
25
|
+
char: "l",
|
|
26
|
+
description: "Show available workflows",
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
force: Flags.boolean({
|
|
30
|
+
char: "f",
|
|
31
|
+
description: "Overwrite existing workflow files",
|
|
32
|
+
default: false,
|
|
33
|
+
}),
|
|
34
|
+
yes: Flags.boolean({
|
|
35
|
+
char: "y",
|
|
36
|
+
description: "Skip interactive prompts, install all workflows",
|
|
37
|
+
default: false,
|
|
38
|
+
}),
|
|
39
|
+
ref: Flags.string({
|
|
40
|
+
description: "Git ref to fetch from (branch, tag, or sha)",
|
|
41
|
+
default: "main",
|
|
42
|
+
}),
|
|
43
|
+
};
|
|
44
|
+
async run() {
|
|
45
|
+
const { argv, flags } = await this.parse(Init);
|
|
46
|
+
const positional = argv;
|
|
47
|
+
// --- list mode ---
|
|
48
|
+
if (flags.list) {
|
|
49
|
+
await this.listWorkflows(flags.ref);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// --- interactive / install mode ---
|
|
53
|
+
this.log();
|
|
54
|
+
this.log(ux.colorize("bold", " just-github-actions-n-workflows"));
|
|
55
|
+
this.log(ux.colorize("dim", " release automation toolkit\n"));
|
|
56
|
+
const ref = await this.resolveRef(flags, positional);
|
|
57
|
+
const selected = await this.selectWorkflows(ref, flags, positional);
|
|
58
|
+
if (selected.length === 0) {
|
|
59
|
+
this.log(ux.colorize("yellow", "\n no workflows selected — nothing to do.\n"));
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const { created, skipped } = await this.installWorkflows(selected, ref, flags);
|
|
63
|
+
this.log(`\n done — ${ux.colorize("green", `${created} created`)}, ${skipped} skipped\n`);
|
|
64
|
+
this.printSecretsReminder(selected);
|
|
65
|
+
this.printNextSteps(created, selected);
|
|
66
|
+
}
|
|
67
|
+
// ── step 1: resolve ref ────────────────────────────────
|
|
68
|
+
async resolveRef(flags, positional) {
|
|
69
|
+
if (flags.ref !== "main" || positional.length > 0 || flags.yes) {
|
|
70
|
+
return flags.ref;
|
|
71
|
+
}
|
|
72
|
+
this.log(ux.colorize("bold", " step 1 — select version\n"));
|
|
73
|
+
const tags = await fetchTags();
|
|
74
|
+
if (tags.length === 0)
|
|
75
|
+
return "main";
|
|
76
|
+
const choices = [
|
|
77
|
+
{ name: `main ${ux.colorize("dim", "(latest)")}`, value: "main" },
|
|
78
|
+
...tags.slice(0, 9).map((tag) => ({ name: tag, value: tag })),
|
|
79
|
+
];
|
|
80
|
+
const ref = await select({
|
|
81
|
+
message: "Pick a version",
|
|
82
|
+
choices,
|
|
83
|
+
default: "main",
|
|
84
|
+
});
|
|
85
|
+
this.log(ux.colorize("green", `\n → using ${ref}\n`));
|
|
86
|
+
return ref;
|
|
87
|
+
}
|
|
88
|
+
// ── step 2: select workflows ───────────────────────────
|
|
89
|
+
async selectWorkflows(ref, flags, positional) {
|
|
90
|
+
let available = await fetchWorkflowList(ref);
|
|
91
|
+
available = await enrichWorkflows(available, ref);
|
|
92
|
+
if (positional.length > 0) {
|
|
93
|
+
return positional.map((name) => {
|
|
94
|
+
const key = name.replace(/\.yml$/, "");
|
|
95
|
+
const wf = available.find((w) => w.name === key);
|
|
96
|
+
if (!wf) {
|
|
97
|
+
this.error(`Unknown workflow "${name}". Available: ${available.map((w) => w.name).join(", ")}`);
|
|
98
|
+
}
|
|
99
|
+
return wf;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (flags.yes)
|
|
103
|
+
return available;
|
|
104
|
+
this.log(ux.colorize("bold", " step 2 — select workflows\n"));
|
|
105
|
+
const selected = await checkbox({
|
|
106
|
+
message: "Select workflows to install",
|
|
107
|
+
choices: available.map((wf) => {
|
|
108
|
+
const secretNames = wf.secrets?.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ");
|
|
109
|
+
const hint = [
|
|
110
|
+
wf.description,
|
|
111
|
+
secretNames ? `requires: ${secretNames}` : null,
|
|
112
|
+
].filter(Boolean).join(" · ");
|
|
113
|
+
return {
|
|
114
|
+
name: `${wf.name.padEnd(26)} ${ux.colorize("dim", hint || wf.file)}`,
|
|
115
|
+
value: wf.name,
|
|
116
|
+
checked: true,
|
|
117
|
+
};
|
|
118
|
+
}),
|
|
119
|
+
});
|
|
120
|
+
return available.filter((w) => selected.includes(w.name));
|
|
121
|
+
}
|
|
122
|
+
// ── step 3: install ────────────────────────────────────
|
|
123
|
+
async installWorkflows(selected, ref, flags) {
|
|
124
|
+
const targetDir = join(process.cwd(), ".github", "workflows");
|
|
125
|
+
mkdirSync(targetDir, { recursive: true });
|
|
126
|
+
if (!flags.yes) {
|
|
127
|
+
this.log(ux.colorize("bold", " step 3 — install\n"));
|
|
128
|
+
}
|
|
129
|
+
this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${ref}\n`));
|
|
130
|
+
let created = 0;
|
|
131
|
+
let skipped = 0;
|
|
132
|
+
const installed = [];
|
|
133
|
+
for (const workflow of selected) {
|
|
134
|
+
const targetPath = join(targetDir, workflow.file);
|
|
135
|
+
if (!flags.force && existsSync(targetPath)) {
|
|
136
|
+
this.log(` ${ux.colorize("yellow", "skip")} ${workflow.file} ${ux.colorize("dim", "(already exists, use --force)")}`);
|
|
137
|
+
skipped++;
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
let content = await fetchWorkflowContent(workflow.file, ref);
|
|
142
|
+
content = injectRefComment(content, ref);
|
|
143
|
+
writeFileSync(targetPath, content, "utf-8");
|
|
144
|
+
this.log(` ${ux.colorize("green", "create")} .github/workflows/${workflow.file}`);
|
|
145
|
+
installed.push({ name: workflow.name, file: workflow.file });
|
|
146
|
+
created++;
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
this.log(` ${ux.colorize("red", "error")} ${workflow.file}: ${error.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// --- write lock file ---
|
|
153
|
+
if (installed.length > 0) {
|
|
154
|
+
const existing = readLockfile();
|
|
155
|
+
const lock = mergeLockfile(existing, ref, installed);
|
|
156
|
+
writeLockfile(lock);
|
|
157
|
+
this.log(ux.colorize("dim", `\n lock file written → .github/workflows/.toolkit-lock.json`));
|
|
158
|
+
}
|
|
159
|
+
return { created, skipped };
|
|
160
|
+
}
|
|
161
|
+
// ── step 4: secrets reminder ───────────────────────────
|
|
162
|
+
printSecretsReminder(selected) {
|
|
163
|
+
const allSecrets = new Map();
|
|
164
|
+
for (const wf of selected) {
|
|
165
|
+
for (const s of wf.secrets || []) {
|
|
166
|
+
const [name, ...descParts] = s.split(/\s+[—–]\s*/);
|
|
167
|
+
if (name && !allSecrets.has(name.trim())) {
|
|
168
|
+
allSecrets.set(name.trim(), descParts.join(" — ").trim());
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
if (allSecrets.size === 0)
|
|
173
|
+
return;
|
|
174
|
+
this.log(ux.colorize("bold", " required secrets:\n"));
|
|
175
|
+
for (const [name, desc] of allSecrets) {
|
|
176
|
+
this.log(` • ${ux.colorize("cyan", name.padEnd(18))} ${ux.colorize("dim", desc)}`);
|
|
177
|
+
}
|
|
178
|
+
this.log(ux.colorize("dim", `\n set these in your repo → Settings → Secrets → Actions\n`));
|
|
179
|
+
}
|
|
180
|
+
// ── next steps ─────────────────────────────────────────
|
|
181
|
+
printNextSteps(created, selected) {
|
|
182
|
+
if (created === 0)
|
|
183
|
+
return;
|
|
184
|
+
const hasSecrets = selected.some((w) => w.secrets && w.secrets.length > 0);
|
|
185
|
+
this.log(ux.colorize("bold", " next steps:\n"));
|
|
186
|
+
this.log(` 1. ${hasSecrets ? "set the secrets listed above" : "set the GH_TOKEN secret in your repo settings"}`);
|
|
187
|
+
this.log(` 2. adjust push.branches / push.tags triggers for your repo`);
|
|
188
|
+
this.log(` 3. commit and push:`);
|
|
189
|
+
this.log(ux.colorize("dim", ` git add .github/ && git commit -m "ci: add workflows" && git push`));
|
|
190
|
+
this.log();
|
|
191
|
+
}
|
|
192
|
+
// ── list mode ──────────────────────────────────────────
|
|
193
|
+
async listWorkflows(ref) {
|
|
194
|
+
const workflows = await enrichWorkflows(await fetchWorkflowList(ref), ref);
|
|
195
|
+
this.log(`\n${ux.colorize("bold", "available workflows")} ${ux.colorize("dim", `(${REPO} @ ${ref})`)}\n`);
|
|
196
|
+
for (const w of workflows) {
|
|
197
|
+
this.log(` ${ux.colorize("cyan", w.name.padEnd(28))} → .github/workflows/${w.file}`);
|
|
198
|
+
if (w.description) {
|
|
199
|
+
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", w.description)}`);
|
|
200
|
+
}
|
|
201
|
+
if (w.secrets?.length) {
|
|
202
|
+
const names = w.secrets.map((s) => s.split(/\s+[—–]\s*/)[0]).join(", ");
|
|
203
|
+
this.log(` ${"".padEnd(28)} ${ux.colorize("dim", `secrets: ${names}`)}`);
|
|
204
|
+
}
|
|
205
|
+
this.log();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// cli/src/commands/status.ts
|
|
2
|
+
// ---
|
|
3
|
+
// shows the current state of installed workflows:
|
|
4
|
+
// version, install date, and whether each is outdated.
|
|
5
|
+
// ---
|
|
6
|
+
import { Command, Flags, ux } from "@oclif/core";
|
|
7
|
+
import { fetchLatestTag } from "../github.js";
|
|
8
|
+
import { readLockfile } from "../lockfile.js";
|
|
9
|
+
export default class Status extends Command {
|
|
10
|
+
static description = "Show the status of installed workflows";
|
|
11
|
+
static examples = [
|
|
12
|
+
"<%= config.bin %> status",
|
|
13
|
+
"<%= config.bin %> status --ref v2.0.0",
|
|
14
|
+
];
|
|
15
|
+
static flags = {
|
|
16
|
+
ref: Flags.string({
|
|
17
|
+
description: "Compare against this ref instead of the latest tag",
|
|
18
|
+
}),
|
|
19
|
+
};
|
|
20
|
+
async run() {
|
|
21
|
+
const { flags } = await this.parse(Status);
|
|
22
|
+
const lock = readLockfile();
|
|
23
|
+
if (!lock || Object.keys(lock.workflows).length === 0) {
|
|
24
|
+
this.log(ux.colorize("yellow", "\n no workflows installed — run `init` first.\n"));
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const targetRef = flags.ref ?? await fetchLatestTag();
|
|
28
|
+
const entries = Object.values(lock.workflows);
|
|
29
|
+
this.log();
|
|
30
|
+
this.log(ux.colorize("bold", " installed workflows"));
|
|
31
|
+
this.log(ux.colorize("dim", ` latest: ${targetRef}\n`));
|
|
32
|
+
let outdatedCount = 0;
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
const current = entry.ref === targetRef;
|
|
35
|
+
const icon = current ? ux.colorize("green", "✓") : ux.colorize("yellow", "⬆");
|
|
36
|
+
const refLabel = current
|
|
37
|
+
? ux.colorize("green", entry.ref)
|
|
38
|
+
: `${ux.colorize("yellow", entry.ref)} → ${ux.colorize("green", targetRef)}`;
|
|
39
|
+
const date = ux.colorize("dim", new Date(entry.installedAt).toLocaleDateString());
|
|
40
|
+
this.log(` ${icon} ${ux.colorize("cyan", entry.name.padEnd(28))} ${refLabel.padEnd(50)} ${date}`);
|
|
41
|
+
if (!current)
|
|
42
|
+
outdatedCount++;
|
|
43
|
+
}
|
|
44
|
+
this.log();
|
|
45
|
+
if (outdatedCount > 0) {
|
|
46
|
+
this.log(ux.colorize("yellow", ` ${outdatedCount} workflow(s) can be updated — run \`update\` to upgrade.\n`));
|
|
47
|
+
}
|
|
48
|
+
else {
|
|
49
|
+
this.log(ux.colorize("green", ` all ${entries.length} workflow(s) are up to date.\n`));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=status.js.map
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// cli/src/commands/update.ts
|
|
2
|
+
// ---
|
|
3
|
+
// updates previously installed workflows to a newer version.
|
|
4
|
+
// reads .toolkit-lock.json to find installed workflows, then
|
|
5
|
+
// re-fetches them from the specified (or latest) git ref.
|
|
6
|
+
// ---
|
|
7
|
+
import { Command, Flags, ux } from "@oclif/core";
|
|
8
|
+
import { confirm } from "@inquirer/prompts";
|
|
9
|
+
import { writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { REPO, fetchLatestTag, fetchWorkflowContent, } from "../github.js";
|
|
12
|
+
import { injectRefComment, mergeLockfile, readLockfile, writeLockfile, } from "../lockfile.js";
|
|
13
|
+
export default class Update extends Command {
|
|
14
|
+
static description = "Update installed workflows to a newer version";
|
|
15
|
+
static examples = [
|
|
16
|
+
"<%= config.bin %> update",
|
|
17
|
+
"<%= config.bin %> update --ref v2.0.0",
|
|
18
|
+
"<%= config.bin %> update --yes",
|
|
19
|
+
];
|
|
20
|
+
static flags = {
|
|
21
|
+
ref: Flags.string({
|
|
22
|
+
description: "Target git ref to update to (branch, tag, or sha)",
|
|
23
|
+
}),
|
|
24
|
+
yes: Flags.boolean({
|
|
25
|
+
char: "y",
|
|
26
|
+
description: "Skip confirmation prompt",
|
|
27
|
+
default: false,
|
|
28
|
+
}),
|
|
29
|
+
};
|
|
30
|
+
async run() {
|
|
31
|
+
const { flags } = await this.parse(Update);
|
|
32
|
+
// --- read lock file ---
|
|
33
|
+
const lock = readLockfile();
|
|
34
|
+
if (!lock || Object.keys(lock.workflows).length === 0) {
|
|
35
|
+
this.error("No .toolkit-lock.json found. Run `init` first to install workflows.", { exit: 1 });
|
|
36
|
+
}
|
|
37
|
+
// --- resolve target ref ---
|
|
38
|
+
const targetRef = flags.ref ?? await fetchLatestTag();
|
|
39
|
+
const entries = Object.values(lock.workflows);
|
|
40
|
+
this.log();
|
|
41
|
+
this.log(ux.colorize("bold", " workflow update check"));
|
|
42
|
+
this.log(ux.colorize("dim", ` target: ${targetRef}\n`));
|
|
43
|
+
// --- compare versions ---
|
|
44
|
+
const outdated = [];
|
|
45
|
+
const upToDate = [];
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.ref === targetRef) {
|
|
48
|
+
upToDate.push(entry);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
outdated.push(entry);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// --- print status table ---
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const current = entry.ref === targetRef;
|
|
57
|
+
const icon = current ? ux.colorize("green", "✓") : ux.colorize("yellow", "⬆");
|
|
58
|
+
const refLabel = current
|
|
59
|
+
? ux.colorize("green", entry.ref)
|
|
60
|
+
: `${ux.colorize("yellow", entry.ref)} → ${ux.colorize("green", targetRef)}`;
|
|
61
|
+
this.log(` ${icon} ${ux.colorize("cyan", entry.name.padEnd(28))} ${refLabel}`);
|
|
62
|
+
}
|
|
63
|
+
this.log();
|
|
64
|
+
if (outdated.length === 0) {
|
|
65
|
+
this.log(ux.colorize("green", ` all ${entries.length} workflow(s) are up to date at ${targetRef}\n`));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// --- confirm ---
|
|
69
|
+
if (!flags.yes) {
|
|
70
|
+
const proceed = await confirm({
|
|
71
|
+
message: `Update ${outdated.length} workflow(s) to ${targetRef}?`,
|
|
72
|
+
default: true,
|
|
73
|
+
});
|
|
74
|
+
if (!proceed) {
|
|
75
|
+
this.log(ux.colorize("yellow", "\n cancelled.\n"));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.log();
|
|
79
|
+
}
|
|
80
|
+
// --- update files ---
|
|
81
|
+
const targetDir = join(process.cwd(), ".github", "workflows");
|
|
82
|
+
let updated = 0;
|
|
83
|
+
let errors = 0;
|
|
84
|
+
const updatedEntries = [];
|
|
85
|
+
this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${targetRef}\n`));
|
|
86
|
+
for (const entry of outdated) {
|
|
87
|
+
try {
|
|
88
|
+
let content = await fetchWorkflowContent(entry.file, targetRef);
|
|
89
|
+
content = injectRefComment(content, targetRef);
|
|
90
|
+
writeFileSync(join(targetDir, entry.file), content, "utf-8");
|
|
91
|
+
this.log(` ${ux.colorize("green", "update")} ${entry.file} ${ux.colorize("dim", `(${entry.ref} → ${targetRef})`)}`);
|
|
92
|
+
updatedEntries.push({ name: entry.name, file: entry.file });
|
|
93
|
+
updated++;
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.log(` ${ux.colorize("red", "error")} ${entry.file}: ${error.message}`);
|
|
97
|
+
errors++;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
// --- update lock file ---
|
|
101
|
+
if (updatedEntries.length > 0) {
|
|
102
|
+
const updatedLock = mergeLockfile(lock, targetRef, updatedEntries);
|
|
103
|
+
writeLockfile(updatedLock);
|
|
104
|
+
}
|
|
105
|
+
this.log(`\n done — ${ux.colorize("green", `${updated} updated`)}, ${errors} errors, ${upToDate.length} already current\n`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
//# sourceMappingURL=update.js.map
|
package/dist/github.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
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
|
+
// --- fetchLatestTag ---
|
|
53
|
+
// returns the most recent tag, or "main" if no tags exist.
|
|
54
|
+
export async function fetchLatestTag() {
|
|
55
|
+
const tags = await fetchTags();
|
|
56
|
+
return tags.length > 0 ? tags[0] : "main";
|
|
57
|
+
}
|
|
58
|
+
export async function fetchWorkflowList(gitRef) {
|
|
59
|
+
const url = `${API_BASE}/contents/workflows?ref=${gitRef}`;
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
headers: { Accept: "application/vnd.github.v3+json" }
|
|
62
|
+
});
|
|
63
|
+
if (!res.ok) {
|
|
64
|
+
throw new Error(`failed to list workflows from ${REPO} @ ${gitRef} (${res.status})`);
|
|
65
|
+
}
|
|
66
|
+
const entries = await res.json();
|
|
67
|
+
return entries
|
|
68
|
+
.filter((e) => e.type === "file" && e.name.endsWith(".yml"))
|
|
69
|
+
.map((e) => ({
|
|
70
|
+
name: e.name.replace(/\.yml$/, ""),
|
|
71
|
+
file: e.name
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
export async function fetchWorkflowContent(file, gitRef) {
|
|
75
|
+
const url = `${RAW_BASE}/${gitRef}/workflows/${file}`;
|
|
76
|
+
const res = await fetch(url);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
throw new Error(`failed to fetch workflows/${file} from ${REPO} @ ${gitRef} (${res.status})`);
|
|
79
|
+
}
|
|
80
|
+
return await res.text();
|
|
81
|
+
}
|
|
82
|
+
export async function enrichWorkflows(workflows, gitRef) {
|
|
83
|
+
const enriched = [];
|
|
84
|
+
for (const wf of workflows) {
|
|
85
|
+
try {
|
|
86
|
+
const content = await fetchWorkflowContent(wf.file, gitRef);
|
|
87
|
+
const { description, secrets } = parseWorkflowHeader(content);
|
|
88
|
+
enriched.push({ ...wf, description, secrets });
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
enriched.push(wf);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return enriched;
|
|
95
|
+
}
|
|
96
|
+
//# 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/dist/lockfile.js
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
// cli/src/lockfile.ts
|
|
2
|
+
// ---
|
|
3
|
+
// reads and writes .github/workflows/.toolkit-lock.json
|
|
4
|
+
// tracks which version (git ref) each workflow was installed from.
|
|
5
|
+
// ---
|
|
6
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
// --- constants ---
|
|
9
|
+
const LOCKFILE_NAME = ".toolkit-lock.json";
|
|
10
|
+
export function lockfilePath() {
|
|
11
|
+
return join(process.cwd(), ".github", "workflows", LOCKFILE_NAME);
|
|
12
|
+
}
|
|
13
|
+
// --- read ---
|
|
14
|
+
export function readLockfile() {
|
|
15
|
+
const path = lockfilePath();
|
|
16
|
+
if (!existsSync(path))
|
|
17
|
+
return null;
|
|
18
|
+
try {
|
|
19
|
+
const raw = readFileSync(path, "utf-8");
|
|
20
|
+
return JSON.parse(raw);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// --- write ---
|
|
27
|
+
export function writeLockfile(lock) {
|
|
28
|
+
const path = lockfilePath();
|
|
29
|
+
writeFileSync(path, JSON.stringify(lock, null, 2) + "\n", "utf-8");
|
|
30
|
+
}
|
|
31
|
+
// --- merge ---
|
|
32
|
+
// merges new entries into an existing lockfile (or creates a new one).
|
|
33
|
+
export function mergeLockfile(existing, ref, entries) {
|
|
34
|
+
const now = new Date().toISOString();
|
|
35
|
+
const lock = existing ?? {
|
|
36
|
+
ref,
|
|
37
|
+
installedAt: now,
|
|
38
|
+
workflows: {},
|
|
39
|
+
};
|
|
40
|
+
lock.ref = ref;
|
|
41
|
+
lock.installedAt = now;
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
lock.workflows[entry.file] = {
|
|
44
|
+
name: entry.name,
|
|
45
|
+
file: entry.file,
|
|
46
|
+
ref,
|
|
47
|
+
installedAt: now,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
return lock;
|
|
51
|
+
}
|
|
52
|
+
// --- version comment ---
|
|
53
|
+
// prepends a `# toolkit-ref: <ref>` comment to workflow content.
|
|
54
|
+
export function injectRefComment(content, ref) {
|
|
55
|
+
const marker = "# toolkit-ref:";
|
|
56
|
+
const comment = `${marker} ${ref}`;
|
|
57
|
+
// replace existing marker if present
|
|
58
|
+
if (content.includes(marker)) {
|
|
59
|
+
return content.replace(/^# toolkit-ref:.*$/m, comment);
|
|
60
|
+
}
|
|
61
|
+
// prepend before first non-comment, non-empty line
|
|
62
|
+
const lines = content.split("\n");
|
|
63
|
+
const insertIdx = lines.findIndex((l) => l.trim() !== "" && !l.startsWith("#"));
|
|
64
|
+
if (insertIdx >= 0) {
|
|
65
|
+
lines.splice(insertIdx, 0, comment);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
lines.push(comment);
|
|
69
|
+
}
|
|
70
|
+
return lines.join("\n");
|
|
71
|
+
}
|
|
72
|
+
//# sourceMappingURL=lockfile.js.map
|
package/package.json
CHANGED
|
@@ -1,14 +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.13",
|
|
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
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc"
|
|
8
11
|
},
|
|
9
|
-
"scripts": {},
|
|
10
12
|
"files": [
|
|
11
|
-
"
|
|
13
|
+
"bin/**/*.js",
|
|
14
|
+
"dist/**/*.js",
|
|
12
15
|
"package.json",
|
|
13
16
|
"README.md"
|
|
14
17
|
],
|
|
@@ -23,8 +26,11 @@
|
|
|
23
26
|
"@oclif/core": "^4",
|
|
24
27
|
"@inquirer/prompts": "^7"
|
|
25
28
|
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"typescript": "^5"
|
|
31
|
+
},
|
|
26
32
|
"oclif": {
|
|
27
|
-
"commands": "./
|
|
33
|
+
"commands": "./dist/commands",
|
|
28
34
|
"default": "init"
|
|
29
35
|
},
|
|
30
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
|