Skip to content

feat: publish via gh-pages-fork (release 3.2.0-rc.0) - #218

Open
JohannesHoppe wants to merge 17 commits into
mainfrom
feat/gh-pages-fork
Open

JohannesHoppe wants to merge 17 commits into
mainfrom
feat/gh-pages-fork

Conversation

@JohannesHoppe

@JohannesHoppe JohannesHoppe commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

Closes #215. See also #90.

Release 3.2.0-rc.0 (then 3.2.0). Supports Angular 18 to 22 and Node.js >= 18.19.1, as before. No breaking changes for users, so everyone on ^3 gets the dependency-free publish step and its fixes.

What

src/gh-pages-fork/ is a minimal TypeScript fork of gh-pages (base: main at 48f62a7), set up like commander-fork: it exists solely for angular-cli-ghpages and will never be a general-purpose library. It keeps the upstream file structure and function names (lib/index.ts, lib/git.ts, lib/util.ts), so upstream changes can be compared and ported file by file. Its README lists the base commit, what was kept, added and removed, and every deviation from upstream.

How a deploy works

  1. stale temporary clones of interrupted deploys are removed
  2. git ls-remote checks the target branch: an existing branch is cloned alone, without checkout, into a fresh temporary directory (shallow, then in full); a new branch starts as an empty repository
  3. .nojekyll, CNAME and 404.html (copy of index.html) are written, then the build is copied (its own 404.html wins)
  4. git add --force --all (with --add: read-tree HEAD + --ignore-removal), commit if something changed, push
  5. the temporary clone is removed; a failing removal is logged and never changes the result

All git commands for the clone run with GIT_DIR/GIT_WORK_TREE pinned to it, without repository variables inherited from a surrounding git process, and with safe.directory for it. A token in the repository URL disables credential helpers, is removed from the clone's remote config right after cloning and is only passed to push; logs and the dry-run output hide it.

Bugs fixed

From two max-effort reviews, each with a regression test (most with real git):

  • concurrent deploys (e.g. nx run-many -t deploy) deleted or shared each other's clone; worst case, git commands ran in the user's project repository
  • a deploy started by git (hooks, ! aliases, worktrees) inherited GIT_DIR/GIT_INDEX_FILE and acted on the user's repository
  • the first deploy with --add to a new branch published the default branch's source tree
  • one transient error on the first clone turned an existing branch into an orphan and the push was rejected
  • a submodule gitlink sorting before .gitmodules (e.g. .github/actions/x) made every deploy fail
  • files matching the user's global gitignore or a .gitignore in the build were silently left out; tracked dotfiles matching an ignore rule were deleted from the site
  • a .nojekyll symlink on the branch made --add write through it
  • a git process killed by a signal (OOM, watchdog) counted as success
  • stderr output of git (e.g. GIT_TRACE) was read as repository URL or commit user
  • a missing git binary and real git config errors were reported as a missing remote
  • relative repository paths (--repo=../pages.git, or from the git config) failed
  • the token stayed in the cached clone's .git/config (and in the directory name of 3.x clones)
  • a second deploy with --no-notfound still published the 404.html that an earlier deploy had written into a reused dist
  • standalone CLI: --add=false turned add mode on, --dry-run=false still ran a dry run, a positional directory argument was silently ignored (it now prints a warning), absolute --dir paths didn't work, and npx angular-cli-ghpages crashed on Node 18 with a fresh install (ERR_REQUIRE_ESM)
  • ng deploy from a subdirectory looked for the build output relative to the current working directory instead of the workspace root
  • the --no-silent deprecation warning appeared on every ng deploy
  • deploy/schema.d.ts was missing from the package (the published typings import it)
  • GitHub Enterprise Server: the commit link in the commit message pointed to github.com
  • re-running ng add failed with "Target name already exists"

Behavior changes (all bug fixes)

  • the build folder is never modified: 404.html goes into the deployment only, and a 404.html of the build is kept instead of being overwritten
  • every file of the build is published (except .git): ignore rules (a global gitignore, a .gitignore inside the build) no longer drop files
  • the first deploy with --add to a new branch starts empty
  • --flag=false leaves a boolean flag unset in the standalone CLI (any other value sets it, as before)
  • ng add explains an existing deploy target of another builder and keeps its own one
  • setupMonkeypatch and the GHPages/PublishOptions types are no longer exported (internal, unused)

Behavior

Unchanged otherwise: 14 scenarios against real git (local bare repos) produce identical branches, commits and exit codes with 3.1.2 and this build, except the --no-notfound fix above. Further scenarios show the fixes (3.1.2 vs this build).

