gitxplain 0.1.9 → 0.2.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.
@@ -16,6 +16,8 @@ jobs:
16
16
  with:
17
17
  node-version: '20'
18
18
  cache: npm
19
+ registry-url: 'https://registry.npmjs.org'
20
+ always-auth: true
19
21
  - name: Install dependencies
20
22
  run: npm ci
21
23
  - name: Lint
@@ -2,12 +2,13 @@ name: Release
2
2
  on:
3
3
  push:
4
4
  tags:
5
- - '*.*.*'
5
+ - 'v*'
6
+ permissions:
7
+ contents: write
6
8
  jobs:
7
- publish:
9
+ release:
10
+ if: startsWith(github.ref_name, 'v')
8
11
  runs-on: ubuntu-latest
9
- permissions:
10
- contents: read
11
12
  steps:
12
13
  - name: Checkout
13
14
  uses: actions/checkout@v4
@@ -16,12 +17,98 @@ jobs:
16
17
  with:
17
18
  node-version: '20'
18
19
  cache: npm
19
- registry-url: 'https://registry.npmjs.org/'
20
+ registry-url: 'https://registry.npmjs.org'
21
+ always-auth: true
20
22
  - name: Install dependencies
21
23
  run: npm ci
24
+ - name: Derive release metadata
25
+ id: meta
26
+ run: |
27
+ VERSION="${GITHUB_REF_NAME#v}"
28
+ echo "version=${VERSION}" >> "${GITHUB_OUTPUT}"
29
+ echo "deb_path=dist/gitxplain_${VERSION}_all.deb" >> "${GITHUB_OUTPUT}"
22
30
  - name: Test
23
31
  run: npm test
32
+ - name: Build Debian package
33
+ run: ./scripts/build-deb.sh
34
+ - name: Verify npm token
35
+ env:
36
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
37
+ run: |
38
+ if [ -z "${NODE_AUTH_TOKEN}" ]; then
39
+ echo "NPM_TOKEN is not configured for this repository."
40
+ echo "Add it in GitHub: Settings -> Secrets and variables -> Actions -> New repository secret."
41
+ exit 1
42
+ fi
43
+ printf "//registry.npmjs.org/:_authToken=%s\n" "${NODE_AUTH_TOKEN}" > "${HOME}/.npmrc"
44
+ npm whoami
24
45
  - name: Publish to npm
25
46
  env:
26
47
  NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
27
48
  run: npm publish
49
+ - name: Compute npm tarball SHA-256
50
+ id: npm
51
+ run: |
52
+ VERSION="${{ steps.meta.outputs.version }}"
53
+ TARBALL_URL="https://registry.npmjs.org/gitxplain/-/gitxplain-${VERSION}.tgz"
54
+ curl -fsSL "${TARBALL_URL}" -o "gitxplain-${VERSION}.tgz"
55
+ SHA256="$(sha256sum "gitxplain-${VERSION}.tgz" | awk '{print $1}')"
56
+ echo "tarball_url=${TARBALL_URL}" >> "${GITHUB_OUTPUT}"
57
+ echo "sha256=${SHA256}" >> "${GITHUB_OUTPUT}"
58
+ - name: Checkout Homebrew tap
59
+ uses: actions/checkout@v4
60
+ with:
61
+ repository: guruswarupa/homebrew-tap
62
+ token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
63
+ path: homebrew-tap
64
+ - name: Update Homebrew formula
65
+ run: |
66
+ VERSION="${{ steps.meta.outputs.version }}"
67
+ SHA256="${{ steps.npm.outputs.sha256 }}"
68
+ FORMULA_PATH="homebrew-tap/Formula/gitxplain.rb"
69
+ mkdir -p "$(dirname "${FORMULA_PATH}")"
70
+ cat > "${FORMULA_PATH}" <<EOF
71
+ class Gitxplain < Formula
72
+ desc "AI-powered Git commit explainer CLI"
73
+ homepage "https://github.com/guruswarupa/gitxplain"
74
+ url "https://registry.npmjs.org/gitxplain/-/gitxplain-${VERSION}.tgz"
75
+ sha256 "${SHA256}"
76
+ license "MIT"
77
+
78
+ depends_on "node"
79
+
80
+ def install
81
+ libexec.install Dir["package/*"]
82
+ bin.install_symlink libexec/"cli/index.js" => "gitxplain"
83
+ bin.install_symlink libexec/"cli/index.js" => "gitxplore"
84
+ end
85
+
86
+ test do
87
+ assert_match "gitxplain", shell_output("#{bin}/gitxplain --help")
88
+ end
89
+ end
90
+ EOF
91
+ - name: Commit and push Homebrew tap changes
92
+ working-directory: homebrew-tap
93
+ run: |
94
+ git config user.name "github-actions[bot]"
95
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
96
+ git add Formula/gitxplain.rb
97
+ if git diff --cached --quiet; then
98
+ echo "No Homebrew formula changes to commit."
99
+ exit 0
100
+ fi
101
+ git commit -m "gitxplain ${GITHUB_REF_NAME}"
102
+ git push
103
+ - name: Create GitHub release and upload Debian package
104
+ uses: softprops/action-gh-release@v2
105
+ with:
106
+ files: ${{ steps.meta.outputs.deb_path }}
107
+ - name: Print AUR update instructions
108
+ run: |
109
+ VERSION="${{ steps.meta.outputs.version }}"
110
+ SHA256="${{ steps.npm.outputs.sha256 }}"
111
+ echo "Manual AUR update steps:"
112
+ echo "1. Update packaging/aur/PKGBUILD with pkgver=${VERSION} and sha256sums=('${SHA256}')."
113
+ echo "2. Run: makepkg --printsrcinfo > .SRCINFO"
114
+ echo "3. Commit PKGBUILD and .SRCINFO to the AUR git repository and push."
package/README.md CHANGED
@@ -46,6 +46,43 @@ Supported providers:
46
46
  - A Git repository in your current working directory
