Skip to content

Commit 8152c35

Browse files
committed
test: add end-to-end real-git integration test for #204 cleanup
Prior tests covered the cleanup hook at the spawn-mock level only — they verified that the right git commands get issued, but not that the resulting gh-pages commit is actually clean. Add a real-filesystem, real-git test that: 1. Creates a local bare repo. 2. Seeds a gh-pages branch containing the exact conditions from #204: dotfiles (.gitignore, .gitmodules), nested dot-directory contents (.github/workflows/deploy.yml), a submodule gitlink, and a stale non-dot file. 3. Runs engine.run() (no mocks of child_process, fs, or gh-pages). 4. Inspects `git ls-tree -r gh-pages` on the bare repo and asserts the final tree contains ONLY index.html. Also adds a baseline test that calls gh-pages.publish() directly (bypassing our hook) and asserts the dotfiles + submodule DO leak — i.e. the upstream bug is real on the installed gh-pages version. If a future gh-pages release fixes the bug, that baseline test will fail as a clear signal that our workaround can be removed. No production changes; the hook implementation and its PR #206-era callback wrapper are exercised end-to-end by the new tests.
1 parent c07a4e0 commit 8152c35

1 file changed

Lines changed: 190 additions & 0 deletions

File tree

Lines changed: 190 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,190 @@
1+
/**
2+
* End-to-end integration test for issue #204 — REAL git, REAL gh-pages, REAL filesystem.
3+
*
4+
* No mocks. This test proves the `beforeAdd` cleanup hook actually produces a clean
5+
* gh-pages commit when the branch previously contained dotfiles and submodule gitlinks.
6+
*
7+
* Flow:
8+
* 1. Create a local bare git repo (serves as "remote").
9+
* 2. Seed a gh-pages branch containing:
10+
* - .github/workflows/deploy.yml (dot-directory, nested)
11+
* - .gitignore (dotfile)
12+
* - .gitmodules (dotfile)
13+
* - a submodule gitlink `build` (mode 160000)
14+
* - stale.html (regular file gh-pages' own remove would catch)
15+
* 3. Run `engine.run()` against the bare repo with a dist containing only index.html.
16+
* 4. `git ls-tree -r gh-pages` on the bare repo → assert ONLY `index.html` landed.
17+
*
18+
* A companion test demonstrates the upstream bug itself by calling `gh-pages.publish()`
19+
* directly (bypassing our hook) and observing the leftovers leak into the commit. That
20+
* test will start failing once tschaub/gh-pages ships PR #612 in a release — which is
21+
* fine; it's the signal that our workaround can be removed.
22+
*/
23+
24+
import * as path from 'path';
25+
import * as fs from 'fs/promises';
26+
import * as os from 'os';
27+
import { execSync } from 'child_process';
28+
29+
import { logging } from '@angular-devkit/core';
30+
31+
import * as engine from './engine';
32+
import { cleanupMonkeypatch } from './engine.prepare-options-helpers';
33+
34+
// NO MOCKS — we want real gh-pages behavior end-to-end
35+
const ghPages = require('gh-pages');
36+
37+
interface GitOptions {
38+
readonly cwd: string;
39+
}
40+
41+
function git(args: string, options: GitOptions): string {
42+
return execSync(`git -C "${options.cwd}" ${args}`, { stdio: ['pipe', 'pipe', 'pipe'] })
43+
.toString()
44+
.trim();
45+
}
46+
47+
async function seedGhPagesBranch(workDir: string, bareRepoPath: string): Promise<void> {
48+
await fs.mkdir(workDir, { recursive: true });
49+
execSync(`git init "${workDir}"`, { stdio: 'pipe' });
50+
git('config user.email "seed@test.com"', { cwd: workDir });
51+
git('config user.name "Seed"', { cwd: workDir });
52+
git('checkout -b gh-pages', { cwd: workDir });
53+
54+
// Dotfiles and dot-directory (what gh-pages' broken remove step misses)
55+
await fs.mkdir(path.join(workDir, '.github', 'workflows'), { recursive: true });
56+
await fs.writeFile(path.join(workDir, '.github', 'workflows', 'deploy.yml'), 'name: deploy\n');
57+
await fs.writeFile(path.join(workDir, '.gitignore'), 'node_modules\n');
58+
await fs.writeFile(
59+
path.join(workDir, '.gitmodules'),
60+
'[submodule "build"]\n\tpath = build\n\turl = https://example.invalid/build.git\n'
61+
);
62+
// A regular non-dot file — gh-pages' own remove step WILL catch this one.
63+
await fs.writeFile(path.join(workDir, 'stale.html'), '<html>stale</html>\n');
64+
65+
git('add .github .gitignore .gitmodules stale.html', { cwd: workDir });
66+
git('commit -m "seed dotfiles and stale content"', { cwd: workDir });
67+
68+
// Add a submodule gitlink without needing a real subrepo.
69+
// update-index --cacheinfo creates the 160000 entry directly in the index.
70+
const seedSha = git('rev-parse HEAD', { cwd: workDir });
71+
git(`update-index --add --cacheinfo 160000,${seedSha},build`, { cwd: workDir });
72+
git('commit -m "add submodule gitlink"', { cwd: workDir });
73+
74+
git(`remote add origin "${bareRepoPath}"`, { cwd: workDir });
75+
git('push origin gh-pages', { cwd: workDir });
76+
}
77+
78+
describe('end-to-end cleanup regression (issue #204, real git)', () => {
79+
let tempDir: string;
80+
let distDir: string;
81+
let bareRepoPath: string;
82+
let originalEnv: NodeJS.ProcessEnv;
83+
84+
// Unique suffix so parallel-ish reruns never collide in /tmp.
85+
const testRunId = `${Date.now()}-${Math.random().toString(36).substring(7)}`;
86+
87+
beforeEach(async () => {
88+
cleanupMonkeypatch();
89+
90+
// Isolate from ambient CI envs and tokens so engine.prepareOptions doesn't
91+
// rewrite the repo URL or append CI metadata during tests.
92+
originalEnv = { ...process.env };
93+
delete process.env.TRAVIS;
94+
delete process.env.CIRCLECI;
95+
delete process.env.GITHUB_ACTIONS;
96+
delete process.env.GH_TOKEN;
97+
delete process.env.PERSONAL_TOKEN;
98+
delete process.env.GITHUB_TOKEN;
99+
100+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), `ghp-204-${testRunId}-`));
101+
102+
distDir = path.join(tempDir, 'dist');
103+
await fs.mkdir(distDir, { recursive: true });
104+
await fs.writeFile(path.join(distDir, 'index.html'), '<html>fresh dist</html>\n');
105+
106+
bareRepoPath = path.join(tempDir, 'bare.git');
107+
execSync(`git init --bare "${bareRepoPath}"`, { stdio: 'pipe' });
108+
109+
await seedGhPagesBranch(path.join(tempDir, 'seed'), bareRepoPath);
110+
111+
// Clear gh-pages' cache so each test starts from a clean clone.
112+
ghPages.clean();
113+
});
114+
115+
afterEach(async () => {
116+
ghPages.clean();
117+
cleanupMonkeypatch();
118+
process.env = originalEnv;
119+
await fs.rm(tempDir, { recursive: true, force: true });
120+
});
121+
122+
it('engine.run() produces a gh-pages commit containing ONLY dist files (no dotfiles, no submodule)', async () => {
123+
await engine.run(
124+
distDir,
125+
{
126+
repo: bareRepoPath,
127+
branch: 'gh-pages',
128+
dotfiles: true,
129+
// Keep assertions simple: don't let our own 404.html / .nojekyll machinery
130+
// add files to the expected set.
131+
notfound: false,
132+
nojekyll: false,
133+
name: 'Test',
134+
email: 'test@test.com',
135+
message: 'test deploy'
136+
},
137+
new logging.NullLogger()
138+
);
139+
140+
// Inspect what actually landed on gh-pages in the bare repo.
141+
const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
142+
.split('\n')
143+
.filter(Boolean)
144+
.sort();
145+
146+
expect(tree).toEqual(['index.html']);
147+
148+
// Explicit negative checks — makes regressions very readable on failure.
149+
expect(tree).not.toContain('.gitignore');
150+
expect(tree).not.toContain('.gitmodules');
151+
expect(tree).not.toContain('.github/workflows/deploy.yml');
152+
expect(tree).not.toContain('build');
153+
expect(tree).not.toContain('stale.html');
154+
}, 30_000);
155+
156+
it('baseline: without our hook, gh-pages alone leaks dotfiles and submodule gitlinks (demonstrates the upstream bug)', async () => {
157+
// Call gh-pages.publish() directly — no engine.run(), no beforeAdd hook.
158+
// This is exactly what angular-cli-ghpages v3 did before our fix.
159+
await new Promise<void>((resolve, reject) => {
160+
ghPages.publish(
161+
distDir,
162+
{
163+
repo: bareRepoPath,
164+
branch: 'gh-pages',
165+
dotfiles: true,
166+
user: { name: 'Test', email: 'test@test.com' },
167+
message: 'baseline: unhooked gh-pages publish'
168+
},
169+
(err: Error | null) => {
170+
if (err) reject(err);
171+
else resolve();
172+
}
173+
);
174+
});
175+
176+
const tree = git('ls-tree -r gh-pages --name-only', { cwd: bareRepoPath })
177+
.split('\n')
178+
.filter(Boolean);
179+
180+
// gh-pages' broken remove step missed all of these:
181+
expect(tree).toContain('.gitignore');
182+
expect(tree).toContain('.gitmodules');
183+
expect(tree).toContain('.github/workflows/deploy.yml');
184+
expect(tree).toContain('build');
185+
// Our dist file did land:
186+
expect(tree).toContain('index.html');
187+
// And the non-dot stale file DID get removed by gh-pages' (partial) remove step:
188+
expect(tree).not.toContain('stale.html');
189+
}, 30_000);
190+
});

0 commit comments

Comments
 (0)