Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@ jobs:
npm link angular-cli-ghpages
npx ng add angular-cli-ghpages
npx ng deploy
echo second > dist/test-app-18/browser/second.txt
npx ng deploy --no-build --message="second deploy"
cd /tmp/remote-18.git && git branch | grep gh-pages
test "$(git rev-list --count gh-pages)" = 2
git cat-file -e gh-pages:.nojekyll
echo "Angular 18: build + deploy successful"

- name: Test Angular 19
Expand All @@ -99,7 +103,11 @@ jobs:
npm link angular-cli-ghpages
npx ng add angular-cli-ghpages
npx ng deploy
echo second > dist/test-app-19/browser/second.txt
npx ng deploy --no-build --message="second deploy"
cd /tmp/remote-19.git && git branch | grep gh-pages
test "$(git rev-list --count gh-pages)" = 2
git cat-file -e gh-pages:.nojekyll
echo "Angular 19: build + deploy successful"

- name: Test Angular 20
Expand All @@ -112,7 +120,11 @@ jobs:
npm link angular-cli-ghpages
npx ng add angular-cli-ghpages
npx ng deploy
echo second > dist/test-app-20/browser/second.txt
npx ng deploy --no-build --message="second deploy"
cd /tmp/remote-20.git && git branch | grep gh-pages
test "$(git rev-list --count gh-pages)" = 2
git cat-file -e gh-pages:.nojekyll
echo "Angular 20: build + deploy successful"

- name: Test Angular 21
Expand All @@ -125,7 +137,11 @@ jobs:
npm link angular-cli-ghpages
npx ng add angular-cli-ghpages
npx ng deploy
echo second > dist/test-app-21/browser/second.txt
npx ng deploy --no-build --message="second deploy"
cd /tmp/remote-21.git && git branch | grep gh-pages
test "$(git rev-list --count gh-pages)" = 2
git cat-file -e gh-pages:.nojekyll
echo "Angular 21: build + deploy successful"

- name: Test Angular 22
Expand All @@ -138,5 +154,9 @@ jobs:
npm link angular-cli-ghpages
npx ng add angular-cli-ghpages
npx ng deploy
echo second > dist/test-app-22/browser/second.txt
npx ng deploy --no-build --message="second deploy"
cd /tmp/remote-22.git && git branch | grep gh-pages
test "$(git rev-list --count gh-pages)" = 2
git cat-file -e gh-pages:.nojekyll
echo "Angular 22: build + deploy successful"
56 changes: 56 additions & 0 deletions src/engine/engine.gh-pages-cleanup.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,62 @@ describe('end-to-end cleanup regression (issue #204, real git)', () => {
expect(tree).not.toContain('stale.html');
}, 30_000);

it('keeps .nojekyll and CNAME on a redeploy', async () => {
const deploy = () => engine.run(
distDir,
{
repo: bareRepoPath,
branch: 'gh-pages',
dotfiles: true,
notfound: false,
nojekyll: true,
cname: 'example.org',
name: 'Test',
email: 'test@test.com',
message: 'test deploy'
},
new logging.NullLogger()
);

await deploy();
await fs.writeFile(path.join(distDir, 'index.html'), '<html>second deploy</html>\n');
await deploy();

const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
.split('\n')
.filter(Boolean)
.sort();

expect(tree).toEqual(['.nojekyll', 'CNAME', 'index.html']);
expect(git('show gh-pages:CNAME', { cwd: bareRepoPath })).toBe('example.org');
}, 30_000);

it('removes a leftover .nojekyll when nojekyll is disabled', async () => {
const deploy = (nojekyll: boolean) => engine.run(
distDir,
{
repo: bareRepoPath,
branch: 'gh-pages',
dotfiles: true,
notfound: false,
nojekyll,
name: 'Test',
email: 'test@test.com',
message: 'test deploy'
},
new logging.NullLogger()
);

await deploy(true);
await deploy(false);

const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
.split('\n')
.filter(Boolean);

expect(tree).toEqual(['index.html']);
}, 30_000);

