@hanseltime/template-repo-sync 2.0.2 → 2.1.0

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.
@@ -34,27 +34,14 @@ jobs:
34
34
  node-version: 24.x
35
35
  cache: "npm"
36
36
  cache-dependency-path: package-lock.json
37
-
38
- - name: Ensure npm supports Trusted Publishing
39
- run: |
40
- npm i -g npm@^11.5.1
41
- npm --version
42
- npm config set registry https://registry.npmjs.org/
43
37
  - name: Install
44
38
  run: npm ci
45
39
  - name: Build
46
40
  run: npm run build
47
- # IMPORTANT: remove any token-based auth config so npm can use OIDC
48
- - name: Clear npm auth config (enable OIDC publish)
49
- run: |
50
- unset NPM_TOKEN
51
- unset NODE_AUTH_TOKEN
52
- rm -f .npmrc
53
- rm -f ~/.npmrc
41
+ - name: Release
42
+ run: npm run release
43
+ env:
44
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
54
45
  - name: Test Run
55
46
  run: |
56
47
  npm publish --access public
57
- # - name: Release
58
- # run: npm run release
59
- # env:
60
- # GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
@@ -53,6 +53,11 @@ jobs:
53
53
  run: |
54
54
  npm run lint
55
55
  npx prettier . --check
56
+ # Add git config user for some intgration testing
57
+ - name: setup git
58
+ run: |
59
+ git config --global user.name github-int-test
60
+ git config --global user.email github-int-test@github.com
56
61
  - name: testing
57
62
  run: |
58
63
  npm run test