Tests

  • npm test type-checks all sources including specs; a vitest setup isolates the git environment (no global/system config, no inherited repository variables, tokens or CI variables); test files run in parallel
  • src/test-helpers/git.ts: shared real-git helpers and one spawn mock for the behavior and git-command specs
  • real-git specs use file:// remotes (shallow clones are real) and bare repositories with main as default branch
  • the gh-pages specs are ported to the fork instead of deleted; the behavior spec's done callbacks (which Vitest ignores, so its tests passed regardless of their assertions) are async
  • the standalone CLI deploys for real in a spec
  • CI installs the packed package fresh and deploys with the standalone CLI on Node 18.19.1, 20, 22 and 24, besides ng new → ng add → ng deploy for Angular 18 to 22

Also: vitest.config.ts → .mts, commander-fork fix 3/3 (boolean flags with a value), docs for the fork and for watching upstream gh-pages.

Vitest 5 warns about ESM syntax in a config file loaded as CommonJS.
src/gh-pages-fork is a minimal TypeScript fork of gh-pages (base: main at 48f62a7), internal to angular-cli-ghpages, with the upstream file structure and function names (lib/index.ts, lib/git.ts, lib/util.ts). It uses async/await throughout, has no dependencies and keeps only the options angular-cli-ghpages passes.

