Skip to content

fix: surface gh-pages publish errors via callback (#205)#206

Merged
JohannesHoppe merged 1 commit intomainfrom
fix/issue-205-silent-publish-errors
Apr 22, 2026
Merged

fix: surface gh-pages publish errors via callback (#205)#206
JohannesHoppe merged 1 commit intomainfrom
fix/issue-205-silent-publish-errors

Conversation

@JohannesHoppe
Copy link
Copy Markdown
Member

@JohannesHoppe JohannesHoppe commented Apr 22, 2026

Summary

Fixes #205 — silent-swallow of gh-pages publish errors. CI builds were printing a green "Successfully published" banner and exiting 0 even when git clone failed with fatal: Authentication failed (e.g., expired GH_TOKEN), causing a full month of stale deploys in at least one reported setup.

  • Restored the 2019 callback wrapper around ghPages.publish() in src/engine/engine.ts. The .then(_, onRejected) handler inside gh-pages absorbs rejections (it calls the user callback without rethrowing), so the returned Promise resolves even on failure. The callback, however, still fires reliably with the error — bridging it to a Promise rejection is the fix.
  • Added 3 regression tests (one spawn-level canary that drives the real gh-pages through a mocked child_process.spawn).
  • Updated existing mocks across 4 spec files that depended on the old Promise-only API (they now also invoke the callback).
  • Bumped version 3.0.2 → 3.0.3.

Root cause

In v3.0.0 (#200) the historical callback wrapper was removed with the comment "gh-pages v5+ fixed the Promise bug". That referred to upstream commit e1374b3 ("always return a promise") — but that commit only fixed two early-exit validation paths (basePath not a directory / statSync throws). The main chain at gh-pages/lib/index.js:254-264 still absorbs rejections:

.then(
  () => done(),
  (error) => { /* ... */ done(error); },   // ← returns undefined, turning rejection into fulfilment
),

Upstream issues tschaub/gh-pages#465, tschaub/gh-pages#412, tschaub/gh-pages#151 are all still open tracking variants of this.

--> Reverting to the previous workaround from the v2 branch.

Note on tests

The spawn-level canary in engine.gh-pages-behavior.spec.ts exercises the real gh-pages library. If a future gh-pages release ever fixes the .then(_, onRejected) absorption, this test will still pass — giving a safer signal for later removing the wrapper than relying on the commit-message claim alone.

gh-pages@6 silently absorbs errors via its internal .then(_, onRejected)
handler that catches rejections and calls the user callback without
rethrowing — which converts the rejection into a fulfilment. The result:
git failures (notably HTTPS auth errors during clone) leave publish()'s
returned Promise in a resolved state, so angular-cli-ghpages printed its
"Successfully published" banner and exited 0 even when nothing was
pushed.

The v3.0.0 refactor removed a 2019 callback wrapper under the assumption
that upstream had fixed this. Upstream commit e1374b3 ("always return a
promise") only fixed two early-exit validation paths — not the main
chain's error absorption. Issues tschaub/gh-pages#465, #412 and #151
remain open.

Restore the callback-based wrapper: the callback always fires with the
error, so we bridge it to a rejection of our own Promise.

Regression tests:
- engine.gh-pages-integration.spec.ts: mock-contract test that encodes
  the exact bug shape (callback fires with error, promise resolves),
  plus a "no success banner on failure" assertion.
- engine.gh-pages-behavior.spec.ts: spawn-level canary that drives the
  real gh-pages library through engine.run() with child_process.spawn
  mocked to emit "fatal: Authentication failed" + exit 128 on clone.

Existing mocks across engine.spec.ts, engine-filesystem.spec.ts, and
parameter-tests/builder-integration.spec.ts updated to invoke the
callback (they previously relied on mockResolvedValue only and would
hang once engine started awaiting a callback).

Fixes #205
@JohannesHoppe JohannesHoppe merged commit ada6fa9 into main Apr 22, 2026
@JohannesHoppe JohannesHoppe self-assigned this Apr 22, 2026
JohannesHoppe added a commit that referenced this pull request Apr 22, 2026
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.
JohannesHoppe added a commit that referenced this pull request Apr 22, 2026
* fix: clean up leftover gh-pages branch files (dotfiles, submodules)

gh-pages@6.3.0's "Removing files" step calls globby without `dot: true`,
so dotfiles (.gitignore, .gitmodules, .github/) and submodule gitlinks
from the gh-pages branch are not removed before our dist is copied on
top. They then get re-committed and leak into the deploy.

Upstream fix tschaub/gh-pages#612 (merged 2025-08-09) adds both
`remove: '**/*'` and `dot: true`, but is unreleased as of v6.3.0. We
can't reach the globby call from options alone.

Workaround: register a `beforeAdd` hook that runs after gh-pages'
broken remove + our file copy. The hook asks git what it still has
indexed (`git ls-files -z`), diffs against the set of files in our
dist, and `git rm`s the leftovers. `git rm` handles submodule gitlinks
correctly, so the `build` gitlink from the reporter's scenario is also
cleaned up.

Skipped when the user opts into `add: true` (additive mode explicitly
preserves existing files).

Adds a spawn-level regression test in engine.gh-pages-behavior.spec.ts
that mocks `git ls-files` to return leftover dotfiles + a submodule
gitlink, runs engine.run() end-to-end through real gh-pages, and
asserts `git rm` targets the leftovers while leaving dist files alone.

Fixes #204

* 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.
@JohannesHoppe JohannesHoppe deleted the fix/issue-205-silent-publish-errors branch April 22, 2026 21:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Fake 'Successfully published' banner when underlying git push fails (silent deploy failure)

1 participant