package/CHANGELOG.md CHANGED
@@ -1,3 +1,24 @@
1
+ # [2.1.0](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.0.4...v2.1.0) (2026-02-08)
2
+
3
+
4
+ ### Features
5
+
6
+ * add support for template repo file deletions ([dd5d732](https://github.com/HanseltimeIndustries/template-repo-sync/commit/dd5d7324e17a0c015a4fb1abd78c079eea58693d))
7
+
8
+ ## [2.0.4](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.0.3...v2.0.4) (2026-02-05)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * leave semantic release out of npm publish ([9c0d9a3](https://github.com/HanseltimeIndustries/template-repo-sync/commit/9c0d9a389faf34c944640a5a3fc153b038e61b1d))
14
+
15
+ ## [2.0.3](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.0.2...v2.0.3) (2026-02-05)
16
+
17
+
18
+ ### Bug Fixes
19
+
20
+ * trigger a new release ([807e84f](https://github.com/HanseltimeIndustries/template-repo-sync/commit/807e84f51eaca42a2607cfbb57d2d672fb84a511))
21
+
1
22
  ## [2.0.2](https://github.com/HanseltimeIndustries/template-repo-sync/compare/v2.0.1...v2.0.2) (2026-02-04)
2
23
 
3
24
 
@@ -1 +1,12 @@
1
- export declare function gitDiff(gitDir: string, afterRef: string): Promise<string[]>;
1
+ /**
2
+ * For a given directory with a git configuration, this will return the modified, added, and
3
+ * deleted files since the last afterRef
4
+ * @param gitDir The git directory folder
5
+ * @param afterRef The git ref to look for diffs after
6
+ * @returns
7
+ */
8
+ export declare function gitDiff(gitDir: string, afterRef: string): Promise<{
9
+ modified: string[];
10
+ added: string[];
11
+ deleted: string[];
12
+ }>;
@@ -2,12 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.gitDiff = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ /**
6
+ * For a given directory with a git configuration, this will return the modified, added, and
7
+ * deleted files since the last afterRef
8
+ * @param gitDir The git directory folder
9
+ * @param afterRef The git ref to look for diffs after
10
+ * @returns
11
+ */
5
12
  async function gitDiff(gitDir, afterRef) {
6
- const diffFilesStr = (0, child_process_1.execSync)(`git diff ${afterRef}.. --name-only`, {
13
+ const baseCommand = `git diff ${afterRef}.. --no-renames --name-only`;
14
+ const modifiedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=M`, {
7
15
  cwd: gitDir,
8
16
  })
9
17
  .toString()
10
- .trim();
11
- return diffFilesStr.split("\n");
18
+ .trim()
19
+ .split("\n")
20
+ .filter((s) => s !== "");
21
+ const addedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=A`, {
22
+ cwd: gitDir,
23
+ })
24
+ .toString()
25
+ .trim()
26
+ .split("\n")
27
+ .filter((s) => s !== "");
28
+ const deletedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=D`, {
29
+ cwd: gitDir,
30
+ })
31
+ .toString()
32
+ .trim()
33
+ .split("\n")
34
+ .filter((s) => s !== "");
35
+ return {
36
+ modified: modifiedFiles,
37
+ added: addedFiles,
38
+ deleted: deletedFiles,
39
+ };
12
40
  }
13
41
  exports.gitDiff = gitDiff;
@@ -1,5 +1,10 @@
1
+ export interface DiffResult {
2
+ modified: string[];
3
+ added: string[];
4
+ deleted: string[];
5
+ }
1
6
  /**
2
7
  * A function that operates within the gitDir, and returns the list of file paths
3
8
  * since the last sha
4
9
  */
5
- export type TemplateDiffDriverFn = (gitDir: string, afterRef: string) => Promise<string[]>;
10
+ export type TemplateDiffDriverFn = (gitDir: string, afterRef: string) => Promise<DiffResult>;
@@ -1,10 +1,11 @@
1
- import { Config } from "./types";
1
+ import { Config, FileOperation } from "./types";
2
2
  import { Change } from "diff";
3
3
  interface MergeFileOptions {
4
4
  localTemplateSyncConfig: Config;
5
5
  templateSyncConfig: Config;
6
6
  tempCloneDir: string;
7
7
  cwd: string;
8
+ fileOperation: FileOperation;
8
9
  }
9
10
  interface MergeFileReturn {
10
11
  /**
@@ -26,6 +26,16 @@ async function mergeFile(relPath, context) {
26
26
  const ext = (0, path_1.extname)(relPath);
27
27
  const filePath = (0, path_1.join)(cwd, relPath);
28
28
  const templatePath = (0, path_1.join)(tempCloneDir, relPath);
29
+ // Unless there's a need, we remove files that were deleted and don't pass them to plugins yet
30
+ if (context.fileOperation === "deleted") {
31
+ if ((0, fs_1.existsSync)(templatePath)) {
32
+ await (0, promises_1.rm)(templatePath);
33
+ return {
34
+ ignoredDueToLocal: false,
35
+ localChanges: [],
36
+ };
37
+ }
38
+ }
29
39
  const mergeConfig = templateSyncConfig.merge?.find((mergeConfig) => (0, micromatch_1.isMatch)(relPath, mergeConfig.glob));
30
40
  const localMergeConfig = localTemplateSyncConfig.merge?.find((mergeConfig) => (0, micromatch_1.isMatch)(relPath, mergeConfig.glob));
31
41
  const mergeHandler = mergeConfig
@@ -69,27 +69,38 @@ async function templateSync(options) {
69
69
  filesToSync = await diffDriver(tempCloneDir, localTemplateSyncConfig.afterRef);
70
70
  }
71
71
  else {
72
- filesToSync = (0, match_1.getAllFilesInDir)(tempCloneDir, [
73
- ...templateSyncConfig.ignore,
74
- ".git/**",
75
- ]);
72
+ filesToSync = {
73
+ added: (0, match_1.getAllFilesInDir)(tempCloneDir, [
74
+ ...templateSyncConfig.ignore,
75
+ ".git/**",
76
+ ]),
77
+ deleted: [],
78
+ modified: [],
79
+ };
76
80
  }
77
81
  const localSkipFiles = [];
78
82
  const localFileChanges = {};
79
- await Promise.all(filesToSync.map(async (f) => {
80
- const result = await (0, merge_file_1.mergeFile)(f, {
81
- localTemplateSyncConfig,
82
- templateSyncConfig,
83
- tempCloneDir,
84
- cwd: options.repoDir,
85
- });
86
- if (result.ignoredDueToLocal) {
87
- localSkipFiles.push(f);
88
- }
89
- else if (result?.localChanges && result.localChanges.length > 0) {
90
- localFileChanges[f] = result.localChanges;
91
- }
92
- }));
83
+ const fileSyncFactory = (op) => {
84
+ return async (f) => {
85
+ const result = await (0, merge_file_1.mergeFile)(f, {
86
+ localTemplateSyncConfig,
87
+ templateSyncConfig,
88
+ tempCloneDir,
89
+ cwd: options.repoDir,
90
+ fileOperation: op,
91
+ });
92
+ if (result.ignoredDueToLocal) {
93
+ localSkipFiles.push(f);
94
+ }
95
+ else if (result?.localChanges && result.localChanges.length > 0) {
96
+ localFileChanges[f] = result.localChanges;
97
+ }
98
+ };
99
+ };
100
+ // Added and modified have the same setup for now
101
+ await Promise.all(filesToSync.added.map(fileSyncFactory("added")));
102
+ await Promise.all(filesToSync.modified.map(fileSyncFactory("modified")));
103
+ await Promise.all(filesToSync.deleted.map(fileSyncFactory("deleted")));
93
104
  // apply after ref
94
105
  if (options.updateAfterRef) {
95
106
  const ref = await currentRefDriver({
@@ -110,4 +110,5 @@ export interface MergePlugin<PluginOptions> {
110
110
  */
111
111
  validate(options: unknown): string[] | undefined;
112
112
  }
113
+ export type FileOperation = "added" | "modified" | "deleted";
113
114
  export {};
@@ -2,12 +2,40 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.gitDiff = void 0;
4
4
  const child_process_1 = require("child_process");
5
+ /**
6
+ * For a given directory with a git configuration, this will return the modified, added, and
7
+ * deleted files since the last afterRef
8
+ * @param gitDir The git directory folder
9
+ * @param afterRef The git ref to look for diffs after
10
+ * @returns
11
+ */
5
12
  async function gitDiff(gitDir, afterRef) {
6
- const diffFilesStr = (0, child_process_1.execSync)(`git diff ${afterRef}.. --name-only`, {
13
+ const baseCommand = `git diff ${afterRef}.. --no-renames --name-only`;
14
+ const modifiedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=M`, {
7
15
  cwd: gitDir,
8
16
  })
9
17
  .toString()
10
- .trim();
11
- return diffFilesStr.split("\n");
18
+ .trim()
19
+ .split("\n")
20
+ .filter((s) => s !== "");
21
+ const addedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=A`, {
22
+ cwd: gitDir,
23
+ })
24
+ .toString()
25
+ .trim()
26
+ .split("\n")
27
+ .filter((s) => s !== "");
28
+ const deletedFiles = (0, child_process_1.execSync)(`${baseCommand} --diff-filter=D`, {
29
+ cwd: gitDir,
30
+ })
31
+ .toString()
32
+ .trim()
33
+ .split("\n")
34
+ .filter((s) => s !== "");
35
+ return {
36
+ modified: modifiedFiles,
37
+ added: addedFiles,
38
+ deleted: deletedFiles,
39
+ };
12
40
  }
13
41
  exports.gitDiff = gitDiff;
@@ -26,6 +26,16 @@ async function mergeFile(relPath, context) {
26
26
  const ext = (0, path_1.extname)(relPath);
27
27
  const filePath = (0, path_1.join)(cwd, relPath);
28
28
  const templatePath = (0, path_1.join)(tempCloneDir, relPath);
29
+ // Unless there's a need, we remove files that were deleted and don't pass them to plugins yet
30
+ if (context.fileOperation === "deleted") {
31
+ if ((0, fs_1.existsSync)(templatePath)) {
32
+ await (0, promises_1.rm)(templatePath);
33
+ return {
34
+ ignoredDueToLocal: false,
35
+ localChanges: [],
36
+ };
37
+ }
38
+ }
29
39
  const mergeConfig = templateSyncConfig.merge?.find((mergeConfig) => (0, micromatch_1.isMatch)(relPath, mergeConfig.glob));
30
40
  const localMergeConfig = localTemplateSyncConfig.merge?.find((mergeConfig) => (0, micromatch_1.isMatch)(relPath, mergeConfig.glob));
31
41
  const mergeHandler = mergeConfig
@@ -69,27 +69,38 @@ async function templateSync(options) {
69
69
  filesToSync = await diffDriver(tempCloneDir, localTemplateSyncConfig.afterRef);
70
70
  }
71
71
  else {
72
- filesToSync = (0, match_1.getAllFilesInDir)(tempCloneDir, [
73
- ...templateSyncConfig.ignore,
74
- ".git/**",
75
- ]);
72
+ filesToSync = {
73
+ added: (0, match_1.getAllFilesInDir)(tempCloneDir, [
74
+ ...templateSyncConfig.ignore,
75
+ ".git/**",
76
+ ]),
77
+ deleted: [],
78
+ modified: [],
79
+ };
76
80
  }
77
81
  const localSkipFiles = [];
78
82
  const localFileChanges = {};
79
- await Promise.all(filesToSync.map(async (f) => {
80
- const result = await (0, merge_file_1.mergeFile)(f, {
81
- localTemplateSyncConfig,
82
- templateSyncConfig,
83
- tempCloneDir,
84
- cwd: options.repoDir,
85
- });
86
- if (result.ignoredDueToLocal) {
87
- localSkipFiles.push(f);
88
- }
89
- else if (result?.localChanges && result.localChanges.length > 0) {
90
- localFileChanges[f] = result.localChanges;
91
- }
92
- }));
83
+ const fileSyncFactory = (op) => {
84
+ return async (f) => {
85
+ const result = await (0, merge_file_1.mergeFile)(f, {
86
+ localTemplateSyncConfig,
87
+ templateSyncConfig,
88
+ tempCloneDir,
89
+ cwd: options.repoDir,
90
+ fileOperation: op,
91
+ });
92
+ if (result.ignoredDueToLocal) {
93
+ localSkipFiles.push(f);
94
+ }
95
+ else if (result?.localChanges && result.localChanges.length > 0) {
96
+ localFileChanges[f] = result.localChanges;
97
+ }
98
+ };
99
+ };
100
+ // Added and modified have the same setup for now
101
+ await Promise.all(filesToSync.added.map(fileSyncFactory("added")));
102
+ await Promise.all(filesToSync.modified.map(fileSyncFactory("modified")));
103
+ await Promise.all(filesToSync.deleted.map(fileSyncFactory("deleted")));
93
104
  // apply after ref
94
105
  if (options.updateAfterRef) {
95
106
  const ref = await currentRefDriver({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanseltime/template-repo-sync",
3
- "version": "2.0.2",
3
+ "version": "2.1.0",
4
4
  "description": "An npm library that enables pluggable, customizable synchronization between template repos",
5
5
  "main": "lib/cjs/index.js",
6
6
  "types": "lib/cjs/index.d.ts",
package/release.config.js CHANGED
@@ -23,12 +23,7 @@ module.exports = {
23
23
  "@semantic-release/npm",
24
24
  {
25
25
  npmPublish: false, // We just use this to increment the versions since semantic-release doesn't handle OIDC well
26
- },
27
- ],
28
- [
29
- "@semantic-release/exec",
30
- {
31
- publishCmd: "npm publish --access public",
26
+ // Call the publish comand in the CI/CD
32
27
  },
33
28
  ],
34
29
  [
@@ -0,0 +1,73 @@
1
+ import { execSync } from "child_process";
2
+ import * as fs from "fs";
3
+ import * as path from "path";
4
+ import { gitDiff } from "./git-diff";
5
+
6
+ describe("gitDiff", () => {
7
+ let tempDir: string;
8
+
9
+ beforeEach(() => {
10
+ tempDir = fs.mkdtempSync("gitdiff-test");
11
+ // Initialize a git repo
12
+ execSync("git init", { cwd: tempDir });
13
+ fs.writeFileSync(path.join(tempDir, "file1.txt"), "hello");
14
+ fs.writeFileSync(path.join(tempDir, "second.txt"), "second");
15
+ fs.writeFileSync(path.join(tempDir, "third.txt"), "third");
16
+ execSync("git add . && git commit -m 'initial'", { cwd: tempDir });
17
+ });
18
+
19
+ afterEach(() => {
20
+ fs.rmSync(tempDir, { recursive: true, force: true });
21
+ });
22
+
23
+ it("handles a rename as a delete and an add", async () => {
24
+ const firstCommit = execSync("git rev-parse HEAD", { cwd: tempDir })
25
+ .toString()
26
+ .trim();
27
+
28
+ // Perform a rename
29
+ execSync("git mv file1.txt file2.txt", { cwd: tempDir });
30
+ execSync("git commit -m 'rename'", { cwd: tempDir });
31
+
32
+ const result = await gitDiff(tempDir, firstCommit);
33
+
34
+ // Because of --no-renames:
35
+ expect(result).toEqual({
36
+ deleted: ["file1.txt"],
37
+ added: ["file2.txt"],
38
+ modified: [],
39
+ });
40
+ });
41
+ it("handles a added, deleted, and modified appropriately across multiple commits", async () => {
42
+ const firstCommit = execSync("git rev-parse HEAD", { cwd: tempDir })
43
+ .toString()
44
+ .trim();
45
+
46
+ // Perform a rename
47
+ execSync("git mv file1.txt file2.txt", { cwd: tempDir });
48
+ execSync("git commit -m 'rename'", { cwd: tempDir });
49
+
50
+ // Perform adds
51
+ fs.writeFileSync(path.join(tempDir, "another.txt"), "another file");
52
+ execSync("git add . && git commit -m 'adding another file'", {
53
+ cwd: tempDir,
54
+ });
55
+
56
+ // Perform a modify
57
+ fs.writeFileSync(path.join(tempDir, "third.txt"), "third+");
58
+ execSync("git add . && git commit -m 'modifying third'", { cwd: tempDir });
59
+
60
+ // Perform a delete
61
+ fs.rmSync(path.join(tempDir, "second.txt"));
62
+ execSync("git add . && git commit -m 'deleting second'", { cwd: tempDir });
63
+
64
+ const result = await gitDiff(tempDir, firstCommit);
65
+
66
+ // Because of --no-renames:
67
+ expect(result).toEqual({
68
+ deleted: expect.arrayContaining(["file1.txt", "second.txt"]),
69
+ added: expect.arrayContaining(["file2.txt", "another.txt"]),
70
+ modified: ["third.txt"],
71
+ });
72
+ });
73
+ });
@@ -1,10 +1,39 @@
1
1
  import { execSync } from "child_process";
2
2
 
3
+ /**
4
+ * For a given directory with a git configuration, this will return the modified, added, and
5
+ * deleted files since the last afterRef
6
+ * @param gitDir The git directory folder
7
+ * @param afterRef The git ref to look for diffs after
8
+ * @returns
9
+ */
3
10
  export async function gitDiff(gitDir: string, afterRef: string) {
4
- const diffFilesStr = execSync(`git diff ${afterRef}.. --name-only`, {
11
+ const baseCommand = `git diff ${afterRef}.. --no-renames --name-only`;
12
+ const modifiedFiles = execSync(`${baseCommand} --diff-filter=M`, {
5
13
  cwd: gitDir,
6
14
  })
7
15
  .toString()
8
- .trim();
9
- return diffFilesStr.split("\n");
16
+ .trim()
17
+ .split("\n")
18
+ .filter((s) => s !== "");
19
+ const addedFiles = execSync(`${baseCommand} --diff-filter=A`, {
20
+ cwd: gitDir,
21
+ })
22
+ .toString()
23
+ .trim()
24
+ .split("\n")
25
+ .filter((s) => s !== "");
26
+ const deletedFiles = execSync(`${baseCommand} --diff-filter=D`, {
27
+ cwd: gitDir,
28
+ })
29
+ .toString()
30
+ .trim()
31
+ .split("\n")
32
+ .filter((s) => s !== "");
33
+
34
+ return {
35
+ modified: modifiedFiles,
36
+ added: addedFiles,
37
+ deleted: deletedFiles,
38
+ };
10
39
  }
@@ -1,3 +1,9 @@
1
+ export interface DiffResult {
2
+ modified: string[];
3
+ added: string[];
4
+ deleted: string[];
5
+ }
6
+
1
7
  /**
2
8
  * A function that operates within the gitDir, and returns the list of file paths
3
9
  * since the last sha
@@ -5,4 +11,4 @@
5
11
  export type TemplateDiffDriverFn = (
6
12
  gitDir: string,
7
13
  afterRef: string,
8
- ) => Promise<string[]>;
14
+ ) => Promise<DiffResult>;