Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
1272ed8
chore: load the vitest config as ESM
JohannesHoppe Oct 9, 2026
5699a75
feat: publish via gh-pages-fork instead of the gh-pages package
JohannesHoppe Oct 9, 2026
7fb7089
docs: describe gh-pages-fork
JohannesHoppe Oct 9, 2026
e896cd3
refactor: remove setupMonkeypatch and the gh-pages types
JohannesHoppe Oct 9, 2026
50cfa13
test: port the gh-pages specs to gh-pages-fork instead of deleting them
JohannesHoppe Oct 9, 2026
c449f3c
fix: harden gh-pages-fork publish()
JohannesHoppe Oct 12, 2026
789ec9c
test: cover the publish() fixes and type-check specs
JohannesHoppe Oct 12, 2026
394a8ba
refactor: fall back to the os temp dir and clone new branches shallow
JohannesHoppe Oct 12, 2026
a8c764b
test: share git helpers between specs and remove duplicate tests
JohannesHoppe Oct 12, 2026
84b7b0d
docs: describe the clone fallbacks of gh-pages-fork
JohannesHoppe Oct 12, 2026
7be6052
fix: prepare the deploy branch without checkout and isolate git from …
JohannesHoppe Oct 12, 2026
947ca9a
fix: standalone CLI options, Node 18 support and engine edge cases
JohannesHoppe Oct 12, 2026
1390bcf
test: cover the new fixes, isolate the git environment and run the CL…
JohannesHoppe Oct 12, 2026
2024cb8
fix: write 404.html into the deployment instead of the build folder
JohannesHoppe Oct 12, 2026
70991ff
docs: describe gh-pages-fork v4 behavior; release 4.0.0
JohannesHoppe Oct 12, 2026
4f4918a
fix: keep the standalone CLI and commit behavior compatible
JohannesHoppe Oct 14, 2026
73ccca1
chore: release 3.2.0-rc.0
JohannesHoppe Oct 14, 2026
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
36 changes: 36 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,42 @@ jobs:
path: src/dist
retention-days: 1

# Install the built package fresh (like `npx angular-cli-ghpages` in a
# non-Angular project) on every Node.js version that engines allows and
# deploy with the standalone CLI to a local bare repository.
test-node-versions:
needs: build-and-test
runs-on: ubuntu-latest
strategy:
matrix:
node-version: ['18.19.1', '20', '22', '24']

steps:
- uses: actions/setup-node@v7
with:
node-version: ${{ matrix.node-version }}

- name: Download build artifact
uses: actions/download-artifact@v8
with:
name: dist
path: dist

- name: Install the package
run: |
(cd dist && npm pack --pack-destination /tmp)
mkdir /tmp/site && cd /tmp/site
npm init -y > /dev/null
npm install --no-fund --no-audit /tmp/angular-cli-ghpages-*.tgz

- name: Deploy with the standalone CLI
run: |
cd /tmp/site
git init --bare --initial-branch=main /tmp/remote.git
mkdir public && echo '<h1>site</h1>' > public/index.html
node node_modules/angular-cli-ghpages/angular-cli-ghpages --dir=public --repo=/tmp/remote.git --name="CI Bot" --email="ci@test.local"
git --git-dir=/tmp/remote.git ls-tree --name-only gh-pages | sort | tr '\n' ' ' | grep -x '.nojekyll 404.html index.html '

# Test ng add and ng deploy across multiple Angular versions (sequentially)
# This catches breaking changes in angular.json structure
# Each test creates its own git repo and deploys locally to verify the full flow
Expand Down
43 changes: 31 additions & 12 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ This file provides guidance when working with code in this repository.

## Overview

`angular-cli-ghpages` is an Angular CLI builder/schematic that deploys Angular applications to GitHub Pages, Cloudflare Pages, or any Git repository. It wraps the `gh-pages` npm package and integrates with Angular CLI's deployment infrastructure via `ng deploy`.
`angular-cli-ghpages` is an Angular CLI builder/schematic that deploys Angular applications to GitHub Pages, Cloudflare Pages, or any Git repository. It publishes via `src/gh-pages-fork/`, a minimal TypeScript fork of the `gh-pages` npm package, and integrates with Angular CLI's deployment infrastructure via `ng deploy`.