47
47
  - An API key for your chosen provider, or a local Ollama instance
48
48
 
49
+ ## Installation
50
+
51
+ Install from npm:
52
+
53
+ ```bash
54
+ npm install -g gitxplain
55
+ ```
56
+
57
+ Install from bun:
58
+
59
+ ```bash
60
+ bun install -g gitxplain
61
+ ```
62
+
63
+ Install with Homebrew:
64
+
65
+ ```bash
66
+ brew tap guruswarupa/homebrew-tap
67
+ brew install gitxplain
68
+ ```
69
+
70
+ Install from the AUR:
71
+
72
+ ```bash
73
+ yay -S gitxplain
74
+ ```
75
+
76
+ ```bash
77
+ paru -S gitxplain
78
+ ```
79
+
80
+ Install from a Debian package downloaded from GitHub Releases:
81
+
82
+ ```bash
83
+ sudo apt install ./gitxplain_<version>_all.deb
84
+ ```
85
+
49
86
  Optional advanced environment variables:
50
87
 
51
88
  - `LLM_PROVIDER` default: `openai`
package/cli/index.js CHANGED
@@ -689,8 +689,16 @@ async function runPipelineCommand(cwd) {
689
689
  return 0;
690
690
  }
691
691
 
692
- const { writtenFiles, notes } = writePipelineFiles(cwd, analysis, selection);
693
- console.log(`\nCreated workflow files: ${writtenFiles.join(", ")}`);
692
+ const { writtenFiles, updatedFiles, unchangedFiles, notes } = writePipelineFiles(cwd, analysis, selection);
693
+
694
+ if (updatedFiles.length === 0 && unchangedFiles.length > 0) {
695
+ console.log(`\nWorkflow files already matched the current template: ${unchangedFiles.join(", ")}`);
696
+ } else if (updatedFiles.length > 0 && unchangedFiles.length === 0) {
697
+ console.log(`\nUpdated workflow files: ${updatedFiles.join(", ")}`);
698
+ } else {
699
+ console.log(`\nUpdated workflow files: ${updatedFiles.join(", ")}`);
700
+ console.log(`Unchanged workflow files: ${unchangedFiles.join(", ")}`);
701
+ }
694
702
 
