@acidgreen-au/ag-cicd-cli 0.0.3 → 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +37 -0
- package/dist/cli.mjs +289 -94
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -30,3 +30,40 @@ ag <command> --help
|
|
|
30
30
|
|
|
31
31
|
- Node.js >= 24.0.0
|
|
32
32
|
- Shopify CLI (for theme commands)
|
|
33
|
+
|
|
34
|
+
## Contributing
|
|
35
|
+
|
|
36
|
+
This project uses [changesets](https://github.com/changesets/changesets) for version management and changelog generation.
|
|
37
|
+
|
|
38
|
+
### Adding a changeset
|
|
39
|
+
|
|
40
|
+
When making changes that should be released, add a changeset:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pnpm changeset
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Select the change type:
|
|
47
|
+
- **patch** - Bug fixes, documentation updates
|
|
48
|
+
- **minor** - New features (backwards compatible)
|
|
49
|
+
- **major** - Breaking changes
|
|
50
|
+
|
|
51
|
+
Write a summary describing your change. This will appear in the CHANGELOG.
|
|
52
|
+
|
|
53
|
+
### Release workflow
|
|
54
|
+
|
|
55
|
+
1. Make changes and add changeset(s) with your PR
|
|
56
|
+
2. Merge PR to main
|
|
57
|
+
3. CI runs `pnpm version` to consume changesets and bump version
|
|
58
|
+
4. CI commits version bump and creates git tag
|
|
59
|
+
5. CI publishes to npm on tag
|
|
60
|
+
|
|
61
|
+
### Manual release (maintainers)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Consume changesets, update version and CHANGELOG
|
|
65
|
+
pnpm version
|
|
66
|
+
|
|
67
|
+
# Build and publish to npm
|
|
68
|
+
pnpm release
|
|
69
|
+
```
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { existsSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { program } from "commander";
|
|
@@ -8,43 +8,17 @@ import prompts from "prompts";
|
|
|
8
8
|
import { parse, stringify } from "smol-toml";
|
|
9
9
|
import concurrently from "concurrently";
|
|
10
10
|
|
|
11
|
-
//#region src/
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
Used in CI/CD to automatically tag releases when package.json version
|
|
20
|
-
is bumped.
|
|
21
|
-
|
|
22
|
-
Exit Codes:
|
|
23
|
-
0 Tag exists, or was successfully created
|
|
24
|
-
1 Tag does not exist (when --create is not specified)
|
|
25
|
-
|
|
26
|
-
Examples:
|
|
27
|
-
$ ag check-tag
|
|
28
|
-
$ ag check-tag --create
|
|
29
|
-
$ ag check-tag --create --push
|
|
30
|
-
$ ag check-tag --create --push --remote origin
|
|
31
|
-
$ ag check-tag --prefix release-
|
|
32
|
-
|
|
33
|
-
GitLab CI/CD Authentication:
|
|
34
|
-
To push tags from GitLab CI/CD, you need to authenticate with the remote.
|
|
35
|
-
|
|
36
|
-
Create a token with write_repository scope in Settings > Access Tokens,
|
|
37
|
-
then add it as a masked CI/CD variable (e.g., GITLAB_TOKEN).
|
|
38
|
-
|
|
39
|
-
$ ag check-tag --create --push \\
|
|
40
|
-
--remote "https://oauth2:\${GITLAB_TOKEN}@\${CI_SERVER_HOST}/\${CI_PROJECT_PATH}.git"`;
|
|
41
|
-
function register$9(program$1) {
|
|
42
|
-
program$1.command("check-tag").description("Check if git tag exists for package.json version").addHelpText("after", helpText$9).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 doesn't exist").option("--push", "Push the tag to remote (requires --create)").option("-r, --remote <remote>", "Remote to push to", "origin").action(checkTagCommand);
|
|
43
|
-
}
|
|
44
|
-
function getVersionFromPackage(packagePath) {
|
|
45
|
-
const content = readFileSync(packagePath, "utf-8");
|
|
46
|
-
return JSON.parse(content).version;
|
|
11
|
+
//#region src/utils/git.ts
|
|
12
|
+
/**
|
|
13
|
+
* Configure git user for commits
|
|
14
|
+
*/
|
|
15
|
+
function configureGit(name, email) {
|
|
16
|
+
execSync(`git config user.name "${name}"`, { stdio: "pipe" });
|
|
17
|
+
execSync(`git config user.email "${email}"`, { stdio: "pipe" });
|
|
47
18
|
}
|
|
19
|
+
/**
|
|
20
|
+
* Sync local tags with remote (prune deleted, fetch new)
|
|
21
|
+
*/
|
|
48
22
|
function syncTagsFromRemote(remote) {
|
|
49
23
|
execSync(`git fetch "${remote}" --prune --prune-tags --force`, {
|
|
50
24
|
encoding: "utf-8",
|
|
@@ -55,6 +29,9 @@ function syncTagsFromRemote(remote) {
|
|
|
55
29
|
]
|
|
56
30
|
});
|
|
57
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* Check if a git tag exists
|
|
34
|
+
*/
|
|
58
35
|
function tagExists(tag) {
|
|
59
36
|
try {
|
|
60
37
|
execSync(`git rev-parse --verify "refs/tags/${tag}"`, {
|
|
@@ -70,6 +47,9 @@ function tagExists(tag) {
|
|
|
70
47
|
return false;
|
|
71
48
|
}
|
|
72
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* Create an annotated git tag
|
|
52
|
+
*/
|
|
73
53
|
function createTag(tag, message) {
|
|
74
54
|
execSync(`git tag -a "${tag}" -m "${message}"`, {
|
|
75
55
|
encoding: "utf-8",
|
|
@@ -80,6 +60,9 @@ function createTag(tag, message) {
|
|
|
80
60
|
]
|
|
81
61
|
});
|
|
82
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* Push a specific tag to remote
|
|
65
|
+
*/
|
|
83
66
|
function pushTag(tag, remote) {
|
|
84
67
|
execSync(`git push "${remote}" "${tag}"`, {
|
|
85
68
|
encoding: "utf-8",
|
|
@@ -90,15 +73,103 @@ function pushTag(tag, remote) {
|
|
|
90
73
|
]
|
|
91
74
|
});
|
|
92
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Check if there are staged or unstaged changes
|
|
78
|
+
*/
|
|
79
|
+
function hasChanges() {
|
|
80
|
+
try {
|
|
81
|
+
const status = execSync("git status --porcelain", {
|
|
82
|
+
encoding: "utf-8",
|
|
83
|
+
stdio: [
|
|
84
|
+
"pipe",
|
|
85
|
+
"pipe",
|
|
86
|
+
"pipe"
|
|
87
|
+
]
|
|
88
|
+
});
|
|
89
|
+
const hasUncommittedChanges = status.trim().length > 0;
|
|
90
|
+
if (hasUncommittedChanges) {
|
|
91
|
+
console.log("Changes detected:");
|
|
92
|
+
console.log(status);
|
|
93
|
+
}
|
|
94
|
+
return hasUncommittedChanges;
|
|
95
|
+
} catch {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Stage all changes and commit with message
|
|
101
|
+
* @param message - Commit message
|
|
102
|
+
* @param skipHooks - Skip pre-commit and commit-msg hooks (for CI releases)
|
|
103
|
+
* Returns false if no changes to commit
|
|
104
|
+
*/
|
|
105
|
+
function commitChanges(message, skipHooks = false) {
|
|
106
|
+
if (!hasChanges()) return false;
|
|
107
|
+
execSync("git add -A", { stdio: "inherit" });
|
|
108
|
+
const noVerify = skipHooks ? " --no-verify" : "";
|
|
109
|
+
try {
|
|
110
|
+
execSync(`git commit -m "${message}"${noVerify}`, { stdio: "inherit" });
|
|
111
|
+
return true;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
console.error("Git commit failed:", error instanceof Error ? error.message : String(error));
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Push to remote with optional branch refspec (for detached HEAD in CI)
|
|
119
|
+
* Pushes commit and tags separately so [skip ci] doesn't affect tag pipeline
|
|
120
|
+
*/
|
|
121
|
+
function pushToRemote(remote, branch) {
|
|
122
|
+
execSync(`git push "${remote}" ${branch ? `HEAD:refs/heads/${branch}` : "HEAD"}`, { stdio: "inherit" });
|
|
123
|
+
execSync(`git push "${remote}" --tags`, { stdio: "inherit" });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/commands/check-tag.ts
|
|
128
|
+
const helpText$10 = `
|
|
129
|
+
Details:
|
|
130
|
+
Reads the version from package.json and checks if a corresponding git tag
|
|
131
|
+
exists. Optionally creates and pushes the tag if it doesn't exist.
|
|
132
|
+
|
|
133
|
+
When --remote is specified, local tags are synced with remote before checking.
|
|
134
|
+
|
|
135
|
+
Used in CI/CD to automatically tag releases when package.json version
|
|
136
|
+
is bumped.
|
|
137
|
+
|
|
138
|
+
Exit Codes:
|
|
139
|
+
0 Tag exists, or was successfully created
|
|
140
|
+
1 Tag does not exist (when --create is not specified)
|
|
141
|
+
|
|
142
|
+
Examples:
|
|
143
|
+
$ ag check-tag
|
|
144
|
+
$ ag check-tag --create
|
|
145
|
+
$ ag check-tag --create --push
|
|
146
|
+
$ ag check-tag --create --push --remote origin
|
|
147
|
+
$ ag check-tag --prefix release-
|
|
148
|
+
|
|
149
|
+
GitLab CI/CD Authentication:
|
|
150
|
+
To push tags from GitLab CI/CD, you need to authenticate with the remote.
|
|
151
|
+
|
|
152
|
+
Create a token with write_repository scope in Settings > Access Tokens,
|
|
153
|
+
then add it as a masked CI/CD variable (e.g., GITLAB_TOKEN).
|
|
154
|
+
|
|
155
|
+
$ ag check-tag --create --push \\
|
|
156
|
+
--remote "https://oauth2:\${GITLAB_TOKEN}@\${CI_SERVER_HOST}/\${CI_PROJECT_PATH}.git"`;
|
|
157
|
+
function register$10(program$1) {
|
|
158
|
+
program$1.command("check-tag").description("Check if git tag exists for package.json version").addHelpText("after", helpText$10).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 doesn't exist").option("--push", "Push the tag to remote (requires --create)").option("-r, --remote <remote>", "Remote to push to", "origin").action(checkTagCommand);
|
|
159
|
+
}
|
|
160
|
+
function getVersionFromPackage(packagePath) {
|
|
161
|
+
const content = readFileSync(packagePath, "utf-8");
|
|
162
|
+
return JSON.parse(content).version;
|
|
163
|
+
}
|
|
93
164
|
function checkTag(options) {
|
|
94
165
|
const packagePath = options.packagePath ?? "package.json";
|
|
95
166
|
const prefix = options.prefix ?? "v";
|
|
96
167
|
const remote = options.remote ?? "origin";
|
|
97
168
|
if (options.remote) syncTagsFromRemote(remote);
|
|
98
|
-
const version = getVersionFromPackage(packagePath);
|
|
99
|
-
const tag = `${prefix}${version}`;
|
|
169
|
+
const version$1 = getVersionFromPackage(packagePath);
|
|
170
|
+
const tag = `${prefix}${version$1}`;
|
|
100
171
|
if (tagExists(tag)) return {
|
|
101
|
-
version,
|
|
172
|
+
version: version$1,
|
|
102
173
|
tag,
|
|
103
174
|
exists: true,
|
|
104
175
|
created: false,
|
|
@@ -106,16 +177,16 @@ function checkTag(options) {
|
|
|
106
177
|
message: `Tag ${tag} already exists`
|
|
107
178
|
};
|
|
108
179
|
if (!options.create) return {
|
|
109
|
-
version,
|
|
180
|
+
version: version$1,
|
|
110
181
|
tag,
|
|
111
182
|
exists: false,
|
|
112
183
|
created: false,
|
|
113
184
|
pushed: false,
|
|
114
185
|
message: `Tag ${tag} does not exist`
|
|
115
186
|
};
|
|
116
|
-
createTag(tag, `Release ${version}`);
|
|
187
|
+
createTag(tag, `Release ${version$1}`);
|
|
117
188
|
if (!options.push) return {
|
|
118
|
-
version,
|
|
189
|
+
version: version$1,
|
|
119
190
|
tag,
|
|
120
191
|
exists: false,
|
|
121
192
|
created: true,
|
|
@@ -124,7 +195,7 @@ function checkTag(options) {
|
|
|
124
195
|
};
|
|
125
196
|
pushTag(tag, remote);
|
|
126
197
|
return {
|
|
127
|
-
version,
|
|
198
|
+
version: version$1,
|
|
128
199
|
tag,
|
|
129
200
|
exists: false,
|
|
130
201
|
created: true,
|
|
@@ -293,7 +364,7 @@ const ALL_CHECKS = [
|
|
|
293
364
|
"theme-check",
|
|
294
365
|
"tsc"
|
|
295
366
|
];
|
|
296
|
-
const helpText$
|
|
367
|
+
const helpText$9 = `
|
|
297
368
|
Checks:
|
|
298
369
|
biome Runs Biome linter for TypeScript/JavaScript
|
|
299
370
|
prettier Checks file formatting with Prettier
|
|
@@ -308,8 +379,8 @@ Examples:
|
|
|
308
379
|
$ ag codequality
|
|
309
380
|
$ ag codequality --checks biome tsc
|
|
310
381
|
$ ag codequality --output gl-code-quality.json --path ./theme`;
|
|
311
|
-
function register$
|
|
312
|
-
program$1.command("codequality").description("Run code quality checks and output GitLab-compatible JSON").addHelpText("after", helpText$
|
|
382
|
+
function register$9(program$1) {
|
|
383
|
+
program$1.command("codequality").description("Run code quality checks and output GitLab-compatible JSON").addHelpText("after", helpText$9).option("-o, --output <file>", "Output JSON file path", "codequality.json").option("-p, --path <path>", "Theme directory path", "theme").option("-c, --checks <checks...>", "Specific checks to run (biome, prettier, theme-check, tsc)").action(codequality);
|
|
313
384
|
}
|
|
314
385
|
function codequality(options) {
|
|
315
386
|
const { output, path, checks = ALL_CHECKS } = options;
|
|
@@ -346,7 +417,7 @@ function codequality(options) {
|
|
|
346
417
|
|
|
347
418
|
//#endregion
|
|
348
419
|
//#region src/commands/commit-msg.ts
|
|
349
|
-
const helpText$
|
|
420
|
+
const helpText$8 = `
|
|
350
421
|
Details:
|
|
351
422
|
Validates that commit messages follow the required format:
|
|
352
423
|
<PREFIX>-<NUMBER> <message>
|
|
@@ -365,8 +436,8 @@ Exit Codes:
|
|
|
365
436
|
Examples:
|
|
366
437
|
$ ag commit-msg --file .git/COMMIT_EDITMSG
|
|
367
438
|
$ ag commit-msg -f .git/COMMIT_EDITMSG --prefix PROJ`;
|
|
368
|
-
function register$
|
|
369
|
-
program$1.command("commit-msg").description("Validate commit message format for git hooks").addHelpText("after", helpText$
|
|
439
|
+
function register$8(program$1) {
|
|
440
|
+
program$1.command("commit-msg").description("Validate commit message format for git hooks").addHelpText("after", helpText$8).requiredOption("-f, --file <file>", "Path to commit message file").option("-p, --prefix <prefix>", "JIRA project prefix", "AIR").action(commitMsg);
|
|
370
441
|
}
|
|
371
442
|
function validateCommitMsg(options) {
|
|
372
443
|
const { file, prefix } = options;
|
|
@@ -427,7 +498,7 @@ function commitMsg(options) {
|
|
|
427
498
|
|
|
428
499
|
//#endregion
|
|
429
500
|
//#region src/commands/config-shopify.ts
|
|
430
|
-
const helpText$
|
|
501
|
+
const helpText$7 = `
|
|
431
502
|
Details:
|
|
432
503
|
Creates or updates a shopify.theme.toml configuration file.
|
|
433
504
|
|
|
@@ -451,8 +522,8 @@ const DEFAULT_IGNORES = [
|
|
|
451
522
|
"public/*"
|
|
452
523
|
];
|
|
453
524
|
const DEFAULT_PATH = "theme";
|
|
454
|
-
function register$
|
|
455
|
-
program$1.command("config:shopify").description("Create or update shopify.theme.toml configuration").addHelpText("after", helpText$
|
|
525
|
+
function register$7(program$1) {
|
|
526
|
+
program$1.command("config:shopify").description("Create or update shopify.theme.toml configuration").addHelpText("after", helpText$7).argument("[name]", "Environment name (default: 'default')").option("-f, --force", "Overwrite existing environment").action(configShopify);
|
|
456
527
|
}
|
|
457
528
|
function loadConfig(configPath) {
|
|
458
529
|
if (!existsSync(configPath)) return { environments: {} };
|
|
@@ -461,20 +532,42 @@ function loadConfig(configPath) {
|
|
|
461
532
|
function saveConfig(configPath, config) {
|
|
462
533
|
writeFileSync(configPath, stringify(config));
|
|
463
534
|
}
|
|
535
|
+
function validateConfig(config, envName, options) {
|
|
536
|
+
if (config.environments[envName] && !options.force) return {
|
|
537
|
+
success: false,
|
|
538
|
+
message: `Environment '${envName}' already exists. Use --force to overwrite.`,
|
|
539
|
+
envName
|
|
540
|
+
};
|
|
541
|
+
if (envName !== "default" && !config.environments.default) return {
|
|
542
|
+
success: false,
|
|
543
|
+
message: "Default environment must be created first. Run 'ag config:shopify' without arguments.",
|
|
544
|
+
envName
|
|
545
|
+
};
|
|
546
|
+
return null;
|
|
547
|
+
}
|
|
548
|
+
function buildEnvConfig(config, response) {
|
|
549
|
+
const defaultEnv = config.environments.default;
|
|
550
|
+
const path = defaultEnv?.path ?? DEFAULT_PATH;
|
|
551
|
+
const ignore = defaultEnv?.ignore ?? DEFAULT_IGNORES;
|
|
552
|
+
const envConfig = {
|
|
553
|
+
store: response.store,
|
|
554
|
+
path,
|
|
555
|
+
ignore
|
|
556
|
+
};
|
|
557
|
+
if (response.themeId) envConfig.theme = response.themeId;
|
|
558
|
+
return envConfig;
|
|
559
|
+
}
|
|
464
560
|
async function configShopify(name, options) {
|
|
465
561
|
const configPath = "shopify.theme.toml";
|
|
466
562
|
const envName = name ?? "default";
|
|
467
563
|
const config = loadConfig(configPath);
|
|
468
564
|
if (!config.environments) config.environments = {};
|
|
469
|
-
|
|
470
|
-
|
|
565
|
+
const validationError = validateConfig(config, envName, options);
|
|
566
|
+
if (validationError) {
|
|
567
|
+
console.error(`Error: ${validationError.message}`);
|
|
471
568
|
process.exit(1);
|
|
472
569
|
}
|
|
473
|
-
|
|
474
|
-
console.error("Error: Default environment must be created first. Run 'ag config:shopify' without arguments.");
|
|
475
|
-
process.exit(1);
|
|
476
|
-
}
|
|
477
|
-
const response = await prompts([{
|
|
570
|
+
const envConfig = buildEnvConfig(config, await prompts([{
|
|
478
571
|
type: "text",
|
|
479
572
|
name: "store",
|
|
480
573
|
message: "Store name (e.g., my-store.myshopify.com)",
|
|
@@ -486,16 +579,7 @@ async function configShopify(name, options) {
|
|
|
486
579
|
}], { onCancel: () => {
|
|
487
580
|
console.log("\nCancelled.");
|
|
488
581
|
process.exit(0);
|
|
489
|
-
} });
|
|
490
|
-
const defaultEnv = config.environments.default;
|
|
491
|
-
const path = defaultEnv?.path ?? DEFAULT_PATH;
|
|
492
|
-
const ignore = defaultEnv?.ignore ?? DEFAULT_IGNORES;
|
|
493
|
-
const envConfig = {
|
|
494
|
-
store: response.store,
|
|
495
|
-
path,
|
|
496
|
-
ignore
|
|
497
|
-
};
|
|
498
|
-
if (response.themeId) envConfig.theme = response.themeId;
|
|
582
|
+
} }));
|
|
499
583
|
config.environments[envName] = envConfig;
|
|
500
584
|
saveConfig(configPath, config);
|
|
501
585
|
console.log(`\n${existsSync(configPath) ? "Updated" : "Created"} ${configPath} with '${envName}' environment`);
|
|
@@ -539,7 +623,7 @@ const defaultIgnores = [
|
|
|
539
623
|
|
|
540
624
|
//#endregion
|
|
541
625
|
//#region src/commands/deploy.ts
|
|
542
|
-
const helpText$
|
|
626
|
+
const helpText$6 = `
|
|
543
627
|
Details:
|
|
544
628
|
Pushes theme files to a specific Shopify theme ID. Use this for deploying
|
|
545
629
|
to staging or production themes where you know the target theme ID.
|
|
@@ -556,8 +640,8 @@ Environment:
|
|
|
556
640
|
Examples:
|
|
557
641
|
$ ag deploy --theme-id 123456789
|
|
558
642
|
$ ag deploy -t 123456789 --path ./theme`;
|
|
559
|
-
function register$
|
|
560
|
-
program$1.command("deploy").description("Deploy theme to an existing Shopify theme by ID").addHelpText("after", helpText$
|
|
643
|
+
function register$6(program$1) {
|
|
644
|
+
program$1.command("deploy").description("Deploy theme to an existing Shopify theme by ID").addHelpText("after", helpText$6).requiredOption("-t, --theme-id <id>", "Target Shopify theme ID").option("-p, --path <path>", "Theme directory path", "theme").action(deploy);
|
|
561
645
|
}
|
|
562
646
|
function deploy(options) {
|
|
563
647
|
const { themeId, path } = options;
|
|
@@ -571,7 +655,7 @@ function deploy(options) {
|
|
|
571
655
|
|
|
572
656
|
//#endregion
|
|
573
657
|
//#region src/commands/deploy-review.ts
|
|
574
|
-
const helpText$
|
|
658
|
+
const helpText$5 = `
|
|
575
659
|
Details:
|
|
576
660
|
Creates or updates an unpublished theme for reviewing branch changes.
|
|
577
661
|
Theme is named "AG Preview: <branch>" for easy identification.
|
|
@@ -593,8 +677,8 @@ Environment:
|
|
|
593
677
|
Examples:
|
|
594
678
|
$ ag deploy:review --branch feature/new-header
|
|
595
679
|
$ ag deploy:review -b $CI_COMMIT_REF_NAME`;
|
|
596
|
-
function register$
|
|
597
|
-
program$1.command("deploy:review").description("Deploy a review app theme for the current branch").addHelpText("after", helpText$
|
|
680
|
+
function register$5(program$1) {
|
|
681
|
+
program$1.command("deploy:review").description("Deploy a review app theme for the current branch").addHelpText("after", helpText$5).requiredOption("-b, --branch <branch>", "Git branch name").option("-p, --path <path>", "Theme directory path", "theme").action(deployReview);
|
|
598
682
|
}
|
|
599
683
|
function deployReview(options) {
|
|
600
684
|
const { branch, path } = options;
|
|
@@ -622,7 +706,7 @@ function deployReview(options) {
|
|
|
622
706
|
|
|
623
707
|
//#endregion
|
|
624
708
|
//#region src/commands/dev.ts
|
|
625
|
-
const helpText$
|
|
709
|
+
const helpText$4 = `
|
|
626
710
|
Details:
|
|
627
711
|
Starts both Vite (for frontend asset compilation) and Shopify theme dev
|
|
628
712
|
servers in parallel. All additional arguments are passed to shopify theme dev.
|
|
@@ -638,8 +722,8 @@ Examples:
|
|
|
638
722
|
$ ag dev
|
|
639
723
|
$ ag dev -- --store my-store
|
|
640
724
|
$ ag dev -- --theme 123456789`;
|
|
641
|
-
function register$
|
|
642
|
-
program$1.command("dev").description("Start Vite and Shopify theme dev servers concurrently").addHelpText("after", helpText$
|
|
725
|
+
function register$4(program$1) {
|
|
726
|
+
program$1.command("dev").description("Start Vite and Shopify theme dev servers concurrently").addHelpText("after", helpText$4).allowUnknownOption().allowExcessArguments().action(dev);
|
|
643
727
|
}
|
|
644
728
|
async function dev(_options, command) {
|
|
645
729
|
const { result } = concurrently([{
|
|
@@ -663,7 +747,7 @@ async function dev(_options, command) {
|
|
|
663
747
|
|
|
664
748
|
//#endregion
|
|
665
749
|
//#region src/commands/release.ts
|
|
666
|
-
const helpText$
|
|
750
|
+
const helpText$3 = `
|
|
667
751
|
Details:
|
|
668
752
|
Creates a new theme for production releases. Clones the current live theme
|
|
669
753
|
to preserve any theme editor customizations, then pushes your code changes.
|
|
@@ -684,8 +768,8 @@ Environment:
|
|
|
684
768
|
Examples:
|
|
685
769
|
$ ag release --name v1.2.3
|
|
686
770
|
$ ag release -n $CI_COMMIT_TAG`;
|
|
687
|
-
function register$
|
|
688
|
-
program$1.command("release").description("Create a release theme by cloning live and pushing changes").addHelpText("after", helpText$
|
|
771
|
+
function register$3(program$1) {
|
|
772
|
+
program$1.command("release").description("Create a release theme by cloning live and pushing changes").addHelpText("after", helpText$3).requiredOption("-n, --name <name>", "Release name (usually git tag)").option("-p, --path <path>", "Theme directory path", "theme").action(release);
|
|
689
773
|
}
|
|
690
774
|
function release(options) {
|
|
691
775
|
const { name, path } = options;
|
|
@@ -703,7 +787,7 @@ function release(options) {
|
|
|
703
787
|
|
|
704
788
|
//#endregion
|
|
705
789
|
//#region src/commands/stop-review.ts
|
|
706
|
-
const helpText$
|
|
790
|
+
const helpText$2 = `
|
|
707
791
|
Details:
|
|
708
792
|
Finds and deletes the unpublished theme named "AG Preview: <branch>".
|
|
709
793
|
Typically called when a merge request is closed or merged.
|
|
@@ -718,8 +802,8 @@ Environment:
|
|
|
718
802
|
Examples:
|
|
719
803
|
$ ag stop:review --branch feature/new-header
|
|
720
804
|
$ ag stop:review -b $CI_COMMIT_REF_NAME`;
|
|
721
|
-
function register$
|
|
722
|
-
program$1.command("stop:review").description("Delete the review app theme for a branch").addHelpText("after", helpText$
|
|
805
|
+
function register$2(program$1) {
|
|
806
|
+
program$1.command("stop:review").description("Delete the review app theme for a branch").addHelpText("after", helpText$2).requiredOption("-b, --branch <branch>", "Git branch name").action(stopReview);
|
|
723
807
|
}
|
|
724
808
|
function stopReview(options) {
|
|
725
809
|
const { branch } = options;
|
|
@@ -735,7 +819,7 @@ function stopReview(options) {
|
|
|
735
819
|
|
|
736
820
|
//#endregion
|
|
737
821
|
//#region src/commands/validate-env.ts
|
|
738
|
-
const helpText = `
|
|
822
|
+
const helpText$1 = `
|
|
739
823
|
Contexts:
|
|
740
824
|
all Check all variables for all contexts (default)
|
|
741
825
|
deploy Check variables for theme deployment
|
|
@@ -753,8 +837,8 @@ Examples:
|
|
|
753
837
|
$ ag validate-env
|
|
754
838
|
$ ag validate-env --context deploy
|
|
755
839
|
$ ag validate-env --vars SHOPIFY_FLAG_STORE CUSTOM_VAR`;
|
|
756
|
-
function register(program$1) {
|
|
757
|
-
program$1.command("validate-env").description("Validate required environment variables are set").addHelpText("after", helpText).option("-c, --context <context>", "Validation context (deploy, review, release, codequality, all)").option("-v, --vars <vars...>", "Specific variables to check").action(validateEnv);
|
|
840
|
+
function register$1(program$1) {
|
|
841
|
+
program$1.command("validate-env").description("Validate required environment variables are set").addHelpText("after", helpText$1).option("-c, --context <context>", "Validation context (deploy, review, release, codequality, all)").option("-v, --vars <vars...>", "Specific variables to check").action(validateEnv);
|
|
758
842
|
}
|
|
759
843
|
const commonVars = [{
|
|
760
844
|
name: "SHOPIFY_CLI_THEME_TOKEN",
|
|
@@ -837,22 +921,133 @@ function validateEnv(options) {
|
|
|
837
921
|
console.log("All required environment variables are set.");
|
|
838
922
|
}
|
|
839
923
|
|
|
924
|
+
//#endregion
|
|
925
|
+
//#region src/commands/version.ts
|
|
926
|
+
const helpText = `
|
|
927
|
+
Details:
|
|
928
|
+
Automates the release versioning workflow using changesets.
|
|
929
|
+
|
|
930
|
+
1. Checks for pending changesets in .changeset/
|
|
931
|
+
2. If changesets exist:
|
|
932
|
+
- Runs "changeset version" to bump version and update CHANGELOG
|
|
933
|
+
- Commits the changes
|
|
934
|
+
- Creates a git tag
|
|
935
|
+
- Optionally pushes to remote
|
|
936
|
+
3. If no changesets:
|
|
937
|
+
- Falls back to check-tag behavior (creates tag if version is untagged)
|
|
938
|
+
|
|
939
|
+
Used in CI/CD to automate releases when changesets are merged.
|
|
940
|
+
|
|
941
|
+
Environment:
|
|
942
|
+
GITLAB_TOKEN GitLab API token for pushing (when using HTTPS remote)
|
|
943
|
+
|
|
944
|
+
Examples:
|
|
945
|
+
$ ag version
|
|
946
|
+
$ ag version --push
|
|
947
|
+
$ ag version --push --remote origin
|
|
948
|
+
$ ag version --push --remote "https://oauth2:\${GITLAB_TOKEN}@gitlab.com/org/repo.git"
|
|
949
|
+
$ ag version --push --branch main # Required for detached HEAD (CI environments)`;
|
|
950
|
+
function register(program$1) {
|
|
951
|
+
program$1.command("version").description("Version packages using changesets and create git tag").addHelpText("after", helpText).option("--push", "Push commits and tags to remote").option("-r, --remote <remote>", "Git remote to push to", "origin").option("-b, --branch <branch>", "Branch to push to (required for detached HEAD in CI)").option("--git-user-name <name>", "Git user name for commits", "CI").option("--git-user-email <email>", "Git user email for commits", "ci@localhost").action(versionCommand);
|
|
952
|
+
}
|
|
953
|
+
function hasChangesets() {
|
|
954
|
+
try {
|
|
955
|
+
return readdirSync(".changeset").filter((f) => f.endsWith(".md") && f !== "README.md").length > 0;
|
|
956
|
+
} catch {
|
|
957
|
+
return false;
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function getPackageVersion() {
|
|
961
|
+
const content = readFileSync("package.json", "utf-8");
|
|
962
|
+
return JSON.parse(content).version;
|
|
963
|
+
}
|
|
964
|
+
function runChangesetVersion() {
|
|
965
|
+
execSync("pnpm changeset version", { stdio: "inherit" });
|
|
966
|
+
}
|
|
967
|
+
function version(options) {
|
|
968
|
+
const remote = options.remote ?? "origin";
|
|
969
|
+
if (options.push && options.remote) {
|
|
970
|
+
console.log("Syncing tags from remote...");
|
|
971
|
+
syncTagsFromRemote(remote);
|
|
972
|
+
}
|
|
973
|
+
if (!hasChangesets()) {
|
|
974
|
+
console.log("No changesets found, checking if tag exists...");
|
|
975
|
+
const checkTagResult = checkTag({
|
|
976
|
+
create: true,
|
|
977
|
+
push: options.push,
|
|
978
|
+
remote: options.push ? remote : void 0
|
|
979
|
+
});
|
|
980
|
+
return {
|
|
981
|
+
hadChangesets: false,
|
|
982
|
+
version: checkTagResult.version,
|
|
983
|
+
tag: checkTagResult.tag,
|
|
984
|
+
committed: false,
|
|
985
|
+
tagged: checkTagResult.created,
|
|
986
|
+
pushed: checkTagResult.pushed,
|
|
987
|
+
message: checkTagResult.message
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
console.log("Found changesets, versioning packages...");
|
|
991
|
+
configureGit(options.gitUserName ?? "CI", options.gitUserEmail ?? "ci@localhost");
|
|
992
|
+
runChangesetVersion();
|
|
993
|
+
const newVersion = getPackageVersion();
|
|
994
|
+
const tag = `v${newVersion}`;
|
|
995
|
+
console.log(`Version bumped to ${newVersion}`);
|
|
996
|
+
const committed = commitChanges(`chore: release ${tag}`, true);
|
|
997
|
+
if (committed) console.log(`Committed version changes`);
|
|
998
|
+
else console.log("No changes to commit");
|
|
999
|
+
const tagAlreadyExists = tagExists(tag);
|
|
1000
|
+
if (!committed && !tagAlreadyExists) throw new Error(`No changes to commit but tag ${tag} doesn't exist. This may indicate changesets were consumed in a previous failed run. Please check if package.json version matches the expected release.`);
|
|
1001
|
+
let tagCreated = false;
|
|
1002
|
+
if (!tagAlreadyExists) {
|
|
1003
|
+
createTag(tag, `Release ${newVersion}`);
|
|
1004
|
+
tagCreated = true;
|
|
1005
|
+
console.log(`Created tag ${tag}`);
|
|
1006
|
+
} else console.log(`Tag ${tag} already exists, skipping`);
|
|
1007
|
+
let pushed = false;
|
|
1008
|
+
if (options.push) {
|
|
1009
|
+
console.log(`Pushing to ${remote}...`);
|
|
1010
|
+
pushToRemote(remote, options.branch);
|
|
1011
|
+
pushed = true;
|
|
1012
|
+
console.log("Pushed successfully");
|
|
1013
|
+
}
|
|
1014
|
+
return {
|
|
1015
|
+
hadChangesets: true,
|
|
1016
|
+
version: newVersion,
|
|
1017
|
+
tag,
|
|
1018
|
+
committed,
|
|
1019
|
+
tagged: tagCreated || tagAlreadyExists,
|
|
1020
|
+
pushed,
|
|
1021
|
+
message: `Released ${tag}`
|
|
1022
|
+
};
|
|
1023
|
+
}
|
|
1024
|
+
function versionCommand(options) {
|
|
1025
|
+
try {
|
|
1026
|
+
const result = version(options);
|
|
1027
|
+
console.log(result.message);
|
|
1028
|
+
} catch (error) {
|
|
1029
|
+
console.error("Error:", error instanceof Error ? error.message : String(error));
|
|
1030
|
+
process.exit(1);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
840
1034
|
//#endregion
|
|
841
1035
|
//#region src/cli.ts
|
|
842
1036
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
843
1037
|
const pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
|
|
844
1038
|
const binName = Object.keys(pkg.bin)[0];
|
|
845
1039
|
program.name(binName).description("Acidgreen CI/CD CLI tools for Shopify theme development").version(pkg.version);
|
|
846
|
-
register$
|
|
1040
|
+
register$9(program);
|
|
1041
|
+
register$6(program);
|
|
847
1042
|
register$5(program);
|
|
848
|
-
register$4(program);
|
|
849
|
-
register$1(program);
|
|
850
1043
|
register$2(program);
|
|
851
|
-
register(program);
|
|
852
1044
|
register$3(program);
|
|
1045
|
+
register$1(program);
|
|
1046
|
+
register$4(program);
|
|
1047
|
+
register$8(program);
|
|
1048
|
+
register$10(program);
|
|
853
1049
|
register$7(program);
|
|
854
|
-
register
|
|
855
|
-
register$6(program);
|
|
1050
|
+
register(program);
|
|
856
1051
|
program.parse();
|
|
857
1052
|
|
|
858
1053
|
//#endregion
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@acidgreen-au/ag-cicd-cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
4
4
|
"description": "Acidgreen CI/CD CLI tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@biomejs/biome": "^2.3.10",
|
|
28
|
+
"@changesets/cli": "^2.29.8",
|
|
28
29
|
"@types/node": "24.10.4",
|
|
29
30
|
"@types/prompts": "^2.4.9",
|
|
30
31
|
"husky": "^9.1.7",
|
|
@@ -48,6 +49,7 @@
|
|
|
48
49
|
"biome:check": "biome check",
|
|
49
50
|
"biome:ci": "biome ci",
|
|
50
51
|
"typecheck": "tsc",
|
|
51
|
-
"precommit": "pnpm run biome:ci && pnpm run typecheck"
|
|
52
|
+
"precommit": "pnpm run biome:ci && pnpm run typecheck",
|
|
53
|
+
"changeset": "changeset"
|
|
52
54
|
}
|
|
53
55
|
}
|