## Development Commands

Expand Down Expand Up @@ -102,13 +102,18 @@ npm dist-tag add angular-cli-ghpages@X.X.X-rc.X next
- Adds deploy target to `angular.json`

3. **Standalone CLI** (`src/angular-cli-ghpages`):
- Bash script for non-Angular CLI usage
- Uses `commander` for CLI parsing
- Node.js script for non-Angular CLI usage (`npx angular-cli-ghpages`)
- Parses options with `src/commander-fork` (frozen subset of commander v3.0.2, see its `README.md`)
- Positional arguments are ignored with a warning; `--flag=false` leaves a boolean flag unset

4. **Core Engine** (`src/engine/`):
- `engine.ts` - Core deployment logic (wraps gh-pages)
- `engine.ts` - Core deployment logic (publishes via gh-pages-fork)
- `defaults.ts` - Default configuration values

5. **gh-pages fork** (`src/gh-pages-fork/`):
- Minimal TypeScript fork of `gh-pages`, internal use only (see its `README.md`)
- Same file structure and function names as upstream (`lib/index.ts`, `lib/git.ts`, `lib/util.ts`)

### Deployment Flow

```
Expand All @@ -121,10 +126,20 @@ actions.ts (deploy function)
│ Uses BuilderContext.scheduleTarget()
└─→ engine.run()
├─→ Prepare options (tokens, CI env vars)
├─→ Create .nojekyll file (bypasses Jekyll on GitHub)
├─→ Create 404.html (copy of index.html for SPAs)
├─→ Create CNAME file (if custom domain)
└─→ Publish via gh-pages package
└─→ gh-pages-fork publish() (never modifies dist)
├─→ Remove stale temporary clones of interrupted deploys
├─→ git ls-remote: does the target branch exist?
│ yes → clone only that branch without checkout into a fresh temporary directory
│ no → empty repository with HEAD on the new branch
├─→ --add: index = tree of the branch (read-tree HEAD)
├─→ Write .nojekyll (bypasses Jekyll on GitHub), CNAME (if custom domain) and
│ 404.html (copy of index.html for SPAs), then copy dist (its own 404.html wins)
├─→ git add --force (--all, or --ignore-removal with --add), commit if something changed
├─→ Push
└─→ Remove the temporary clone

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.
```

### Build Target Resolution
Expand All @@ -139,10 +154,11 @@ actions.ts (deploy function)
- Final target: `prerenderTarget || staticBuildTarget` (see `src/deploy/builder.ts`)

Output directory resolution:
- Checks `angular.json` for `outputPath`
- Checks `angular.json` for `outputPath` (resolved against the workspace root)
- If undefined: `dist/<project>/browser` (Angular 20+ default)
- If string: appends `/browser` (modern Angular convention)
- If object: uses `${base}/${browser}` properties
- Can be overridden with `--dir` option
- Can be overridden with `--dir` option (relative to the current working directory, or absolute)

### Token Injection

Expand Down Expand Up @@ -175,7 +191,7 @@ The engine appends CI metadata to commit messages when running on:

1. **No Server-Side Rendering**: GitHub Pages only supports static files. SSR/Universal build targets are not supported.

2. **404.html Handling**:
2. **404.html Handling** (written by gh-pages-fork into the deployment, a `404.html` of the build wins):
- **GitHub Pages**: Requires `404.html` workaround (only way to get SPA routing, but returns HTTP 404 status)
- **Cloudflare Pages**: MUST NOT have `404.html` - its presence disables native SPA mode
- **Future**: Consider changing default or auto-detecting deployment target
Expand All @@ -194,7 +210,8 @@ The engine appends CI metadata to commit messages when running on:

## Testing