This removes all workarounds around gh-pages:
- the util.debuglog monkeypatch (the fork takes a log callback)
- the CACHE_DIR fallback for directories without package.json (#203)
- the beforeAdd cleanup hook (#204/#217); the fork removes all tracked files with git rm before copying dist
- the callback-to-promise bridge (#205); publish() rejects on every error
- the type declarations for gh-pages/lib/git

Removes the gh-pages dependency and its tree (48 packages, including the braces advisory CVE-2026-93687). Deploy results are unchanged: 14 real-git scenarios (first deploy, redeploy, seeded branch with dotfiles and submodule, --add, --no-dotfiles, user handling, symlinks, push rejection, dry run, --remote) produce identical branches, commits and exit codes with 3.1.2 and this build.

setupMonkeypatch and the GHPages/PublishOptions types stay exported as deprecated for API compatibility.

Closes #215
No external code depends on them, so they don't need to stay as deprecated exports.
The behavior, clean, cleanup, file creation, integration and no-package-json specs pin the deploy behavior, so they now run against gh-pages-fork with their expectations unchanged where the behavior is the same. Tests that only demonstrated upstream gh-pages bugs are dropped.

The behavior spec used done callbacks, which Vitest doesn't support: the first parameter is the test context, so these 24 tests passed regardless of their assertions. They are async now and fail on a wrong expectation.
- every publish() clones into a fresh temporary directory below the cache directory and removes it afterwards: concurrent deploys no longer delete or share each other's clone, no token stays in .git/config, and the engine no longer wipes the shared cache
- GIT_CEILING_DIRECTORIES keeps git inside the clone, so a broken clone can never fall through to the user's project repository
- credential helpers are disabled for repository URLs that carry credentials
- the remote url mismatch check is gone (every clone is fresh), so relative repository paths work
- git rm removes .gitmodules before all other files, so a submodule gitlink sorting before it (e.g. .github/actions/x) no longer breaks every deploy
- a new branch starts as an empty orphan branch, so the first deploy with --add no longer publishes the default branch's source tree
- spawn() rejects unless the exit code is 0 (a git process killed by a signal no longer counts as success) and returns stdout only
- getRemoteUrl() rethrows errors other than a failed git config, so a missing git binary is reported as such
- listFiles() skips broken symlinks and symlink cycles
- copy() writes files without the executable bit (like gh-pages v6.3.0), creates every directory once and copies with limited concurrency
- getUser() uses the configured git executable
- the fork's MIT license ships in the package
- real-git regression tests for every fix in gh-pages-fork publish()
- the clean spec checks that the temporary clone is removed after successful and failed deploys
- file creation tests check the pushed branch instead of the clone
- the dry-run 404.html test is restored (it was merged with a cname test)
- behavior and integration specs clear token variables and describe the fork instead of gh-pages internals
- npm test type-checks all sources including specs; spec type errors are fixed
- test files run in parallel again (no shared cache between suites)
- a non-writable cache directory (e.g. a read-only node_modules) falls back to a clone directory in the os temp dir
- when the target branch doesn't exist yet, the default branch is cloned shallow and without a checkout (only the remote is needed), with a full clone as the last resort for servers without shallow clone support
- src/test-helpers/git.ts: git(), tree(), commitCount(), createBareRepo(), seedBranch(), createMockChildProcess()
- real-git specs use the shared helpers instead of their own git wrappers and seed code
- the behavior spec configures responses through one spawnOverride hook instead of three copies of the spawn mock
- engine.spec no longer repeats the publish error handling of the integration spec
- tests for the temp dir and full clone fallbacks
@JohannesHoppe

Copy link
Copy Markdown
Member Author
VID-20260901-WA0006.mp4

…the user's repository

- git ls-remote decides whether the branch exists: an existing branch is cloned (shallow, then in full), a new branch starts as an empty repository. A transient error on the first clone no longer turns an existing branch into an orphan, hard failures report the real git error after one attempt, and the default branch is never checked out
- the branch is cloned without a checkout and staged with git add --force --all (--ignore-removal plus read-tree HEAD with --add): no git rm, so .gitmodules, ignore rules (global gitignore, .gitignore in dist) and symlinks on the branch can no longer drop or redirect files
- commands for the clone run with GIT_DIR/GIT_WORK_TREE pinned to it, without inherited repository variables (hooks, aliases, worktrees) and with safe.directory for it
- credential helpers are only disabled for URLs with a password; a URL with only a username (Bitbucket, Azure DevOps) keeps using them
- the token is removed from the clone's remote config right after cloning and only passed to push on the command line
- a failing cleanup of the temporary clone is logged instead of replacing the deploy result
- stale clones of interrupted deploys and clones of older versions are removed before each deploy; without a cache directory, clones go to a private directory in the os temp dir
- relative repository paths are resolved before cloning (from git config: against the repository top level)
- commits skip hooks (--no-verify); signal kills keep git's output in the error; getRemoteUrl reports real git errors
- listFiles skips .git and uses the JavaScript realpath
- shared spawn mock and git helpers for the specs
- commander-fork: boolean flags accept =true/=false (--add=false turned add mode on, --dry-run=false still ran a dry run)
- standalone CLI: unexpected positional arguments are an error (a directory argument was silently ignored); --dir accepts absolute paths
- utils.ts loads @angular-devkit/schematics only in createHost, so the engine and the standalone CLI run on Node 18 with a fresh install
- ng deploy resolves the auto-detected output directory against the workspace root instead of the current working directory
- an existing 404.html in dist is kept instead of being overwritten with index.html
- the dry-run output hides credentials of the repository URL
- the GitHub Actions commit link in the commit message uses GITHUB_SERVER_URL (GitHub Enterprise Server)
- the --no-silent deprecation warning only appears when the option is passed (schema without default; standalone CLI --no-silent)
- ng add keeps an existing angular-cli-ghpages deploy target and explains an existing deploy target of another builder; the workspace write is awaited
- deploy/schema.d.ts ships with the package (the published typings import it)
- remote URL discovery uses gh-pages-fork getRepo directly
…I on all Node versions

- vitest setup: empty global git config, no system config, no inherited repository variables, tokens or CI variables
- real-git tests use file:// remotes (shallow clones are real) and bare repositories with main as default branch
- new real-git tests: more files than the copy concurrency, nested dotfiles and .well-known, non-default remote, transient shallow clone failure keeps history, global gitignore, basePath that is a file, git identity from the environment, stale clone removal, failing cleanup (separate spec)
- standalone CLI: argument validation, --add=true/false, absolute --dir and a real deploy
- engine: kept 404.html, dry-run token redaction, GitHub Enterprise links, --no-silent of the standalone CLI, exact error preservation, ng add with an existing deploy target
- commander-fork: tests for boolean flags with a value
- behavior and git-commands specs share one spawn mock
- CI: the packed package is installed fresh and deploys with the standalone CLI on Node 18.19.1, 20, 22 and 24
publish() copies index.html to 404.html in the clone before copying the build, so a 404.html of the build wins and the build folder is never modified. A 404.html generated by an earlier deploy can no longer go stale in a reused dist folder or be published despite --no-notfound.
- fork README: structure, kept/added/removed parts and every deviation from upstream
- CLAUDE.md: deployment flow, standalone CLI, output directory resolution, watching upstream gh-pages
- README and standalone docs: dotfiles and ignore rules, 404.html, --add, --dir
- contributor docs: compiling programmatic usage examples, watching the forks, version list
- tests for 404.html in the deployment, remove an unused spy
@JohannesHoppe JohannesHoppe changed the title feat: publish via gh-pages-fork instead of the gh-pages package feat!: publish via gh-pages-fork (release 4.0.0) Oct 12, 2026
- deploy commits run git hooks again (no --no-verify): there is no reported problem it would fix
- commander-fork: --flag=false leaves a boolean flag unset; every other value sets it, as before
- standalone CLI: positional arguments are ignored with a warning instead of failing the deploy
- Git.exec() resolves with stdout; the mutable output field is gone
No user-facing breaking changes, so the fork ships as a minor version: everyone on ^3 gets the dependency-free publish step and its fixes.
@JohannesHoppe JohannesHoppe changed the title feat!: publish via gh-pages-fork (release 4.0.0) feat: publish via gh-pages-fork (release 3.2.0-rc.0) Oct 14, 2026

This branch has not been deployed

No deployments
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.

Remove dependency on gh-pages

1 participant