@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.
Files changed (3) hide show
  1. package/README.md +37 -0
  2. package/dist/cli.mjs +289 -94
  3. 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/commands/check-tag.ts
12
- const helpText$9 = `
13
- Details:
14
- Reads the version from package.json and checks if a corresponding git tag
15
- exists. Optionally creates and pushes the tag if it doesn't exist.
16
-
17
- When --remote is specified, local tags are synced with remote before checking.
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$8 = `
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$8(program$1) {
312
- program$1.command("codequality").description("Run code quality checks and output GitLab-compatible JSON").addHelpText("after", helpText$8).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);
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$7 = `
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$7(program$1) {
369
- program$1.command("commit-msg").description("Validate commit message format for git hooks").addHelpText("after", helpText$7).requiredOption("-f, --file <file>", "Path to commit message file").option("-p, --prefix <prefix>", "JIRA project prefix", "AIR").action(commitMsg);
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$6 = `
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$6(program$1) {
455
- program$1.command("config:shopify").description("Create or update shopify.theme.toml configuration").addHelpText("after", helpText$6).argument("[name]", "Environment name (default: 'default')").option("-f, --force", "Overwrite existing environment").action(configShopify);
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
- if (config.environments[envName] && !options.force) {
470
- console.error(`Error: Environment '${envName}' already exists. Use --force to overwrite.`);
565
+ const validationError = validateConfig(config, envName, options);
566
+ if (validationError) {
567
+ console.error(`Error: ${validationError.message}`);
471
568
  process.exit(1);
472
569
  }
473
- if (envName !== "default" && !config.environments.default) {
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$5 = `
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$5(program$1) {
560
- program$1.command("deploy").description("Deploy theme to an existing Shopify theme by ID").addHelpText("after", helpText$5).requiredOption("-t, --theme-id <id>", "Target Shopify theme ID").option("-p, --path <path>", "Theme directory path", "theme").action(deploy);
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$4 = `
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$4(program$1) {
597
- program$1.command("deploy:review").description("Deploy a review app theme for the current branch").addHelpText("after", helpText$4).requiredOption("-b, --branch <branch>", "Git branch name").option("-p, --path <path>", "Theme directory path", "theme").action(deployReview);
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$3 = `
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$3(program$1) {
642
- program$1.command("dev").description("Start Vite and Shopify theme dev servers concurrently").addHelpText("after", helpText$3).allowUnknownOption().allowExcessArguments().action(dev);
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$2 = `
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$2(program$1) {
688
- program$1.command("release").description("Create a release theme by cloning live and pushing changes").addHelpText("after", helpText$2).requiredOption("-n, --name <name>", "Release name (usually git tag)").option("-p, --path <path>", "Theme directory path", "theme").action(release);
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$1 = `
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$1(program$1) {
722
- program$1.command("stop:review").description("Delete the review app theme for a branch").addHelpText("after", helpText$1).requiredOption("-b, --branch <branch>", "Git branch name").action(stopReview);
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$8(program);
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$9(program);
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",
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
  }