- Uses Vitest (`npm test`), tests in `*.spec.ts` files
- Uses Vitest (`npm test`, also type-checks all sources including specs), tests in `*.spec.ts` files
- Shared helpers for specs that run real git or mock child processes: `src/test-helpers/git.ts` (not part of the package)
- Requires git clone with `origin` remote (see `test-prerequisites.spec.ts`)
- All tests preserve/restore `process.env` using `originalEnv` pattern
- **No test counts in documentation** - they become stale quickly and are bragging
Expand All @@ -220,6 +237,8 @@ For sync considerations with AngularFire, monitor:
- https://github.com/angular/angularfire/blob/master/src/schematics/deploy/builder.ts
- https://github.com/angular/angularfire/blob/master/src/schematics/deploy/actions.ts

For `src/gh-pages-fork`, monitor upstream https://github.com/tschaub/gh-pages (releases, security advisories, `lib/`). npm audit and Dependabot can't report problems in forked code; port relevant fixes file by file and update the base commit in `src/gh-pages-fork/README.md`.

## GitHub CLI Usage

When performing GitHub operations (creating issues, PRs, etc.), use the `gh` CLI tool instead of web requests to avoid rate limiting and authentication issues.
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -302,6 +302,8 @@ In this case, provide **both** `name` and `email` string values to identify the

The command includes dotfiles by default (e.g. `.htaccess` will be committed).
With `--no-dotfiles` files starting with `.` are ignored.
Files and folders named `.git` are never published.
Ignore rules (a global gitignore or a `.gitignore` inside the build) don't apply: every file of the build is published.

#### --no-notfound <a name="no-notfound"></a>

Expand All @@ -312,6 +314,8 @@ With `--no-dotfiles` files starting with `.` are ignored.
- `ng deploy --no-notfound` – No `404.html` file is created.

By default, a `404.html` file is created, because this is the only known workaround for SPA routing on GitHub Pages (note: GitHub still returns HTTP 404 status, which may affect Brave browser and social previews).
The `404.html` is added to the deployment only; your build folder isn't modified.
If the build already contains a `404.html` (e.g. a custom error page), it is kept.

