Skip to content

Commit bd50c37

Browse files
fix: keep .nojekyll and CNAME on redeploy (release 3.1.2) (#217)
* fix: keep .nojekyll and CNAME on redeploy The cleanup hook from #207 removes every tracked file that is not in dist. gh-pages writes .nojekyll and CNAME into its clone, not into dist, so a redeploy removed the .nojekyll that gh-pages had just created and GitHub Pages ran Jekyll again. CNAME survived only because gh-pages' own remove step unstages non-dot files first. The hook now keeps the files gh-pages generates for the active options. A leftover .nojekyll is still removed when nojekyll is disabled. CI deploys every Angular version twice and checks for .nojekyll. * chore: release 3.1.2 * fix: address review findings for the redeploy fix - CI checks the exact file name (git cat-file -e) and asserts that the second deploy created a commit - generatedFiles is a required parameter of createCleanupBeforeAddHook - unit tests for the hook with a mocked git - comments describe the current behaviour
1 parent a77bf04 commit bd50c37

7 files changed

Lines changed: 146 additions & 9 deletions

File tree

‎.github/workflows/main.yml‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,11 @@ jobs:
8686
npm link angular-cli-ghpages
8787
npx ng add angular-cli-ghpages
8888
npx ng deploy
89+
echo second > dist/test-app-18/browser/second.txt
90+
npx ng deploy --no-build --message="second deploy"
8991
cd /tmp/remote-18.git && git branch | grep gh-pages
92+
test "$(git rev-list --count gh-pages)" = 2
93+
git cat-file -e gh-pages:.nojekyll
9094
echo "Angular 18: build + deploy successful"
9195
9296
- name: Test Angular 19
@@ -99,7 +103,11 @@ jobs:
99103
npm link angular-cli-ghpages
100104
npx ng add angular-cli-ghpages
101105
npx ng deploy
106+
echo second > dist/test-app-19/browser/second.txt
107+
npx ng deploy --no-build --message="second deploy"
102108
cd /tmp/remote-19.git && git branch | grep gh-pages
109+
test "$(git rev-list --count gh-pages)" = 2
110+
git cat-file -e gh-pages:.nojekyll
103111
echo "Angular 19: build + deploy successful"
104112
105113
- name: Test Angular 20
@@ -112,7 +120,11 @@ jobs:
112120
npm link angular-cli-ghpages
113121
npx ng add angular-cli-ghpages
114122
npx ng deploy
123+
echo second > dist/test-app-20/browser/second.txt
124+
npx ng deploy --no-build --message="second deploy"
115125
cd /tmp/remote-20.git && git branch | grep gh-pages
126+
test "$(git rev-list --count gh-pages)" = 2
127+
git cat-file -e gh-pages:.nojekyll
116128
echo "Angular 20: build + deploy successful"
117129
118130
- name: Test Angular 21
@@ -125,7 +137,11 @@ jobs:
125137
npm link angular-cli-ghpages
126138
npx ng add angular-cli-ghpages
127139
npx ng deploy
140+
echo second > dist/test-app-21/browser/second.txt
141+
npx ng deploy --no-build --message="second deploy"
128142
cd /tmp/remote-21.git && git branch | grep gh-pages
143+
test "$(git rev-list --count gh-pages)" = 2
144+
git cat-file -e gh-pages:.nojekyll
129145
echo "Angular 21: build + deploy successful"
130146
131147
- name: Test Angular 22
@@ -138,5 +154,9 @@ jobs:
138154
npm link angular-cli-ghpages
139155
npx ng add angular-cli-ghpages
140156
npx ng deploy
157+
echo second > dist/test-app-22/browser/second.txt
158+
npx ng deploy --no-build --message="second deploy"
141159
cd /tmp/remote-22.git && git branch | grep gh-pages
160+
test "$(git rev-list --count gh-pages)" = 2
161+
git cat-file -e gh-pages:.nojekyll
142162
echo "Angular 22: build + deploy successful"

‎src/engine/engine.gh-pages-cleanup.spec.ts‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,62 @@ describe('end-to-end cleanup regression (issue #204, real git)', () => {
153153
expect(tree).not.toContain('stale.html');
154154
}, 30_000);
155155

156+
it('keeps .nojekyll and CNAME on a redeploy', async () => {
157+
const deploy = () => engine.run(
158+
distDir,
159+
{
160+
repo: bareRepoPath,
161+
branch: 'gh-pages',
162+
dotfiles: true,
163+
notfound: false,
164+
nojekyll: true,
165+
cname: 'example.org',
166+
name: 'Test',
167+
email: 'test@test.com',
168+
message: 'test deploy'
169+
},
170+
new logging.NullLogger()
171+
);
172+
173+
await deploy();
174+
await fs.writeFile(path.join(distDir, 'index.html'), '<html>second deploy</html>\n');
175+
await deploy();
176+
177+
const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
178+
.split('\n')
179+
.filter(Boolean)
180+
.sort();
181+
182+
expect(tree).toEqual(['.nojekyll', 'CNAME', 'index.html']);
183+
expect(git('show gh-pages:CNAME', { cwd: bareRepoPath })).toBe('example.org');
184+
}, 30_000);
185+
186+
it('removes a leftover .nojekyll when nojekyll is disabled', async () => {
187+
const deploy = (nojekyll: boolean) => engine.run(
188+
distDir,
189+
{
190+
repo: bareRepoPath,
191+
branch: 'gh-pages',
192+
dotfiles: true,
193+
notfound: false,
194+
nojekyll,
195+
name: 'Test',
196+
email: 'test@test.com',
197+
message: 'test deploy'
198+
},
199+
new logging.NullLogger()
200+
);
201+
202+
await deploy(true);
203+
await deploy(false);
204+
205+
const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
206+
.split('\n')
207+
.filter(Boolean);
208+
209+
expect(tree).toEqual(['index.html']);
210+
}, 30_000);
211+
156212
it('baseline: without our hook, gh-pages alone leaks dotfiles and submodule gitlinks (demonstrates the upstream bug)', async () => {
157213
// Call gh-pages.publish() directly — no engine.run(), no beforeAdd hook.
158214
// This is exactly what angular-cli-ghpages v3 did before our fix.

‎src/engine/engine.prepare-options-helpers.spec.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,4 +721,47 @@ describe('prepareOptions helpers - intensive tests', () => {
721721
}
722722
});
723723
});
724+
725+
describe('createCleanupBeforeAddHook', () => {
726+
let distDir: string;
727+
728+
beforeEach(async () => {
729+
distDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ghp-hook-'));
730+
await fs.writeFile(path.join(distDir, 'index.html'), '<html></html>');
731+
});
732+
733+
afterEach(async () => {
734+
await fs.rm(distDir, { recursive: true, force: true });
735+
});
736+
737+
function createGit(tracked: string[]) {
738+
const git: helpers.GhPagesGit = {
739+
cwd: '/clone',
740+
output: '',
741+
exec: vi.fn(async () => {
742+
git.output = tracked.join('\0');
743+
return git;
744+
}),
745+
rm: vi.fn(async () => git)
746+
};
747+
return git;
748+
}
749+
750+
it('keeps generated files that are tracked but not in dist', async () => {
751+
const git = createGit(['.nojekyll', 'CNAME', 'index.html']);
752+
const generatedFiles = ['.nojekyll', 'CNAME'];
753+
754+
await helpers.createCleanupBeforeAddHook(distDir, true, testLogger, generatedFiles)(git);
755+
756+
expect(git.rm).not.toHaveBeenCalled();
757+
});
758+
759+
it('removes tracked files that are neither in dist nor generated', async () => {
760+
const git = createGit(['.nojekyll', 'index.html', 'stale.html']);
761+
762+
await helpers.createCleanupBeforeAddHook(distDir, true, testLogger, [])(git);
763+
764+
expect(git.rm).toHaveBeenCalledWith(['.nojekyll', 'stale.html']);
765+
});
766+
});
724767
});

