@justanarthur/just-github-actions-n-workflows-cli 0.0.0-beta.12 → 0.0.0-beta.15
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 +16 -2
- package/dist/commands/status.js +53 -0
- package/dist/commands/update.js +108 -0
- package/dist/github.js +19 -2
- package/dist/lockfile.js +72 -0
- package/package.json +1 -1
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,7 @@ import { checkbox, select } from "@inquirer/prompts";
|
|
|
3
3
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { REPO, enrichWorkflows, fetchTags, fetchWorkflowContent, fetchWorkflowList, } from "../github.js";
|
|
6
|
+
import { injectRefComment, mergeLockfile, readLockfile, writeLockfile, } from "../lockfile.js";
|
|
6
7
|
export default class Init extends Command {
|
|
7
8
|
static description = "Scaffold workflow files into .github/workflows/ of the current repo";
|
|
8
9
|
static examples = [
|
|
@@ -74,7 +75,10 @@ export default class Init extends Command {
|
|
|
74
75
|
return "main";
|
|
75
76
|
const choices = [
|
|
76
77
|
{ name: `main ${ux.colorize("dim", "(latest)")}`, value: "main" },
|
|
77
|
-
...tags.slice(0, 9).map((
|
|
78
|
+
...tags.slice(0, 9).map((t) => ({
|
|
79
|
+
name: `${t.version} ${ux.colorize("dim", `(${t.tag})`)}`,
|
|
80
|
+
value: t.tag,
|
|
81
|
+
})),
|
|
78
82
|
];
|
|
79
83
|
const ref = await select({
|
|
80
84
|
message: "Pick a version",
|
|
@@ -128,6 +132,7 @@ export default class Init extends Command {
|
|
|
128
132
|
this.log(ux.colorize("dim", ` fetching from ${REPO} @ ${ref}\n`));
|
|
129
133
|
let created = 0;
|
|
130
134
|
let skipped = 0;
|
|
135
|
+
const installed = [];
|
|
131
136
|
for (const workflow of selected) {
|
|
132
137
|
const targetPath = join(targetDir, workflow.file);
|
|
133
138
|
if (!flags.force && existsSync(targetPath)) {
|
|
@@ -136,15 +141,24 @@ export default class Init extends Command {
|
|
|
136
141
|
continue;
|
|
137
142
|
}
|
|
138
143
|
try {
|
|
139
|
-
|
|
144
|
+
let content = await fetchWorkflowContent(workflow.file, ref);
|
|
145
|
+
content = injectRefComment(content, ref);
|
|
140
146
|
writeFileSync(targetPath, content, "utf-8");
|
|
141
147
|
this.log(` ${ux.colorize("green", "create")} .github/workflows/${workflow.file}`);
|
|
148
|
+
installed.push({ name: workflow.name, file: workflow.file });
|
|
142
149
|
created++;
|
|
143
150
|
}
|
|
144
151
|
catch (error) {
|
|
145
152
|
this.log(` ${ux.colorize("red", "error")} ${workflow.file}: ${error.message}`);
|
|
146
153
|
}
|
|
147
154
|
}
|
|
155
|
+
// --- write lock file ---
|
|
156
|
+
if (installed.length > 0) {
|
|
157
|
+
const existing = readLockfile();
|
|
158
|
+
const lock = mergeLockfile(existing, ref, installed);
|
|
159
|
+
writeLockfile(lock);
|
|
160
|
+
this.log(ux.colorize("dim", `\n lock file written → .github/workflows/.toolkit-lock.json`));
|
|
161
|
+
}
|
|
148
162
|
return { created, skipped };
|
|
149
163
|
}
|
|
150
164
|
// ── step 4: secrets reminder ───────────────────────────
|
|
@@ -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
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
2
|
const require = createRequire(import.meta.url);
|
|
3
3
|
const pkg = require("../package.json");
|
|
4
|
+
const rootPkg = require("../../package.json");
|
|
4
5
|
const REPO = pkg.repository.url
|
|
5
6
|
.replace(/^https?:\/\/github\.com\//, "")
|
|
6
7
|
.replace(/\.git$/, "");
|
|
7
8
|
const API_BASE = `https://api.github.com/repos/${REPO}`;
|
|
8
9
|
const RAW_BASE = `https://raw.githubusercontent.com/${REPO}`;
|
|
10
|
+
// --- root package tag prefix ---
|
|
11
|
+
// tags for the root toolkit package follow `<root-name>@<version>`.
|
|
12
|
+
// the CLI only shows these tags as version choices since workflows are part of the root package.
|
|
13
|
+
const TAG_PREFIX = `${rootPkg.name}@`;
|
|
9
14
|
export { REPO };
|
|
10
15
|
// --- parse workflow header ---
|
|
11
16
|
// extracts description and required secrets from the yaml header comment block.
|
|
@@ -40,14 +45,26 @@ export function parseWorkflowHeader(content) {
|
|
|
40
45
|
}
|
|
41
46
|
// --- api ---
|
|
42
47
|
export async function fetchTags() {
|
|
43
|
-
const url = `${API_BASE}/tags?per_page=
|
|
48
|
+
const url = `${API_BASE}/tags?per_page=100`;
|
|
44
49
|
const res = await fetch(url, {
|
|
45
50
|
headers: { Accept: "application/vnd.github.v3+json" }
|
|
46
51
|
});
|
|
47
52
|
if (!res.ok)
|
|
48
53
|
return [];
|
|
49
54
|
const tags = await res.json();
|
|
50
|
-
return tags
|
|
55
|
+
return tags
|
|
56
|
+
.map((t) => t.name)
|
|
57
|
+
.filter((name) => name.startsWith(TAG_PREFIX))
|
|
58
|
+
.map((name) => ({
|
|
59
|
+
tag: name,
|
|
60
|
+
version: name.slice(TAG_PREFIX.length)
|
|
61
|
+
}));
|
|
62
|
+
}
|
|
63
|
+
// --- fetchLatestTag ---
|
|
64
|
+
// returns the most recent root toolkit tag, or "main" if none exist.
|
|
65
|
+
export async function fetchLatestTag() {
|
|
66
|
+
const tags = await fetchTags();
|
|
67
|
+
return tags.length > 0 ? tags[0].tag : "main";
|
|
51
68
|
}
|
|
52
69
|
export async function fetchWorkflowList(gitRef) {
|
|
53
70
|
const url = `${API_BASE}/contents/workflows?ref=${gitRef}`;
|
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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@justanarthur/just-github-actions-n-workflows-cli",
|
|
3
|
-
"version": "0.0.0-beta.
|
|
3
|
+
"version": "0.0.0-beta.15",
|
|
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": {
|