> [!IMPORTANT]
> **Cloudflare Pages users:** You **must** use `--no-notfound` to enable native SPA routing. Cloudflare Pages only activates SPA mode when no `404.html` file exists. See [#178](https://github.com/angular-schule/angular-cli-ghpages/issues/178)
Expand Down Expand Up @@ -353,17 +357,16 @@ A CNAME file will be created enabling you to use a custom domain.
- `ng deploy --add=true`

If it is set to `true`, it will only add, and never remove existing files.
By default, existing files in the target branch are removed before adding new ones.
[More information](https://www.npmjs.com/package/gh-pages#optionsadd).
By default, the build replaces the complete content of the target branch.

#### --dir <a name="dir"></a>

- **optional**
- Default: `undefined` (string) – Conventions will be used to guess the correct directory in your `dist` folder.
- Default: `undefined` (string) – The output directory of the build target in `angular.json` is used (relative to the workspace root).
- Example:
- `ng deploy --dir=dist/completely-different-folder/en`

Overrides the directory for all published sources, relative to the current working directory.
Overrides the directory for all published sources, relative to the current working directory (absolute paths work as well).

#### --dry-run <a name="dry-run"></a>

Expand Down
55 changes: 18 additions & 37 deletions docs/README_contributors.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
- [Programmatic Usage](#programmatic-usage)
- [Available Types](#available-types)
- [Advanced: Angular Builder Integration](#advanced-angular-builder-integration)
- [Dependency on gh-pages Internal API](#dependency-on-gh-pages-internal-api)
- [Remote URL Discovery](#remote-url-discovery)
- [gh-pages fork](#gh-pages-fork)
- [Supporting a new Angular version](#supporting-a-new-angular-version)
- [Keeping track of all the forks](#keeping-track-of-all-the-forks)

Expand Down Expand Up @@ -215,27 +214,20 @@ npm dist-tag add angular-cli-ghpages@X.X.X-rc.X next
For advanced use cases, `angular-cli-ghpages` can be used programmatically in Node.js scripts:

```typescript
import { deployToGHPages, defaults, Schema } from 'angular-cli-ghpages';
import { logging } from '@angular-devkit/core';
import { deployToGHPages, Schema } from 'angular-cli-ghpages';

// Deploy with custom options
// Options not set here fall back to the defaults (exported as `defaults`)
const options: Schema = {
...defaults,
dir: 'dist/my-app/browser',
repo: 'https://github.com/user/repo.git',
message: 'Custom deploy message',
branch: 'gh-pages',
name: 'Deploy Bot',
email: 'bot@example.com'
};

// Simple logger implementation
const logger = {
info: (msg: string) => console.log(msg),
warn: (msg: string) => console.warn(msg),
error: (msg: string) => console.error(msg),
debug: (msg: string) => console.debug(msg),
fatal: (msg: string) => console.error(msg)
};
const logger = new logging.Logger('deploy');
logger.subscribe((entry) => console.log(entry.message));

try {
await deployToGHPages('dist/my-app/browser', options, logger);
Expand All @@ -252,42 +244,27 @@ The package exports these TypeScript types for programmatic usage:
- `Schema` - Complete options interface
- `PreparedOptions` - Internal options after processing
- `DeployUser` - User credentials type
- `GHPages` - gh-pages library wrapper interface
- `defaults` - Default configuration object

### Advanced: Angular Builder Integration

For custom Angular builders:

```typescript
import { angularDeploy } from 'angular-cli-ghpages';
import { BuilderContext } from '@angular-devkit/architect';
import { angularDeploy, deployToGHPages, Schema } from 'angular-cli-ghpages';

// Inside your custom builder
const result = await angularDeploy(context, builderConfig, 'your-project-name');
// Inside your custom builder: build the target, then deploy its output
async function deploy(context: BuilderContext, options: Schema) {
await angularDeploy({ run: deployToGHPages }, context, { name: 'my-app:build:production' }, options);
}
```

**Note:** The CLI (`ng deploy`) remains the primary and recommended way to use this tool. Programmatic usage is considered advanced/experimental and may change between versions.

## Dependency on gh-pages Internal API

### Remote URL Discovery

The `getRemoteUrl` function in `src/engine/engine.prepare-options-helpers.ts` calls into `gh-pages/lib/git`, which is an **internal API** not documented in gh-pages' public interface. This dependency carries upgrade risk.

**What we depend on:**
- `new Git(process.cwd(), options.git).getRemoteUrl(options.remote)` from `gh-pages/lib/git`
- The exact error message format when remote doesn't exist or not in a git repository
## gh-pages fork

**Upgrade process for gh-pages v6+:**

1. Check test failures in `src/engine/engine.prepare-options-helpers.spec.ts` first, specifically the `getRemoteUrl` test block
2. If those tests fail, it likely indicates a breaking change in gh-pages' internal Git API
3. Options:
- If `gh-pages/lib/git` still exists with same interface: update our error message assertions
- If the internal API changed significantly: implement our own git remote discovery using `child_process.execSync('git config --get remote.{remote}.url')`
- If gh-pages added a public API for this: switch to the public API

**Current baseline:** Tests are pinned to gh-pages v6.3.0 behavior and error messages.
Publishing (clone the target branch, replace its content, commit, push) lives in [`src/gh-pages-fork/`](../src/gh-pages-fork/README.md), a minimal TypeScript fork of the [gh-pages](https://github.com/tschaub/gh-pages) npm package. It exists solely for angular-cli-ghpages and keeps the upstream file structure and function names, so upstream changes can be compared and ported file by file. Its README lists the upstream base commit, what was kept and what was removed.

## Compatibility principle

Expand Down Expand Up @@ -327,3 +304,7 @@ To stay in sync with the stuff the Angular team is doing, you might want to keep

- [builder.ts](https://github.com/angular/angularfire/blob/master/src/schematics/deploy/builder.ts)
- [actions.ts](https://github.com/angular/angularfire/blob/master/src/schematics/deploy/actions.ts)

`src/gh-pages-fork` follows [tschaub/gh-pages](https://github.com/tschaub/gh-pages). npm audit and Dependabot can't report problems in forked code, so watch the upstream repository (releases, security advisories, `lib/`) and port relevant fixes file by file (see `src/gh-pages-fork/README.md`).

`src/commander-fork` is a frozen subset of [commander v3.0.2](https://github.com/tj/commander.js), see `src/commander-fork/README.md`.
12 changes: 6 additions & 6 deletions docs/README_standalone.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ Output the version number. Please provide the version number on any bug report!
- Default: URL of the origin remote of the current dir (assumes a git repository)
- Example: `npx angular-cli-ghpages --repo=https://GH_TOKEN@github.com/<username>/<repositoryname>.git`

By default, **gh-pages** assumes that the current working directory is a git repository,
By default, **angular-cli-ghpages** assumes that the current working directory is a git repository,
and that you want to push changes to the `origin` remote.
If instead, your files are not in a git repository, or if you want to push to another repository,
you can provide the repository URL in the `repo` option.
Expand All @@ -130,7 +130,7 @@ you can provide the repository URL in the `repo` option.
- Default: `origin`
- Example: `npx angular-cli-ghpages --remote=github`

By default, **gh-pages** assumes that the current working directory is a git repository,
By default, **angular-cli-ghpages** assumes that the current working directory is a git repository,
and that you want to push changes to the `origin` remote.
If you want to push to another remote, you can provide the remote name in the `remote` option.

Expand Down Expand Up @@ -170,7 +170,8 @@ In this case provide both `name` and `email` string values to identify the commi
- **optional**
- Default: `dist`

Directory for all published sources, relative to the current working directory.
Directory for all published sources, relative to the current working directory (absolute paths work as well).
Pass it as an option (`--dir=dist/my-app/browser`); positional arguments are ignored (with a warning).
**Starting with Angular CLI 6, the build artifacts will be put in a subfolder under `dist`.
Please take a look at the `dist` folder to see whether there is a subfolder with your project's name or not.**

Expand Down Expand Up @@ -212,11 +213,10 @@ A CNAME file will be created enabling you to use a custom domain. [More informat
- **optional**
- Default: `false` (boolean) – The existing files will be removed from the branch you'll be pushing to as expected.
- Example:
- `npx angular-cli-ghpages --add=true`
- `npx angular-cli-ghpages --add` (or `--add=true`; `--add=false` keeps the default)

If is set to `true`, it will only add, and never remove existing files.
By default, existing files in the target branch are removed before adding the ones.
[More information](https://www.npmjs.com/package/gh-pages#optionsadd).
By default, the published directory replaces the complete content of the target branch.

#### --no-notfound <a name="no-notfound"></a>

Expand Down
9 changes: 5 additions & 4 deletions src/angular-cli-ghpages
Original file line number Diff line number Diff line change
Expand Up @@ -100,11 +100,12 @@ var consoleLogger = {
// --no-notfound becomes notfound: false
// --no-nojekyll becomes nojekyll: false

// console.log('*** dotfiles', commander.dotfiles)
// console.log('*** notfound', commander.notfound)
// console.log('*** nojekyll', commander.nojekyll)
if (commander.args.length > 0) {
consoleLogger.warn('⚠️ Ignoring argument(s): ' + commander.args.join(' ') + ' – use --dir=<dir> to choose the directory to deploy.');
}

var dir = path.join(process.cwd(), commander.dir);
// absolute paths (also Windows drive paths) stay as they are
var dir = path.resolve(process.cwd(), commander.dir);

engine.run(dir, commander, consoleLogger).catch(function (error) {
consoleLogger.error('❌ An error occurred when trying to deploy:');
Expand Down
6 changes: 6 additions & 0 deletions src/commander-fork/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,12 @@ This fork intentionally removes features (subcommands, actions, etc.) and preser
- Custom event listeners (except version and help)
- Most EventEmitter functionality

### Fixes on top of commander v3.0.2 🔧

1. **Negate detection**: only real `--no-` flags count as negated options
2. **Version flags**: short-only (`-v`) and custom version flags work
3. **Boolean flags with `=false`**: `--add=false` leaves the flag unset; any other value sets it, as in commander v3.0.2 (which also set the flag for `=false`)

## Compatibility Matrix

| Capability | Commander v3.0.2 | commander-fork |
Expand Down
Loading
Loading