695
703
  if (notes.length > 0) {
696
704
  console.log(`\n${notes.join("\n")}`);
@@ -765,6 +773,10 @@ export async function main(argv = process.argv) {
765
773
  return 0;
766
774
  }
767
775
 
776
+ if (parsed.pipelineCommand) {
777
+ return runPipelineCommand(cwd);
778
+ }
779
+
768
780
  if (
769
781
  parsed.addCommand ||
770
782
  parsed.removeCommand ||
@@ -33,6 +33,10 @@ function unique(values) {
33
33
  return [...new Set(values)];
34
34
  }
35
35
 
36
+ function formatVersionTag(version) {
37
+ return `v${version}`;
38
+ }
39
+
36
40
  function extractVersions(line) {
37
41
  return unique(line.match(VERSION_PATTERN) ?? []);
38
42
  }
@@ -319,23 +323,26 @@ export function selectReleaseWindows(sourceCommits, releaseCommits = []) {
319
323
  export function selectReleaseTags(sourceCommits, existingTagNames = [], existingTagTargets = []) {
320
324
  const windows = selectLatestWindowsPerVersion(buildReleaseWindows(sourceCommits));
321
325
  const taggedVersions = extractTaggedVersions(existingTagNames);
322
- const targetByVersion = new Map(
326
+ const existingTagByVersion = new Map(
323
327
  existingTagTargets
324
328
  .map((tag) => {
325
329
  const version = tag.tagName?.match(TAG_VERSION_PATTERN)?.[1] ?? null;
326
- return version ? [version, tag.targetSha] : null;
330
+ return version ? [version, tag] : null;
327
331
  })
328
332
  .filter(Boolean)
329
333
  );
330
334
  const tags = windows
331
335
  .map((window) => {
332
336
  const targetCommit = window.commits.at(-1) ?? null;
333
- const existingTargetSha = targetByVersion.get(window.version) ?? null;
337
+ const existingTag = existingTagByVersion.get(window.version) ?? null;
338
+ const existingTargetSha = existingTag?.targetSha ?? null;
334
339
  const windowCommitShas = new Set(window.commits.map((commit) => commit.sha));
335
340
 
336
341
  return {
337
342
  ...window,
338
- tagName: window.version,
343
+ version: window.version,
344
+ tagName: existingTag?.tagName ?? formatVersionTag(window.version),
345
+ existingTagName: existingTag?.tagName ?? null,
339
346
  existingTargetSha,
340
347
  needsMove:
341
348
  existingTargetSha != null &&
@@ -394,7 +401,7 @@ export function selectReleaseTagsFromReleaseCommits(releaseCommits, existingTagN
394
401
  .filter((entry) => !taggedVersions.has(entry.version))
395
402
  .map(({ commit, version }) => ({
396
403
  version,
397
- tagName: version,
404
+ tagName: formatVersionTag(version),
398
405
  startRef: commit.shortSha,
399
406
  endRef: commit.shortSha,
400
407
  targetSha: commit.sha,
@@ -827,10 +834,10 @@ export function executeReleaseTagPlan(plan, cwd) {
827
834
  try {
828
835
  for (const tag of plan.tags) {
829
836
  if (tag.needsMove) {
830
- gitDeleteTag(tag.tagName, cwd);
837
+ gitDeleteTag(tag.existingTagName ?? tag.tagName, cwd);
831
838
  }
832
839
 
833
- gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.tagName}`, cwd);
840
+ gitCreateAnnotatedTag(tag.tagName, tag.targetSha, `release ${tag.version ?? tag.tagName.replace(/^v/, "")}`, cwd);
834
841
  createdTags.push(tag.tagName);
835
842
  }
836
843
  } catch (error) {
@@ -5,6 +5,7 @@ import {
5
5
  readdirSync,
6
6
  writeFileSync
7
7
  } from "node:fs";
8
+ import { execFileSync } from "node:child_process";
8
9
  import path from "node:path";
9
10
 
10
11
  const WORKFLOW_DIR = ".github/workflows";
@@ -64,6 +65,56 @@ function detectNodeVersion(cwd, packageJson) {
64
65
  };
65
66
  }
66
67
 
68
+ function detectGitHubRepository(cwd) {
69
+ try {
70
+ const remote = execFileSync("git", ["config", "--get", "remote.origin.url"], {
71
+ cwd,
72
+ encoding: "utf8",
73
+ stdio: ["ignore", "pipe", "ignore"]
74
+ }).trim();
75
+
76
+ const match =
77
+ remote.match(/github\.com[:/]([^/]+)\/([^/]+?)(?:\.git)?$/i) ??
78
+ remote.match(/^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/i);
79
+
80
+ if (!match) {
81
+ return null;
82
+ }
83
+
84
+ return {
85
+ owner: match[1],
86
+ repo: match[2],
87
+ slug: `${match[1]}/${match[2]}`
88
+ };
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+
94
+ function detectNodePackaging(cwd, packageJson) {
95
+ const packageName = (packageJson?.name ?? "package").replace(/^@[^/]+\//, "");
96
+ const githubRepository = detectGitHubRepository(cwd);
97
+ const homebrewFormulaPath = `packaging/homebrew-tap/Formula/${packageName}.rb`;
98
+
99
+ return {
100
+ deb: fileExists(cwd, "scripts/build-deb.sh"),
101
+ aur: fileExists(cwd, "packaging/aur/PKGBUILD"),
102
+ homebrew: fileExists(cwd, homebrewFormulaPath),
103
+ homebrewFormulaPath,
104
+ homebrewTapRepo: githubRepository ? `${githubRepository.owner}/homebrew-tap` : null,
105
+ githubRepository
106
+ };
107
+ }
108
+
109
+ function toHomebrewClassName(packageName) {
110
+ return packageName
111
+ .replace(/^@[^/]+\//, "")
112
+ .split(/[^a-zA-Z0-9]+/)
113
+ .filter(Boolean)
114
+ .map((part) => part[0].toUpperCase() + part.slice(1))
115
+ .join("");
116
+ }
117
+
67
118
  function detectNodeProject(cwd) {
68
119
  if (!fileExists(cwd, "package.json")) {
69
120
  return null;
@@ -75,12 +126,14 @@ function detectNodeProject(cwd) {
75
126
  const packageManager = detectPackageManager(cwd);
76
127
  const releaseSupported = packageJson.private !== true && typeof packageJson.name === "string";
77
128
  const packSupported = packageJson.private !== true || Boolean(packageJson.bin);
129
+ const packaging = detectNodePackaging(cwd, packageJson);
78
130
 
79
131
  return {
80
132
  type: "node",
81
133
  displayName: packageJson.name || path.basename(cwd),
82
134
  packageManager,
83
135
  packageJson,
136
+ packaging,
84
137
  nodeVersion,
85
138
  commands: {
86
139
  install:
@@ -277,7 +330,9 @@ function buildNodeSetupStep(nodeVersion, packageManager = "npm") {
277
330
  " uses: actions/setup-node@v4",
278
331
  " with:",
279
332
  " node-version-file: .nvmrc",
280
- ` cache: ${packageManager}`
333
+ ` cache: ${packageManager}`,
334
+ " registry-url: 'https://registry.npmjs.org'",
335
+ " always-auth: true"
281
336
  ].join("\n");
282
337
  }
283
338
 
@@ -286,7 +341,9 @@ function buildNodeSetupStep(nodeVersion, packageManager = "npm") {
286
341
  " uses: actions/setup-node@v4",
287
342
  " with:",
288
343
  ` node-version: '${nodeVersion.value}'`,
289
- ` cache: ${packageManager}`
344
+ ` cache: ${packageManager}`,
345
+ " registry-url: 'https://registry.npmjs.org'",
346
+ " always-auth: true"
290
347
  ].join("\n");
291
348
  }
292
349
 
@@ -440,8 +497,10 @@ export function inspectRepositoryForPipeline(cwd) {
440
497
  id: "ci-release",
441
498
  label: `CI plus ${primary.release.type} release automation`,
442
499
  description:
443
- primary.type === "node"
444
- ? "Publishes to npm when you push a version tag like v1.2.3."
500
+ primary.type === "node" && (primary.packaging?.deb || primary.packaging?.homebrew || primary.packaging?.aur)
501
+ ? "Publishes to npm on version tags, builds Debian packages, updates Homebrew when configured, and prints AUR update instructions."
502
+ : primary.type === "node"
503
+ ? "Publishes to npm when you push a version tag like v1.2.3."
445
504
  : primary.type === "python"
446
505
  ? "Builds and publishes to PyPI when you push a version tag like v1.2.3."
447
506
  : "Publishes to crates.io when you push a version tag like v1.2.3.",
@@ -569,16 +628,154 @@ export function buildReleaseWorkflow(context) {
569
628
  }
570
629
 
571
630
  if (context.type === "node") {
631
+ const packaging = context.packaging ?? {};
632
+ const githubRepository = packaging.githubRepository?.slug ?? null;
633
+ const homebrewTapRepo = packaging.homebrewTapRepo ?? null;
634
+ const packageName = context.packageJson?.name ?? context.displayName ?? "package";
635
+ const formulaClassName = toHomebrewClassName(packageName);
636
+ const binEntries = Object.entries(context.packageJson?.bin ?? {});
637
+ const executablePath = (binEntries[0]?.[1] ?? "cli/index.js").replace(/^\.\//, "");
638
+ const executableNames = binEntries.length > 0 ? binEntries.map(([name]) => name) : [packageName];
639
+
640
+ if (packaging.deb || packaging.homebrew || packaging.aur) {
641
+ return [
642
+ "name: Release",
643
+ "",
644
+ "on:",
645
+ " push:",
646
+ " tags:",
647
+ " - 'v*'",
648
+ "",
649
+ "permissions:",
650
+ " contents: write",
651
+ "",
652
+ "jobs:",
653
+ " release:",
654
+ " if: startsWith(github.ref_name, 'v')",
655
+ " runs-on: ubuntu-latest",
656
+ "",
657
+ " steps:",
658
+ " - name: Checkout",
659
+ " uses: actions/checkout@v4",
660
+ buildNodeReleaseSetup(context),
661
+ " - name: Derive release metadata",
662
+ " id: meta",
663
+ " run: |",
664
+ " VERSION=\"${GITHUB_REF_NAME#v}\"",
665
+ " echo \"version=${VERSION}\" >> \"${GITHUB_OUTPUT}\"",
666
+ packaging.deb ? ` echo "deb_path=dist/${packageName}_\${VERSION}_all.deb" >> "\${GITHUB_OUTPUT}"` : "",
667
+ context.commands.test ? formatRunStep("Test", context.commands.test) : "",
668
+ context.commands.build ? formatRunStep("Build", context.commands.build) : "",
669
+ packaging.deb ? formatRunStep("Build Debian package", "./scripts/build-deb.sh") : "",
670
+ " - name: Verify npm token",
671
+ " env:",
672
+ " NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
673
+ " run: |",
674
+ " if [ -z \"${NODE_AUTH_TOKEN}\" ]; then",
675
+ " echo \"NPM_TOKEN is not configured for this repository.\"",
676
+ " echo \"Add it in GitHub: Settings -> Secrets and variables -> Actions -> New repository secret.\"",
677
+ " exit 1",
678
+ " fi",
679
+ " printf \"//registry.npmjs.org/:_authToken=%s\\n\" \"${NODE_AUTH_TOKEN}\" > \"${HOME}/.npmrc\"",
680
+ " npm whoami",
681
+ " - name: Publish to npm",
682
+ " env:",
683
+ " NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
684
+ " run: npm publish",
685
+ " - name: Compute npm tarball SHA-256",
686
+ " id: npm",
687
+ " run: |",
688
+ " VERSION=\"${{ steps.meta.outputs.version }}\"",
689
+ ` TARBALL_URL="https://registry.npmjs.org/${packageName}/-/${packageName}-\${VERSION}.tgz"`,
690
+ ` curl -fsSL "\${TARBALL_URL}" -o "${packageName}-\${VERSION}.tgz"`,
691
+ ` SHA256="$(sha256sum "${packageName}-\${VERSION}.tgz" | awk '{print $1}')"`,
692
+ " echo \"tarball_url=${TARBALL_URL}\" >> \"${GITHUB_OUTPUT}\"",
693
+ " echo \"sha256=${SHA256}\" >> \"${GITHUB_OUTPUT}\"",
694
+ packaging.homebrew && homebrewTapRepo
695
+ ? [
696
+ " - name: Checkout Homebrew tap",
697
+ " uses: actions/checkout@v4",
698
+ " with:",
699
+ ` repository: ${homebrewTapRepo}`,
700
+ " token: ${{ secrets.HOMEBREW_TAP_TOKEN }}",
701
+ " path: homebrew-tap",
702
+ " - name: Update Homebrew formula",
703
+ " run: |",
704
+ " VERSION=\"${{ steps.meta.outputs.version }}\"",
705
+ " SHA256=\"${{ steps.npm.outputs.sha256 }}\"",
706
+ ` FORMULA_PATH="homebrew-tap/Formula/${packageName}.rb"`,
707
+ " mkdir -p \"$(dirname \"${FORMULA_PATH}\")\"",
708
+ " cat > \"${FORMULA_PATH}\" <<EOF",
709
+ ` class ${formulaClassName} < Formula`,
710
+ ` desc ${JSON.stringify(context.packageJson?.description ?? "")}`,
711
+ ` homepage ${JSON.stringify(githubRepository ? `https://github.com/${githubRepository}` : "")}`,
712
+ ` url "https://registry.npmjs.org/${packageName}/-/${packageName}-${"${VERSION}"}.tgz"`,
713
+ " sha256 \"${SHA256}\"",
714
+ ` license ${JSON.stringify(context.packageJson?.license ?? "MIT")}`,
715
+ "",
716
+ " depends_on \"node\"",
717
+ "",
718
+ " def install",
719
+ " libexec.install Dir[\"package/*\"]",
720
+ ...executableNames.map((name) => ` bin.install_symlink libexec/"${executablePath}" => "${name}"`),
721
+ " end",
722
+ "",
723
+ " test do",
724
+ ` assert_match ${JSON.stringify(executableNames[0])}, shell_output("#{bin}/${executableNames[0]} --help")`,
725
+ " end",
726
+ " end",
727
+ " EOF",
728
+ " - name: Commit and push Homebrew tap changes",
729
+ " working-directory: homebrew-tap",
730
+ " run: |",
731
+ " git config user.name \"github-actions[bot]\"",
732
+ " git config user.email \"41898282+github-actions[bot]@users.noreply.github.com\"",
733
+ ` git add Formula/${packageName}.rb`,
734
+ " if git diff --cached --quiet; then",
735
+ " echo \"No Homebrew formula changes to commit.\"",
736
+ " exit 0",
737
+ " fi",
738
+ " git commit -m \"gitxplain ${GITHUB_REF_NAME}\"",
739
+ " git push"
740
+ ].join("\n")
741
+ : "",
742
+ packaging.deb
743
+ ? [
744
+ " - name: Create GitHub release and upload Debian package",
745
+ " uses: softprops/action-gh-release@v2",
746
+ " with:",
747
+ " files: ${{ steps.meta.outputs.deb_path }}"
748
+ ].join("\n")
749
+ : "",
750
+ packaging.aur
751
+ ? [
752
+ " - name: Print AUR update instructions",
753
+ " run: |",
754
+ " VERSION=\"${{ steps.meta.outputs.version }}\"",
755
+ " SHA256=\"${{ steps.npm.outputs.sha256 }}\"",
756
+ " echo \"Manual AUR update steps:\"",
757
+ " echo \"1. Update packaging/aur/PKGBUILD with pkgver=${VERSION} and sha256sums=('${SHA256}').\"",
758
+ " echo \"2. Run: makepkg --printsrcinfo > .SRCINFO\"",
759
+ " echo \"3. Commit PKGBUILD and .SRCINFO to the AUR git repository and push.\""
760
+ ].join("\n")
761
+ : ""
762
+ ]
763
+ .filter(Boolean)
764
+ .join("\n")
765
+ .concat("\n");
766
+ }
767
+
572
768
  return [
573
769
  "name: Release",
574
770
  "",
575
771
  "on:",
576
772
  " push:",
577
773
  " tags:",
578
- " - 'v*.*.*'",
774
+ " - 'v*'",
579
775
  "",
580
776
  "jobs:",
581
777
  " publish:",
778
+ " if: startsWith(github.ref_name, 'v')",
582
779
  " runs-on: ubuntu-latest",
583
780
  " permissions:",
584
781
  " contents: read",
@@ -588,6 +785,17 @@ export function buildReleaseWorkflow(context) {
588
785
  buildNodeReleaseSetup(context),
589
786
  context.commands.test ? formatRunStep("Test", context.commands.test) : "",
590
787
  context.commands.build ? formatRunStep("Build", context.commands.build) : "",
788
+ " - name: Verify npm token",
789
+ " env:",
790
+ " NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
791
+ " run: |",
792
+ " if [ -z \"${NODE_AUTH_TOKEN}\" ]; then",
793
+ " echo \"NPM_TOKEN is not configured for this repository.\"",
794
+ " echo \"Add it in GitHub: Settings -> Secrets and variables -> Actions -> New repository secret.\"",
795
+ " exit 1",
796
+ " fi",
797
+ " printf \"//registry.npmjs.org/:_authToken=%s\\n\" \"${NODE_AUTH_TOKEN}\" > \"${HOME}/.npmrc\"",
798
+ " npm whoami",
591
799
  " - name: Publish to npm",
592
800
  " env:",
593
801
  " NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}",
@@ -605,7 +813,7 @@ export function buildReleaseWorkflow(context) {
605
813
  "on:",
606
814
  " push:",
607
815
  " tags:",
608
- " - 'v*.*.*'",
816
+ " - 'v*'",
609
817
  "",
610
818
  "jobs:",
611
819
  " publish:",
@@ -635,10 +843,11 @@ export function buildReleaseWorkflow(context) {
635
843
  "on:",
636
844
  " push:",
637
845
  " tags:",
638
- " - 'v*.*.*'",
846
+ " - 'v*'",
639
847
  "",
640
848
  "jobs:",
641
849
  " publish:",
850
+ " if: startsWith(github.ref_name, 'v')",
642
851
  " runs-on: ubuntu-latest",
643
852
  " steps:",
644
853
  " - name: Checkout",
@@ -660,7 +869,7 @@ export function buildContainerWorkflow() {
660
869
  " push:",
661
870
  " branches: [main, master]",
662
871
  " tags:",
663
- " - 'v*.*.*'",
872
+ " - 'v*'",
664
873
  " pull_request:",
665
874
  "",
666
875
  "jobs:",
@@ -784,13 +993,21 @@ export function writePipelineFiles(cwd, analysis, selection) {
784
993
  mkdirSync(workflowDir, { recursive: true });
785
994
 
786
995
  const writtenFiles = [];
996
+ const updatedFiles = [];
997
+ const unchangedFiles = [];
787
998
  const notes = [];
788
999
 
789
1000
  const writeWorkflow = (relativePath, contents) => {
790
1001
  const absolutePath = path.join(cwd, relativePath);
791
1002
  mkdirSync(path.dirname(absolutePath), { recursive: true });
1003
+ const existingContents = existsSync(absolutePath) ? readFileSync(absolutePath, "utf8") : null;
792
1004
  writeFileSync(absolutePath, contents, "utf8");
793
1005
  writtenFiles.push(relativePath);
1006
+ if (existingContents === contents) {
1007
+ unchangedFiles.push(relativePath);
1008
+ } else {
1009
+ updatedFiles.push(relativePath);
1010
+ }
794
1011
  };
795
1012
 
796
1013
  if (selection.id === "ci" || selection.id === "ci-release") {
@@ -802,6 +1019,12 @@ export function writePipelineFiles(cwd, analysis, selection) {
802
1019
 
803
1020
  if (analysis.primary.release.type === "npm") {
804
1021
  notes.push("Add an `NPM_TOKEN` repository secret before pushing a release tag.");
1022
+ if (analysis.primary.packaging?.homebrew) {
1023
+ notes.push("Add a `HOMEBREW_TAP_TOKEN` repository secret so CI can update your tap repository.");
1024
+ }
1025
+ if (analysis.primary.packaging?.aur) {
1026
+ notes.push("AUR updates are still manual. The generated release workflow prints the exact PKGBUILD and .SRCINFO refresh steps.");
1027
+ }
805
1028
  } else if (analysis.primary.release.type === "pypi") {
806
1029
  notes.push("Add a `PYPI_TOKEN` repository secret before pushing a release tag.");
807
1030
  } else if (analysis.primary.release.type === "crates") {
@@ -829,5 +1052,5 @@ export function writePipelineFiles(cwd, analysis, selection) {
829
1052
  notes.push("This option only creates the container workflow. Run `gitxplain --pipeline` again if you also want CI verification.");
830
1053
  }
831
1054
 
832
- return { writtenFiles, notes };
1055
+ return { writtenFiles, updatedFiles, unchangedFiles, notes };
833
1056
  }
package/package.json CHANGED
@@ -1,11 +1,11 @@
1
1
  {
2
2
  "name": "gitxplain",
3
- "version": "0.1.9",
3
+ "version": "0.2.1",
4
4
  "description": "AI-powered Git commit explainer CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "gitxplain": "./cli/index.js",
8
- "gitxplore": "./cli/index.js"
7
+ "gitxplain": "cli/index.js",
8
+ "gitxplore": "cli/index.js"
9
9
  },
10
10
  "scripts": {
11
11
  "start": "node ./cli/index.js",
@@ -0,0 +1,60 @@
1
+ # Packaging
2
+
3
+ This directory contains packaging assets for Homebrew, AUR, and Debian releases.
4
+
5
+ ## Homebrew tap
6
+
7
+ Template formula: `packaging/homebrew-tap/Formula/gitxplain.rb`
8
+
9
+ Create and publish the tap repository:
10
+
11
+ ```bash
12
+ gh repo create guruswarupa/homebrew-tap --public --clone
13
+ cd homebrew-tap
14
+ mkdir -p Formula
15
+ cp /path/to/gitxplain/packaging/homebrew-tap/Formula/gitxplain.rb Formula/gitxplain.rb
16
+ git add Formula/gitxplain.rb
17
+ git commit -m "Add gitxplain formula"
18
+ git push -u origin main
19
+ ```
20
+
21
+ After each new npm publish, update the formula `url` and replace `"<SHA256_PLACEHOLDER>"` with the tarball SHA-256, then push the change.
22
+
23
+ ## AUR
24
+
25
+ Template files:
26
+
27
+ - `packaging/aur/PKGBUILD`
28
+ - `packaging/aur/.SRCINFO`
29
+
30
+ Generate `.SRCINFO` after updating `PKGBUILD`:
31
+
32
+ ```bash
33
+ cd /path/to/gitxplain/packaging/aur
34
+ makepkg --printsrcinfo > .SRCINFO
35
+ ```
36
+
37
+ Create and publish the AUR package:
38
+
39
+ ```bash
40
+ git clone ssh://aur@aur.archlinux.org/gitxplain.git aur-gitxplain
41
+ cd aur-gitxplain
42
+ cp /path/to/gitxplain/packaging/aur/PKGBUILD .
43
+ cp /path/to/gitxplain/packaging/aur/.SRCINFO .
44
+ makepkg --printsrcinfo > .SRCINFO
45
+ git add PKGBUILD .SRCINFO
46
+ git commit -m "Initial gitxplain release"
47
+ git push
48
+ ```
49
+
50
+ Replace `"<SHA256_PLACEHOLDER>"` with the npm tarball SHA-256 before publishing.
51
+
52
+ ## Debian
53
+
54
+ Build the Debian package from this repository:
55
+
56
+ ```bash
57
+ ./scripts/build-deb.sh
58
+ ```
59
+
60
+ The package is written to `dist/gitxplain_<version>_all.deb`.
@@ -0,0 +1,12 @@
1
+ pkgbase = gitxplain
2
+ pkgdesc = AI-powered Git commit explainer CLI
3
+ pkgver = 0.1.9
4
+ pkgrel = 1
5
+ url = https://github.com/guruswarupa/gitxplain
6
+ arch = any
7
+ license = MIT
8
+ depends = nodejs
9
+ source = https://registry.npmjs.org/gitxplain/-/gitxplain-0.1.9.tgz
10
+ sha256sums = <SHA256_PLACEHOLDER>
11
+
12
+ pkgname = gitxplain
@@ -0,0 +1,22 @@
1
+ pkgname=gitxplain
2
+ pkgver=0.1.9
3
+ pkgrel=1
4
+ pkgdesc="AI-powered Git commit explainer CLI"
5
+ arch=('any')
6
+ url="https://github.com/guruswarupa/gitxplain"
7
+ license=('MIT')
8
+ depends=('nodejs')
9
+ source=("https://registry.npmjs.org/${pkgname}/-/${pkgname}-${pkgver}.tgz")
10
+ sha256sums=('<SHA256_PLACEHOLDER>')
11
+
12
+ package() {
13
+ install -d "${pkgdir}/usr/lib/${pkgname}"
14
+ tar -xzf "${srcdir}/${pkgname}-${pkgver}.tgz" -C "${srcdir}"
15
+ cp -a "${srcdir}/package/." "${pkgdir}/usr/lib/${pkgname}/"
16
+
17
+ chmod 755 "${pkgdir}/usr/lib/${pkgname}/cli/index.js"
18
+
19
+ install -d "${pkgdir}/usr/bin"
20
+ ln -sf "/usr/lib/${pkgname}/cli/index.js" "${pkgdir}/usr/bin/gitxplain"
21
+ ln -sf "/usr/lib/${pkgname}/cli/index.js" "${pkgdir}/usr/bin/gitxplore"
22
+ }
@@ -0,0 +1,19 @@
1
+ class Gitxplain < Formula
2
+ desc "AI-powered Git commit explainer CLI"
3
+ homepage "https://github.com/guruswarupa/gitxplain"
4
+ url "https://registry.npmjs.org/gitxplain/-/gitxplain-0.1.9.tgz"
5
+ sha256 "<SHA256_PLACEHOLDER>"
6
+ license "MIT"
7
+
8
+ depends_on "node"
9
+
10
+ def install
11
+ libexec.install Dir["package/*"]
12
+ bin.install_symlink libexec/"cli/index.js" => "gitxplain"
13
+ bin.install_symlink libexec/"cli/index.js" => "gitxplore"
14
+ end
15
+
16
+ test do
17
+ assert_match "gitxplain", shell_output("#{bin}/gitxplain --help")
18
+ end
19
+ end
@@ -0,0 +1,64 @@
1
+ #!/usr/bin/env bash
2
+
3
+ set -euo pipefail
4
+
5
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
6
+ PACKAGE_JSON="${ROOT_DIR}/package.json"
7
+ VERSION="$(node -p "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).version" "${PACKAGE_JSON}")"
8
+ DESCRIPTION="$(node -p "JSON.parse(require('node:fs').readFileSync(process.argv[1], 'utf8')).description" "${PACKAGE_JSON}")"
9
+ PACKAGE_NAME="gitxplain"
10
+ ARCHITECTURE="${DEB_ARCHITECTURE:-all}"
11
+ MAINTAINER="${DEB_MAINTAINER:-Guruswarupa <opensource@local.invalid>}"
12
+ OUTPUT_DIR="${ROOT_DIR}/dist"
13
+ BUILD_ROOT="$(mktemp -d)"
14
+ PACKAGE_ROOT="${BUILD_ROOT}/${PACKAGE_NAME}_${VERSION}"
15
+ INSTALL_ROOT="${PACKAGE_ROOT}/usr/lib/${PACKAGE_NAME}"
16
+ CONTROL_DIR="${PACKAGE_ROOT}/DEBIAN"
17
+ DOC_DIR="${PACKAGE_ROOT}/usr/share/doc/${PACKAGE_NAME}"
18
+ DEB_FILE="${OUTPUT_DIR}/${PACKAGE_NAME}_${VERSION}_${ARCHITECTURE}.deb"
19
+
20
+ cleanup() {
21
+ rm -rf "${BUILD_ROOT}"
22
+ }
23
+
24
+ trap cleanup EXIT
25
+
26
+ mkdir -p "${OUTPUT_DIR}" "${INSTALL_ROOT}" "${CONTROL_DIR}" "${DOC_DIR}" "${PACKAGE_ROOT}/usr/bin"
27
+
28
+ cp -a \
29
+ "${ROOT_DIR}/cli" \
30
+ "${ROOT_DIR}/prompts" \
31
+ "${ROOT_DIR}/package.json" \
32
+ "${ROOT_DIR}/README.md" \
33
+ "${INSTALL_ROOT}/"
34
+
35
+ find "${PACKAGE_ROOT}" -type d -exec chmod 755 {} +
36
+ find "${PACKAGE_ROOT}" -type f -exec chmod 644 {} +
37
+ chmod 755 "${INSTALL_ROOT}/cli/index.js"
38
+ ln -s "../lib/${PACKAGE_NAME}/cli/index.js" "${PACKAGE_ROOT}/usr/bin/gitxplain"
39
+ ln -s "../lib/${PACKAGE_NAME}/cli/index.js" "${PACKAGE_ROOT}/usr/bin/gitxplore"
40
+
41
+ cat > "${CONTROL_DIR}/control" <<EOF
42
+ Package: ${PACKAGE_NAME}
43
+ Version: ${VERSION}
44
+ Section: utils
45
+ Priority: optional
46
+ Architecture: ${ARCHITECTURE}
47
+ Maintainer: ${MAINTAINER}
48
+ Depends: nodejs (>= 18)
49
+ Homepage: https://github.com/guruswarupa/gitxplain
50
+ Description: ${DESCRIPTION}
51
+ AI-powered Git commit explainer CLI distributed as a Debian package.
52
+ EOF
53
+
54
+ cat > "${DOC_DIR}/copyright" <<EOF
55
+ Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
56
+ Upstream-Name: gitxplain
57
+ Source: https://github.com/guruswarupa/gitxplain
58
+
59
+ Files: *
60
+ License: MIT
61
+ EOF
62
+
63
+ dpkg-deb --root-owner-group --build "${PACKAGE_ROOT}" "${DEB_FILE}" >/dev/null
64
+ echo "${DEB_FILE}"