@acidgreen-au/ag-cicd-cli 0.0.1
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/cli.d.mts +1 -0
- package/dist/cli.mjs +584 -0
- package/package.json +51 -0
package/dist/cli.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
package/dist/cli.mjs
ADDED
|
@@ -0,0 +1,584 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { program } from "commander";
|
|
3
|
+
import { execSync, spawn } from "node:child_process";
|
|
4
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
5
|
+
|
|
6
|
+
//#region src/commands/check-tag.ts
|
|
7
|
+
function getVersionFromPackage(packagePath) {
|
|
8
|
+
const content = readFileSync(packagePath, "utf-8");
|
|
9
|
+
return JSON.parse(content).version;
|
|
10
|
+
}
|
|
11
|
+
function tagExists(tag) {
|
|
12
|
+
try {
|
|
13
|
+
execSync(`git rev-parse --verify "refs/tags/${tag}"`, {
|
|
14
|
+
encoding: "utf-8",
|
|
15
|
+
stdio: [
|
|
16
|
+
"pipe",
|
|
17
|
+
"pipe",
|
|
18
|
+
"pipe"
|
|
19
|
+
]
|
|
20
|
+
});
|
|
21
|
+
return true;
|
|
22
|
+
} catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function createTag(tag, message) {
|
|
27
|
+
execSync(`git tag -a "${tag}" -m "${message}"`, {
|
|
28
|
+
encoding: "utf-8",
|
|
29
|
+
stdio: [
|
|
30
|
+
"pipe",
|
|
31
|
+
"pipe",
|
|
32
|
+
"pipe"
|
|
33
|
+
]
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function pushTag(tag, remote) {
|
|
37
|
+
execSync(`git push "${remote}" "${tag}"`, {
|
|
38
|
+
encoding: "utf-8",
|
|
39
|
+
stdio: [
|
|
40
|
+
"pipe",
|
|
41
|
+
"pipe",
|
|
42
|
+
"pipe"
|
|
43
|
+
]
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
function checkTag(options) {
|
|
47
|
+
const packagePath = options.packagePath ?? "package.json";
|
|
48
|
+
const prefix = options.prefix ?? "v";
|
|
49
|
+
const remote = options.remote ?? "origin";
|
|
50
|
+
const version = getVersionFromPackage(packagePath);
|
|
51
|
+
const tag = `${prefix}${version}`;
|
|
52
|
+
if (tagExists(tag)) return {
|
|
53
|
+
version,
|
|
54
|
+
tag,
|
|
55
|
+
exists: true,
|
|
56
|
+
created: false,
|
|
57
|
+
pushed: false,
|
|
58
|
+
message: `Tag ${tag} already exists`
|
|
59
|
+
};
|
|
60
|
+
if (!options.create) return {
|
|
61
|
+
version,
|
|
62
|
+
tag,
|
|
63
|
+
exists: false,
|
|
64
|
+
created: false,
|
|
65
|
+
pushed: false,
|
|
66
|
+
message: `Tag ${tag} does not exist`
|
|
67
|
+
};
|
|
68
|
+
createTag(tag, `Release ${version}`);
|
|
69
|
+
if (!options.push) return {
|
|
70
|
+
version,
|
|
71
|
+
tag,
|
|
72
|
+
exists: false,
|
|
73
|
+
created: true,
|
|
74
|
+
pushed: false,
|
|
75
|
+
message: `Created tag ${tag}`
|
|
76
|
+
};
|
|
77
|
+
pushTag(tag, remote);
|
|
78
|
+
return {
|
|
79
|
+
version,
|
|
80
|
+
tag,
|
|
81
|
+
exists: false,
|
|
82
|
+
created: true,
|
|
83
|
+
pushed: true,
|
|
84
|
+
message: `Created and pushed tag ${tag}`
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function checkTagCommand(options) {
|
|
88
|
+
try {
|
|
89
|
+
const result = checkTag(options);
|
|
90
|
+
console.log(result.message);
|
|
91
|
+
if (!result.exists && !options.create) process.exit(1);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
//#endregion
|
|
99
|
+
//#region src/utils/codequality.ts
|
|
100
|
+
function runBiome() {
|
|
101
|
+
try {
|
|
102
|
+
const result = execSync("pnpm exec biome ci --reporter=gitlab --colors=off", {
|
|
103
|
+
encoding: "utf-8",
|
|
104
|
+
stdio: [
|
|
105
|
+
"pipe",
|
|
106
|
+
"pipe",
|
|
107
|
+
"pipe"
|
|
108
|
+
]
|
|
109
|
+
});
|
|
110
|
+
return JSON.parse(result);
|
|
111
|
+
} catch (error) {
|
|
112
|
+
if (error instanceof Error && "stdout" in error) {
|
|
113
|
+
const stdout = error.stdout;
|
|
114
|
+
try {
|
|
115
|
+
return JSON.parse(stdout);
|
|
116
|
+
} catch {
|
|
117
|
+
return [];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function runPrettier() {
|
|
124
|
+
let files = [];
|
|
125
|
+
try {
|
|
126
|
+
files = parsePrettierOutput(execSync("pnpm exec prettier --check --list-different ./theme", {
|
|
127
|
+
encoding: "utf-8",
|
|
128
|
+
stdio: [
|
|
129
|
+
"pipe",
|
|
130
|
+
"pipe",
|
|
131
|
+
"pipe"
|
|
132
|
+
]
|
|
133
|
+
}));
|
|
134
|
+
} catch (error) {
|
|
135
|
+
if (error instanceof Error && "stdout" in error) {
|
|
136
|
+
const stdout = error.stdout;
|
|
137
|
+
files = parsePrettierOutput(stdout);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return files.map((file) => ({
|
|
141
|
+
description: "File not formatted according to Prettier rules",
|
|
142
|
+
check_name: "prettier",
|
|
143
|
+
fingerprint: Buffer.from(file).toString("base64"),
|
|
144
|
+
severity: "minor",
|
|
145
|
+
location: {
|
|
146
|
+
path: file,
|
|
147
|
+
lines: { begin: 1 }
|
|
148
|
+
}
|
|
149
|
+
}));
|
|
150
|
+
}
|
|
151
|
+
function parsePrettierOutput(output) {
|
|
152
|
+
return output.split("\n").map((line) => line.trim()).filter((line) => line && !line.startsWith(">") && !line.startsWith("$"));
|
|
153
|
+
}
|
|
154
|
+
function runThemeCheck(themePath) {
|
|
155
|
+
let themeCheckResult = [];
|
|
156
|
+
try {
|
|
157
|
+
const result = execSync(`shopify theme check --path ${themePath} --output json`, {
|
|
158
|
+
encoding: "utf-8",
|
|
159
|
+
stdio: [
|
|
160
|
+
"pipe",
|
|
161
|
+
"pipe",
|
|
162
|
+
"pipe"
|
|
163
|
+
]
|
|
164
|
+
});
|
|
165
|
+
themeCheckResult = JSON.parse(result);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (error instanceof Error && "stdout" in error) {
|
|
168
|
+
const stdout = error.stdout;
|
|
169
|
+
try {
|
|
170
|
+
themeCheckResult = JSON.parse(stdout);
|
|
171
|
+
} catch {
|
|
172
|
+
themeCheckResult = [];
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return themeCheckResult.flatMap((file) => file.offenses.map((offense) => ({
|
|
177
|
+
description: offense.message,
|
|
178
|
+
check_name: offense.check,
|
|
179
|
+
fingerprint: Buffer.from(`${file.path}:${offense.start_row}:${offense.check}`).toString("base64"),
|
|
180
|
+
severity: mapThemeSeverity(offense.severity),
|
|
181
|
+
location: {
|
|
182
|
+
path: file.path,
|
|
183
|
+
lines: { begin: offense.start_row || 1 }
|
|
184
|
+
}
|
|
185
|
+
})));
|
|
186
|
+
}
|
|
187
|
+
function mapThemeSeverity(severity) {
|
|
188
|
+
switch (severity) {
|
|
189
|
+
case 0: return "info";
|
|
190
|
+
case 1: return "minor";
|
|
191
|
+
case 2: return "major";
|
|
192
|
+
default: return "critical";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
function runTsc() {
|
|
196
|
+
let errors = [];
|
|
197
|
+
try {
|
|
198
|
+
execSync("pnpm exec tsc --noEmit", {
|
|
199
|
+
encoding: "utf-8",
|
|
200
|
+
stdio: [
|
|
201
|
+
"pipe",
|
|
202
|
+
"pipe",
|
|
203
|
+
"pipe"
|
|
204
|
+
]
|
|
205
|
+
});
|
|
206
|
+
} catch (error) {
|
|
207
|
+
if (error instanceof Error && "stdout" in error) {
|
|
208
|
+
const stdout = error.stdout;
|
|
209
|
+
errors = parseTscOutput(stdout);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return errors.map((err) => ({
|
|
213
|
+
description: `TS${err.code}: ${err.message}`,
|
|
214
|
+
check_name: `typescript/${err.code}`,
|
|
215
|
+
fingerprint: Buffer.from(`${err.file}:${err.line}:${err.column}:${err.code}`).toString("base64"),
|
|
216
|
+
severity: "major",
|
|
217
|
+
location: {
|
|
218
|
+
path: err.file,
|
|
219
|
+
lines: { begin: err.line }
|
|
220
|
+
}
|
|
221
|
+
}));
|
|
222
|
+
}
|
|
223
|
+
function parseTscOutput(output) {
|
|
224
|
+
const errors = [];
|
|
225
|
+
const lines = output.split("\n");
|
|
226
|
+
const errorRegex = /^(.+)\((\d+),(\d+)\):\s*error\s+TS(\d+):\s*(.+)$/;
|
|
227
|
+
for (const line of lines) {
|
|
228
|
+
const match = line.match(errorRegex);
|
|
229
|
+
if (match) errors.push({
|
|
230
|
+
file: match[1],
|
|
231
|
+
line: parseInt(match[2], 10),
|
|
232
|
+
column: parseInt(match[3], 10),
|
|
233
|
+
code: match[4],
|
|
234
|
+
message: match[5]
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return errors;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
//#endregion
|
|
241
|
+
//#region src/commands/codequality.ts
|
|
242
|
+
const ALL_CHECKS = [
|
|
243
|
+
"biome",
|
|
244
|
+
"prettier",
|
|
245
|
+
"theme-check",
|
|
246
|
+
"tsc"
|
|
247
|
+
];
|
|
248
|
+
function codequality(options) {
|
|
249
|
+
const { output, path, checks = ALL_CHECKS } = options;
|
|
250
|
+
const allIssues = [];
|
|
251
|
+
const enabledChecks = new Set(checks);
|
|
252
|
+
console.log(`Running code quality checks: ${checks.join(", ")}\n`);
|
|
253
|
+
if (enabledChecks.has("biome")) {
|
|
254
|
+
console.log("Running Biome...");
|
|
255
|
+
const biomeIssues = runBiome();
|
|
256
|
+
allIssues.push(...biomeIssues);
|
|
257
|
+
console.log(` Found ${biomeIssues.length} issues\n`);
|
|
258
|
+
}
|
|
259
|
+
if (enabledChecks.has("prettier")) {
|
|
260
|
+
console.log("Running Prettier...");
|
|
261
|
+
const prettierIssues = runPrettier();
|
|
262
|
+
allIssues.push(...prettierIssues);
|
|
263
|
+
console.log(` Found ${prettierIssues.length} issues\n`);
|
|
264
|
+
}
|
|
265
|
+
if (enabledChecks.has("theme-check")) {
|
|
266
|
+
console.log("Running Theme Check...");
|
|
267
|
+
const themeIssues = runThemeCheck(path);
|
|
268
|
+
allIssues.push(...themeIssues);
|
|
269
|
+
console.log(` Found ${themeIssues.length} issues\n`);
|
|
270
|
+
}
|
|
271
|
+
if (enabledChecks.has("tsc")) {
|
|
272
|
+
console.log("Running TypeScript...");
|
|
273
|
+
const tscIssues = runTsc();
|
|
274
|
+
allIssues.push(...tscIssues);
|
|
275
|
+
console.log(` Found ${tscIssues.length} issues\n`);
|
|
276
|
+
}
|
|
277
|
+
writeFileSync(output, JSON.stringify(allIssues, null, 2));
|
|
278
|
+
console.log(`Wrote ${allIssues.length} total issues to ${output}`);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
//#endregion
|
|
282
|
+
//#region src/commands/commit-msg.ts
|
|
283
|
+
function validateCommitMsg(options) {
|
|
284
|
+
const { file, prefix } = options;
|
|
285
|
+
const ticketRegex = /* @__PURE__ */ new RegExp(`^${prefix}-[0-9]+`);
|
|
286
|
+
let message;
|
|
287
|
+
try {
|
|
288
|
+
message = readFileSync(file, "utf-8").trim();
|
|
289
|
+
} catch {
|
|
290
|
+
return {
|
|
291
|
+
success: false,
|
|
292
|
+
message: `Error: Unable to read commit message file: ${file}`
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const match = message.match(ticketRegex);
|
|
296
|
+
if (!match) return {
|
|
297
|
+
success: false,
|
|
298
|
+
message: `Error: Commit message must start with a JIRA ticket ID (e.g., ${prefix}-69420).`
|
|
299
|
+
};
|
|
300
|
+
const ticket = match[0];
|
|
301
|
+
let branch;
|
|
302
|
+
try {
|
|
303
|
+
branch = execSync("git rev-parse --abbrev-ref HEAD", { encoding: "utf-8" }).trim();
|
|
304
|
+
} catch {
|
|
305
|
+
return {
|
|
306
|
+
success: true,
|
|
307
|
+
message: "Warning: Unable to determine current branch name; skipping branch-ticket check.",
|
|
308
|
+
ticket
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
if (!branch.match(/* @__PURE__ */ new RegExp(`${prefix}-[0-9]+`))) return {
|
|
312
|
+
success: true,
|
|
313
|
+
message: "OK",
|
|
314
|
+
ticket,
|
|
315
|
+
branch
|
|
316
|
+
};
|
|
317
|
+
if (!branch.includes(ticket)) return {
|
|
318
|
+
success: false,
|
|
319
|
+
message: `Error: Branch name \`${branch}\` does not contain ticket ID ${ticket}.`,
|
|
320
|
+
ticket,
|
|
321
|
+
branch
|
|
322
|
+
};
|
|
323
|
+
return {
|
|
324
|
+
success: true,
|
|
325
|
+
message: "OK",
|
|
326
|
+
ticket,
|
|
327
|
+
branch
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
function commitMsg(options) {
|
|
331
|
+
const result = validateCommitMsg(options);
|
|
332
|
+
if (!result.success) {
|
|
333
|
+
console.error(result.message);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
if (result.message.startsWith("Warning:")) console.warn(result.message);
|
|
337
|
+
process.exit(0);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/utils/shopify.ts
|
|
342
|
+
function listThemes() {
|
|
343
|
+
const result = execSync("shopify theme list --json", { encoding: "utf-8" });
|
|
344
|
+
return JSON.parse(result);
|
|
345
|
+
}
|
|
346
|
+
function findThemeByName(name) {
|
|
347
|
+
return listThemes().find((t) => t.name === name);
|
|
348
|
+
}
|
|
349
|
+
function findLiveTheme() {
|
|
350
|
+
return listThemes().find((t) => t.role === "live");
|
|
351
|
+
}
|
|
352
|
+
function duplicateTheme(themeId, name) {
|
|
353
|
+
const result = execSync(`shopify theme duplicate -j --theme "${themeId}" --name "${name}"`, { encoding: "utf-8" });
|
|
354
|
+
return JSON.parse(result);
|
|
355
|
+
}
|
|
356
|
+
function pushTheme(path, themeId, ignorePatterns = []) {
|
|
357
|
+
const result = execSync(`shopify theme push --path ${path} --theme "${themeId}" ${ignorePatterns.map((p) => `--ignore "${p}"`).join(" ")} --json`, { encoding: "utf-8" });
|
|
358
|
+
return JSON.parse(result);
|
|
359
|
+
}
|
|
360
|
+
function deleteTheme(themeId) {
|
|
361
|
+
execSync(`shopify theme delete --theme "${themeId}" --force`, { encoding: "utf-8" });
|
|
362
|
+
}
|
|
363
|
+
function writeDeployEnv(previewUrl, editorUrl, themeId, extras = {}) {
|
|
364
|
+
let content = `PREVIEW_URL=${previewUrl}\nEDITOR_URL=${editorUrl}\nTHEME_ID=${themeId}\n`;
|
|
365
|
+
for (const [key, value] of Object.entries(extras)) content += `${key}=${value}\n`;
|
|
366
|
+
writeFileSync("deploy.env", content);
|
|
367
|
+
}
|
|
368
|
+
const defaultIgnores = [
|
|
369
|
+
"templates/**/*.json",
|
|
370
|
+
"locales/**/*.json",
|
|
371
|
+
"config/settings_data.json",
|
|
372
|
+
"src/*",
|
|
373
|
+
"public/*"
|
|
374
|
+
];
|
|
375
|
+
|
|
376
|
+
//#endregion
|
|
377
|
+
//#region src/commands/deploy.ts
|
|
378
|
+
function deploy(options) {
|
|
379
|
+
const { themeId, path } = options;
|
|
380
|
+
const themeIdNum = parseInt(themeId, 10);
|
|
381
|
+
const { preview_url, editor_url } = pushTheme(path, themeIdNum, defaultIgnores).theme;
|
|
382
|
+
console.log("Release deployed successfully!");
|
|
383
|
+
console.log(`Preview URL: ${preview_url}`);
|
|
384
|
+
console.log(`Editor URL: ${editor_url}`);
|
|
385
|
+
writeDeployEnv(preview_url, editor_url, themeIdNum);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
//#endregion
|
|
389
|
+
//#region src/commands/deploy-review.ts
|
|
390
|
+
function deployReview(options) {
|
|
391
|
+
const { branch, path } = options;
|
|
392
|
+
const themeName = `AG Preview: ${branch}`;
|
|
393
|
+
console.log(`Setting up review app for branch: ${branch}`);
|
|
394
|
+
let reviewThemeId;
|
|
395
|
+
const existingTheme = findThemeByName(themeName);
|
|
396
|
+
if (!existingTheme) {
|
|
397
|
+
console.log("Creating new unpublished theme...");
|
|
398
|
+
const liveTheme = findLiveTheme();
|
|
399
|
+
if (!liveTheme) throw new Error("No live theme found to clone from");
|
|
400
|
+
console.log(`Cloning from published theme ID: ${liveTheme.id}`);
|
|
401
|
+
reviewThemeId = duplicateTheme(liveTheme.id, themeName).theme.id;
|
|
402
|
+
} else {
|
|
403
|
+
console.log(`Theme already exists with ID: ${existingTheme.id}`);
|
|
404
|
+
reviewThemeId = existingTheme.id;
|
|
405
|
+
}
|
|
406
|
+
console.log(`Deploying branch ${branch} to theme ID: ${reviewThemeId}`);
|
|
407
|
+
const { preview_url, editor_url } = pushTheme(path, reviewThemeId, defaultIgnores).theme;
|
|
408
|
+
console.log("Review app deployed successfully!");
|
|
409
|
+
console.log(`Preview URL: ${preview_url}`);
|
|
410
|
+
console.log(`Editor URL: ${editor_url}`);
|
|
411
|
+
writeDeployEnv(preview_url, editor_url, reviewThemeId, { EXISTING_THEME_ID: existingTheme?.id?.toString() ?? "" });
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
//#endregion
|
|
415
|
+
//#region src/commands/dev.ts
|
|
416
|
+
function dev(_options, command) {
|
|
417
|
+
const themeArgs = command.args;
|
|
418
|
+
console.log("Starting development servers...");
|
|
419
|
+
const vite = spawn("pnpm", ["exec", "vite"], {
|
|
420
|
+
stdio: "inherit",
|
|
421
|
+
shell: true
|
|
422
|
+
});
|
|
423
|
+
const shopify = spawn("pnpm", [
|
|
424
|
+
"exec",
|
|
425
|
+
"shopify",
|
|
426
|
+
"theme",
|
|
427
|
+
"dev",
|
|
428
|
+
...themeArgs
|
|
429
|
+
], {
|
|
430
|
+
stdio: "inherit",
|
|
431
|
+
shell: true
|
|
432
|
+
});
|
|
433
|
+
const cleanup = () => {
|
|
434
|
+
vite.kill();
|
|
435
|
+
shopify.kill();
|
|
436
|
+
process.exit(0);
|
|
437
|
+
};
|
|
438
|
+
process.on("SIGINT", cleanup);
|
|
439
|
+
process.on("SIGTERM", cleanup);
|
|
440
|
+
vite.on("close", (code) => {
|
|
441
|
+
if (code !== 0) {
|
|
442
|
+
console.error(`Vite exited with code ${code}`);
|
|
443
|
+
shopify.kill();
|
|
444
|
+
process.exit(code ?? 1);
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
shopify.on("close", (code) => {
|
|
448
|
+
if (code !== 0) {
|
|
449
|
+
console.error(`Shopify theme dev exited with code ${code}`);
|
|
450
|
+
vite.kill();
|
|
451
|
+
process.exit(code ?? 1);
|
|
452
|
+
}
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
//#endregion
|
|
457
|
+
//#region src/commands/release.ts
|
|
458
|
+
function release(options) {
|
|
459
|
+
const { name, path } = options;
|
|
460
|
+
const themeName = `AG Release: ${name}`;
|
|
461
|
+
const liveTheme = findLiveTheme();
|
|
462
|
+
if (!liveTheme) throw new Error("No live theme found to clone from");
|
|
463
|
+
console.log(`Cloning from published theme ID: ${liveTheme.id}`);
|
|
464
|
+
const releaseThemeId = duplicateTheme(liveTheme.id, themeName).theme.id;
|
|
465
|
+
const { preview_url, editor_url } = pushTheme(path, releaseThemeId, defaultIgnores).theme;
|
|
466
|
+
console.log("Release deployed successfully!");
|
|
467
|
+
console.log(`Preview URL: ${preview_url}`);
|
|
468
|
+
console.log(`Editor URL: ${editor_url}`);
|
|
469
|
+
writeDeployEnv(preview_url, editor_url, releaseThemeId, { PUBLISHED_THEME_ID: liveTheme.id.toString() });
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
//#endregion
|
|
473
|
+
//#region src/commands/stop-review.ts
|
|
474
|
+
function stopReview(options) {
|
|
475
|
+
const { branch } = options;
|
|
476
|
+
const themeName = `AG Preview: ${branch}`;
|
|
477
|
+
console.log(`Cleaning up review app for branch: ${branch}`);
|
|
478
|
+
const theme = findThemeByName(themeName);
|
|
479
|
+
if (theme) {
|
|
480
|
+
console.log(`Deleting theme: ${themeName} (ID: ${theme.id})`);
|
|
481
|
+
deleteTheme(theme.id);
|
|
482
|
+
console.log("Review app cleaned up successfully!");
|
|
483
|
+
} else console.log(`No theme found with name: ${themeName}`);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
//#endregion
|
|
487
|
+
//#region src/commands/validate-env.ts
|
|
488
|
+
const commonVars = [{
|
|
489
|
+
name: "SHOPIFY_CLI_THEME_TOKEN",
|
|
490
|
+
required: true,
|
|
491
|
+
description: "Shopify CLI authentication token"
|
|
492
|
+
}, {
|
|
493
|
+
name: "SHOPIFY_FLAG_STORE",
|
|
494
|
+
required: true,
|
|
495
|
+
description: "Shopify store URL"
|
|
496
|
+
}];
|
|
497
|
+
const contextVars = {
|
|
498
|
+
deploy: [{
|
|
499
|
+
name: "SHOPIFY_THEME_ID",
|
|
500
|
+
required: true,
|
|
501
|
+
description: "Target theme ID for deployment"
|
|
502
|
+
}],
|
|
503
|
+
review: [{
|
|
504
|
+
name: "CI_COMMIT_REF_NAME",
|
|
505
|
+
required: true,
|
|
506
|
+
description: "Git branch name for review app"
|
|
507
|
+
}],
|
|
508
|
+
release: [{
|
|
509
|
+
name: "CI_COMMIT_TAG",
|
|
510
|
+
required: true,
|
|
511
|
+
description: "Git tag for release"
|
|
512
|
+
}],
|
|
513
|
+
codequality: []
|
|
514
|
+
};
|
|
515
|
+
function getVarsToCheck(options) {
|
|
516
|
+
const { context, vars } = options;
|
|
517
|
+
if (vars && vars.length > 0) return vars.map((name) => ({
|
|
518
|
+
name,
|
|
519
|
+
required: true,
|
|
520
|
+
description: "User-specified variable"
|
|
521
|
+
}));
|
|
522
|
+
const varsToCheck = [...commonVars];
|
|
523
|
+
const ctx = context ?? "all";
|
|
524
|
+
if (ctx !== "all" && contextVars[ctx]) varsToCheck.push(...contextVars[ctx]);
|
|
525
|
+
else if (ctx === "all") for (const ctxVars of Object.values(contextVars)) varsToCheck.push(...ctxVars);
|
|
526
|
+
return varsToCheck;
|
|
527
|
+
}
|
|
528
|
+
function checkEnvVars(varsToCheck, env = process.env) {
|
|
529
|
+
const missing = [];
|
|
530
|
+
const present = [];
|
|
531
|
+
for (const envVar of varsToCheck) {
|
|
532
|
+
const value = env[envVar.name];
|
|
533
|
+
if (!value && envVar.required) missing.push(envVar);
|
|
534
|
+
else present.push({
|
|
535
|
+
name: envVar.name,
|
|
536
|
+
value,
|
|
537
|
+
redacted: envVar.name.includes("TOKEN")
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
return {
|
|
541
|
+
success: missing.length === 0,
|
|
542
|
+
present,
|
|
543
|
+
missing
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
function validateEnv(options) {
|
|
547
|
+
const { context, vars } = options;
|
|
548
|
+
const ctx = context ?? "all";
|
|
549
|
+
if (vars && vars.length > 0) console.log("Validating specified environment variables:\n");
|
|
550
|
+
else console.log(`Validating environment for context: ${ctx}\n`);
|
|
551
|
+
const result = checkEnvVars(getVarsToCheck(options));
|
|
552
|
+
if (result.present.length > 0) {
|
|
553
|
+
console.log("✓ Present:");
|
|
554
|
+
for (const item of result.present) {
|
|
555
|
+
const displayValue = item.redacted ? "[REDACTED]" : item.value;
|
|
556
|
+
console.log(` ${item.name}=${displayValue}`);
|
|
557
|
+
}
|
|
558
|
+
console.log();
|
|
559
|
+
}
|
|
560
|
+
if (result.missing.length > 0) {
|
|
561
|
+
console.log("✗ Missing:");
|
|
562
|
+
for (const envVar of result.missing) console.log(` ${envVar.name} - ${envVar.description}`);
|
|
563
|
+
console.log();
|
|
564
|
+
process.exit(1);
|
|
565
|
+
}
|
|
566
|
+
console.log("All required environment variables are set.");
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
//#endregion
|
|
570
|
+
//#region src/cli.ts
|
|
571
|
+
program.name("agci").description("Acidgreen CI/CD CLI tools").version("1.0.0");
|
|
572
|
+
program.command("codequality").description("Run code quality checks and combine output").option("-o, --output <file>", "Output file", "codequality.json").option("-p, --path <path>", "Theme path", "theme").option("-c, --checks <checks...>", "Checks to run (biome, prettier, theme-check, tsc)").action(codequality);
|
|
573
|
+
program.command("deploy:review").description("Deploy a review app for the current branch").requiredOption("-b, --branch <branch>", "Branch name (CI_COMMIT_REF_NAME)").option("-p, --path <path>", "Theme path", "theme").action(deployReview);
|
|
574
|
+
program.command("stop:review").description("Stop and delete a review app").requiredOption("-b, --branch <branch>", "Branch name (CI_COMMIT_REF_NAME)").action(stopReview);
|
|
575
|
+
program.command("deploy").description("Deploy to a specific theme").requiredOption("-t, --theme-id <id>", "Theme ID (SHOPIFY_THEME_ID)").option("-p, --path <path>", "Theme path", "theme").action(deploy);
|
|
576
|
+
program.command("release").description("Create a release by cloning live theme and pushing").requiredOption("-n, --name <name>", "Release name/tag").option("-p, --path <path>", "Theme path", "theme").action(release);
|
|
577
|
+
program.command("validate-env").description("Validate required environment variables are set").option("-c, --context <context>", "Context to validate (deploy, review, release, codequality, all)").option("-v, --vars <vars...>", "Specific environment variables to check").action(validateEnv);
|
|
578
|
+
program.command("dev").description("Start Vite and Shopify theme dev servers concurrently").allowUnknownOption().allowExcessArguments().action(dev);
|
|
579
|
+
program.command("commit-msg").description("Validate commit message format (for git hooks)").requiredOption("-f, --file <file>", "Path to commit message file").option("-p, --prefix <prefix>", "JIRA project prefix", "AIR").action(commitMsg);
|
|
580
|
+
program.command("check-tag").description("Check if git tag exists for package.json version").option("-p, --package-path <path>", "Path to package.json", "package.json").option("--prefix <prefix>", "Tag prefix", "v").option("-c, --create", "Create the tag if it does not exist").option("--push", "Push the tag to remote (requires --create)").option("-r, --remote <remote>", "Remote to push to", "origin").action(checkTagCommand);
|
|
581
|
+
program.parse();
|
|
582
|
+
|
|
583
|
+
//#endregion
|
|
584
|
+
export { };
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@acidgreen-au/ag-cicd-cli",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Acidgreen CI/CD CLI tools",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://gitlab.com/acidgreen/internal/agcicd.git"
|
|
8
|
+
},
|
|
9
|
+
"type": "module",
|
|
10
|
+
"bin": {
|
|
11
|
+
"agci": "./dist/cli.mjs"
|
|
12
|
+
},
|
|
13
|
+
"main": "./dist/cli.mjs",
|
|
14
|
+
"types": "./dist/cli.d.mts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": "./dist/cli.mjs",
|
|
17
|
+
"./package.json": "./package.json"
|
|
18
|
+
},
|
|
19
|
+
"files": [
|
|
20
|
+
"dist"
|
|
21
|
+
],
|
|
22
|
+
"dependencies": {
|
|
23
|
+
"commander": "13.1.0"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.3.10",
|
|
27
|
+
"@types/node": "24.10.4",
|
|
28
|
+
"husky": "^9.1.7",
|
|
29
|
+
"tsdown": "^0.18.2",
|
|
30
|
+
"typescript": "^5.9.3",
|
|
31
|
+
"vitest": "^4.0.16"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=24.0.0"
|
|
35
|
+
},
|
|
36
|
+
"volta": {
|
|
37
|
+
"node": "24.12.0"
|
|
38
|
+
},
|
|
39
|
+
"module": "./dist/cli.mjs",
|
|
40
|
+
"scripts": {
|
|
41
|
+
"build": "tsdown",
|
|
42
|
+
"cli": "node src/cli.ts",
|
|
43
|
+
"test": "vitest",
|
|
44
|
+
"test:run": "vitest run",
|
|
45
|
+
"biome:unsafe": "biome check --write --unsafe",
|
|
46
|
+
"biome:check": "biome check",
|
|
47
|
+
"biome:ci": "biome ci",
|
|
48
|
+
"typecheck": "tsc",
|
|
49
|
+
"precommit": "pnpm run biome:ci && pnpm run typecheck"
|
|
50
|
+
}
|
|
51
|
+
}
|