‎src/engine/engine.prepare-options-helpers.ts‎

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,20 +346,25 @@ export async function collectDistFiles(
346346
* indexed (`git ls-files -z`), diff against the set of files in our dist, and
347347
* `git rm` the leftovers. `git rm` correctly handles submodule gitlinks too.
348348
*
349+
* `generatedFiles` lists files that gh-pages itself writes into the clone
350+
* (`.nojekyll`, `CNAME`). They never exist in dist, so they are kept explicitly;
351+
* otherwise a redeploy would remove the `.nojekyll` that gh-pages just created.
352+
*
349353
* Upstream fix: tschaub/gh-pages#612 (merged 2025-08-09, unreleased as of
350354
* gh-pages@6.3.0). When a release containing that PR lands, this hook becomes
351355
* redundant and can be removed.
352356
*/
353357
export function createCleanupBeforeAddHook(
354358
distDir: string,
355359
dotfiles: boolean,
356-
logger: logging.LoggerApi
360+
logger: logging.LoggerApi,
361+
generatedFiles: string[]
357362
): (git: GhPagesGit) => Promise<void> {
358363
return async (git) => {
359364
const distFiles = await collectDistFiles(distDir, dotfiles);
360365
await git.exec('ls-files', '-z');
361366
const tracked = (git.output || '').split('\0').filter(Boolean);
362-
const toRemove = tracked.filter((f) => !distFiles.has(f));
367+
const toRemove = tracked.filter((f) => !distFiles.has(f) && !generatedFiles.includes(f));
363368
if (toRemove.length === 0) {
364369
return;
365370
}

‎src/engine/engine.ts‎

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ export async function run(
4949

5050
await checkIfDistFolderExists(dir);
5151
await createNotFoundFile(dir, prepared, logger);
52-
// Note: CNAME and .nojekyll files are now created by gh-pages v6+ via options
52+
// gh-pages writes CNAME and .nojekyll into its clone (cname/nojekyll options)
5353
await publishViaGhPages(ghpages, dir, prepared, logger);
5454

5555
if (!prepared.dryRun) {
@@ -150,8 +150,21 @@ async function createNotFoundFile(
150150
}
151151
}
152152

153-
// CNAME and .nojekyll files are now handled by gh-pages v6+ via the cname/nojekyll options
154-
// Previously we created these files ourselves before publishing, but gh-pages PR #533 added native support
153+
/**
154+
* Files that gh-pages writes into its clone (not into dist) for the given options.
155+
* gh-pages' own remove step already unstages CNAME before rewriting it; listing it
156+
* here keeps the cleanup hook correct independently of that detail.
157+
*/
158+
function generatedFiles(options: PreparedOptions): string[] {
159+
const files: string[] = [];
160+
if (options.nojekyll) {
161+
files.push('.nojekyll');
162+
}
163+
if (options.cname) {
164+
files.push('CNAME');
165+
}
166+
return files;
167+
}
155168

156169
async function publishViaGhPages(
157170
ghPages: GHPages,
@@ -206,7 +219,7 @@ async function publishViaGhPages(
206219
// Workaround for gh-pages#612 (unreleased in v6.3.0): clean up leftover
207220
// gh-pages branch files (dotfiles, submodule gitlinks) that the broken
208221
// remove step misses. Skipped when the user opts into additive mode.
209-
beforeAdd: options.add ? undefined : createCleanupBeforeAddHook(dir, options.dotfiles, logger)
222+
beforeAdd: options.add ? undefined : createCleanupBeforeAddHook(dir, options.dotfiles, logger, generatedFiles(options))
210223
};
211224

212225
// gh-pages@6 silently absorbs errors via its internal .then(_, onRejected)

‎src/package-lock.json‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎src/package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "angular-cli-ghpages",
3-
"version": "3.1.1",
3+
"version": "3.1.2",
44
"description": "Deploy your Angular app to GitHub Pages or Cloudflare Pages directly from the Angular CLI (ng deploy)",
55
"main": "index.js",
66
"types": "index.d.ts",

0 commit comments

Comments
 (0)