it('baseline: without our hook, gh-pages alone leaks dotfiles and submodule gitlinks (demonstrates the upstream bug)', async () => {
// Call gh-pages.publish() directly — no engine.run(), no beforeAdd hook.
// This is exactly what angular-cli-ghpages v3 did before our fix.
Expand Down
43 changes: 43 additions & 0 deletions src/engine/engine.prepare-options-helpers.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,4 +721,47 @@ describe('prepareOptions helpers - intensive tests', () => {
}
});
});

describe('createCleanupBeforeAddHook', () => {
let distDir: string;

beforeEach(async () => {
distDir = await fs.mkdtemp(path.join(os.tmpdir(), 'ghp-hook-'));
await fs.writeFile(path.join(distDir, 'index.html'), '<html></html>');
});

afterEach(async () => {
await fs.rm(distDir, { recursive: true, force: true });
});

function createGit(tracked: string[]) {
const git: helpers.GhPagesGit = {
cwd: '/clone',
output: '',
exec: vi.fn(async () => {
git.output = tracked.join('\0');
return git;
}),
rm: vi.fn(async () => git)
};
return git;
}

it('keeps generated files that are tracked but not in dist', async () => {
const git = createGit(['.nojekyll', 'CNAME', 'index.html']);
const generatedFiles = ['.nojekyll', 'CNAME'];

await helpers.createCleanupBeforeAddHook(distDir, true, testLogger, generatedFiles)(git);

expect(git.rm).not.toHaveBeenCalled();
});

it('removes tracked files that are neither in dist nor generated', async () => {
const git = createGit(['.nojekyll', 'index.html', 'stale.html']);

await helpers.createCleanupBeforeAddHook(distDir, true, testLogger, [])(git);

expect(git.rm).toHaveBeenCalledWith(['.nojekyll', 'stale.html']);
});
});
});
9 changes: 7 additions & 2 deletions src/engine/engine.prepare-options-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -346,20 +346,25 @@ export async function collectDistFiles(
* indexed (`git ls-files -z`), diff against the set of files in our dist, and
* `git rm` the leftovers. `git rm` correctly handles submodule gitlinks too.
*
* `generatedFiles` lists files that gh-pages itself writes into the clone
* (`.nojekyll`, `CNAME`). They never exist in dist, so they are kept explicitly;
* otherwise a redeploy would remove the `.nojekyll` that gh-pages just created.
*
* Upstream fix: tschaub/gh-pages#612 (merged 2025-08-09, unreleased as of
* gh-pages@6.3.0). When a release containing that PR lands, this hook becomes
* redundant and can be removed.
*/
export function createCleanupBeforeAddHook(
distDir: string,
dotfiles: boolean,
logger: logging.LoggerApi
logger: logging.LoggerApi,
generatedFiles: string[]
): (git: GhPagesGit) => Promise<void> {
return async (git) => {
const distFiles = await collectDistFiles(distDir, dotfiles);
await git.exec('ls-files', '-z');
const tracked = (git.output || '').split('\0').filter(Boolean);
const toRemove = tracked.filter((f) => !distFiles.has(f));
const toRemove = tracked.filter((f) => !distFiles.has(f) && !generatedFiles.includes(f));
if (toRemove.length === 0) {
return;
}
Expand Down
21 changes: 17 additions & 4 deletions src/engine/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export async function run(

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

if (!prepared.dryRun) {
Expand Down Expand Up @@ -150,8 +150,21 @@ async function createNotFoundFile(
}
}

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

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

// gh-pages@6 silently absorbs errors via its internal .then(_, onRejected)
Expand Down
4 changes: 2 additions & 2 deletions src/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion src/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "angular-cli-ghpages",
"version": "3.1.1",
"version": "3.1.2",
"description": "Deploy your Angular app to GitHub Pages or Cloudflare Pages directly from the Angular CLI (ng deploy)",
"main": "index.js",
"types": "index.d.ts",
Expand Down
Loading