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
58 changes: 47 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,19 @@ name: ci
on:
pull_request:
paths:
- 'crates/**'
- 'experiments/**'
- 'runtime/**'
- 'scripts/**'
- 'tests/**'
- 'wp-plugin/**'
- 'vendor/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/ci.yml'
- "crates/**"
- "experiments/**"
- "runtime/**"
- "scripts/**"
- "installer/**"
- "tests/**"
- "wp-plugin/**"
- "vendor/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/ci.yml"
push:
branches: ['trunk']
branches: ["trunk"]

jobs:
linux-cow-e2e:
Expand Down Expand Up @@ -116,3 +117,38 @@ jobs:
run: tests/cow/e2e.sh target/${{ matrix.target }}/release/forkpress
env:
FORKPRESS_FORCE_MACOS_APFS_SPARSEBUNDLE: "1"

windows-cow-check:
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc

- name: Rust unit tests (workspace crates)
shell: pwsh
run: |
cargo test --target x86_64-pc-windows-msvc --workspace --exclude forkpress-cli

- name: Rust unit tests (CLI)
shell: pwsh
run: |
$bundle = Join-Path $pwd 'empty-runtime.tar.gz'
Set-Content -Path $bundle -Value '' -NoNewline
$env:FORKPRESS_RUNTIME_BUNDLE = $bundle
cargo test --target x86_64-pc-windows-msvc -p forkpress-cli --bin forkpress

- name: Windows script syntax
shell: pwsh
run: |
foreach ($script in @(
'scripts/windows/build-dist.ps1',
'scripts/windows/install.ps1',
'scripts/windows/setup-dev-drive.ps1',
'scripts/windows/package.ps1',
'scripts/windows/sign.ps1'
)) {
[scriptblock]::Create((Get-Content -Raw $script)) | Out-Null
}
151 changes: 136 additions & 15 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@ name: release

on:
push:
tags: ['v*']
tags: ["v*"]
workflow_dispatch:
pull_request:
paths:
- 'crates/**'
- 'runtime/cow/**'
- 'runtime/wp.zip'
- 'scripts/build-dist.sh'
- 'scripts/cow/**'
- 'scripts/git/**'
- 'scripts/shared/**'
- 'wp-plugin/**'
- 'vendor/**'
- 'Cargo.toml'
- 'Cargo.lock'
- '.github/workflows/release.yml'
- "crates/**"
- "runtime/cow/**"
- "runtime/wp.zip"
- "scripts/build-dist.sh"
- "scripts/windows/**"
- "scripts/cow/**"
- "scripts/git/**"
- "scripts/shared/**"
- "installer/windows/**"
- "wp-plugin/**"
- "vendor/**"
- "Cargo.toml"
- "Cargo.lock"
- ".github/workflows/release.yml"

jobs:
build:
Expand All @@ -38,7 +40,13 @@ jobs:
- runner: ubuntu-24.04-arm
target: aarch64-unknown-linux-musl
os: linux
- runner: windows-latest
target: x86_64-pc-windows-msvc
os: windows
runs-on: ${{ matrix.runner }}
env:
WINDOWS_CODESIGN_CERT_BASE64: ${{ secrets.WINDOWS_CODESIGN_CERT_BASE64 }}
WINDOWS_CODESIGN_PASSWORD: ${{ secrets.WINDOWS_CODESIGN_PASSWORD }}
steps:
- uses: actions/checkout@v4

Expand All @@ -57,6 +65,12 @@ jobs:
build-essential clang curl git pkg-config unzip \
musl-tools php-cli composer re2c bison

- name: Install toolchain (windows)
if: matrix.os == 'windows'
shell: pwsh
run: |
choco install innosetup --no-progress -y

# Static PHP builds are slow (3-5 min of compilation). Cache the whole
# .build/ directory so we skip the clone, composer install, downloads,
# and the PHP/lib compilation when inputs haven't changed. Cache key
Expand All @@ -68,6 +82,7 @@ jobs:
key: >-
build-${{ matrix.target }}-${{ hashFiles(
'scripts/build-dist.sh',
'scripts/windows/build-dist.ps1',
'crates/forkpress-cli/build.rs'
) }}

Expand All @@ -76,23 +91,126 @@ jobs:
targets: ${{ matrix.target }}

- name: Build production dist bundle
if: matrix.os != 'windows'
run: scripts/build-dist.sh
env:
FORKPRESS_TARGET: ${{ matrix.target }}
GITHUB_TOKEN: ${{ github.token }}

- name: Build production dist bundle (windows)
if: matrix.os == 'windows'
shell: pwsh
run: scripts/windows/build-dist.ps1
env:
FORKPRESS_TARGET: ${{ matrix.target }}

- name: Build forkpress
run: cargo build --release --target ${{ matrix.target }} -p forkpress-cli --bin forkpress

- name: Require signing for tagged Windows releases
if: matrix.os == 'windows' && startsWith(github.ref, 'refs/tags/v') && env.WINDOWS_CODESIGN_CERT_BASE64 == ''
shell: pwsh
run: |
throw 'Tagged Windows releases require WINDOWS_CODESIGN_CERT_BASE64 and WINDOWS_CODESIGN_PASSWORD secrets.'

- name: Sign forkpress.exe (windows)
if: matrix.os == 'windows' && env.WINDOWS_CODESIGN_CERT_BASE64 != ''
shell: pwsh
run: scripts/windows/sign.ps1 -Files "target/${{ matrix.target }}/release/forkpress.exe"

- name: Package
if: matrix.os != 'windows'
run: |
cd target/${{ matrix.target }}/release
tar -czf ${{ github.workspace }}/forkpress-${{ matrix.target }}.tar.gz forkpress

- name: Package (windows)
if: matrix.os == 'windows'
shell: pwsh
run: |
$stage = Join-Path $env:RUNNER_TEMP 'forkpress-windows-package'
scripts/windows/package.ps1 `
-ForkPressExe "target/${{ matrix.target }}/release/forkpress.exe" `
-Output "forkpress-${{ matrix.target }}.zip" `
-StageDir $stage `
-KeepStage
$iscc = "${env:ProgramFiles(x86)}\Inno Setup 6\ISCC.exe"
& $iscc installer/windows/ForkPress.iss `
/DSourceDir="$stage" `
/DAppVersion="${{ github.ref_name }}" `
/O"${{ github.workspace }}"

- name: Sign installer (windows)
if: matrix.os == 'windows' && env.WINDOWS_CODESIGN_CERT_BASE64 != ''
shell: pwsh
run: scripts/windows/sign.ps1 -Files ForkPressSetup.exe

- name: Smoke packaged Windows artifact
if: matrix.os == 'windows'
shell: pwsh
run: |
$zip = "forkpress-${{ matrix.target }}.zip"
if (-not (Test-Path -LiteralPath $zip)) {
throw "Missing $zip"
}
if (-not (Test-Path -LiteralPath 'ForkPressSetup.exe')) {
throw 'Missing ForkPressSetup.exe'
}
$extract = Join-Path $env:RUNNER_TEMP 'forkpress-zip-smoke'
Remove-Item -Recurse -Force -LiteralPath $extract -ErrorAction SilentlyContinue
Expand-Archive -LiteralPath $zip -DestinationPath $extract
foreach ($required in @(
'forkpress.exe',
'scripts/windows/install.ps1',
'scripts/windows/setup-dev-drive.ps1',
'vendor/vc_redist.x64.exe'
)) {
if (-not (Test-Path -LiteralPath (Join-Path $extract $required))) {
throw "Packaged artifact is missing $required"
}
}
& (Join-Path $extract 'forkpress.exe') --version
if ($LASTEXITCODE -ne 0) {
throw "Packaged forkpress.exe failed with exit code $LASTEXITCODE"
}
$installRoot = Join-Path $env:RUNNER_TEMP 'forkpress-install-smoke'
$mountPath = Join-Path $env:RUNNER_TEMP 'ForkPressDevDriveSmoke'
$vhdPath = Join-Path $env:RUNNER_TEMP 'forkpress-smoke.vhdx'
Remove-Item -Recurse -Force -LiteralPath $installRoot, $mountPath -ErrorAction SilentlyContinue
Remove-Item -Force -LiteralPath $vhdPath -ErrorAction SilentlyContinue
try {
& (Join-Path $extract 'scripts/windows/install.ps1') `
-SourceRoot $extract `
-InstallRoot $installRoot `
-VhdPath $vhdPath `
-MountPath $mountPath `
-SiteName 'CI Smoke Site' `
-SizeGB 50 `
-AllowPlainReFS `
-FailOnRebootRequired `
-SkipAutoMount
if ($LASTEXITCODE -ne 0) {
throw "Packaged install.ps1 failed with exit code $LASTEXITCODE"
}
$siteWorkDir = Join-Path $mountPath 'Sites/CI Smoke Site/.forkpress'
if (-not (Test-Path -LiteralPath (Join-Path $siteWorkDir 'site.toml'))) {
throw 'Packaged install did not create an initialized ForkPress site.'
}
& (Join-Path $installRoot 'bin/forkpress.exe') storage status --work-dir $siteWorkDir
if ($LASTEXITCODE -ne 0) {
throw "Installed forkpress.exe storage status failed with exit code $LASTEXITCODE"
}
} finally {
Dismount-DiskImage -ImagePath $vhdPath -ErrorAction SilentlyContinue | Out-Null
}

- uses: actions/upload-artifact@v4
with:
name: forkpress-${{ matrix.target }}
path: forkpress-${{ matrix.target }}.tar.gz
path: |
forkpress-${{ matrix.target }}.tar.gz
forkpress-${{ matrix.target }}.zip
ForkPressSetup.exe
retention-days: 14

release:
Expand All @@ -109,5 +227,8 @@ jobs:
merge-multiple: true
- uses: softprops/action-gh-release@v2
with:
files: forkpress-*.tar.gz
files: |
forkpress-*.tar.gz
forkpress-*.zip
ForkPressSetup.exe
generate_release_notes: true
5 changes: 4 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ macOS release targets link only against `libSystem`:
- `crates/forkpress-core/` contains shared layout, manifest, path, and storage
strategy types.
- `crates/forkpress-storage/` contains production COW branch storage:
APFS clonefile, APFS sparsebundle, Linux `FICLONE`, and file-copy fallback.
APFS clonefile, APFS sparsebundle, Linux `FICLONE`, Windows ReFS block clone,
and file-copy fallback.
- `crates/forkpress-runtime/` contains embedded PHP/WordPress runtime
preparation and PHP script execution.
- `crates/forkpress-server/` contains the server process registry, stop/list
Expand All @@ -41,6 +42,8 @@ macOS release targets link only against `libSystem`:
- `runtime/` contains production COW runtime files and the WordPress archive
embedded into the binary.
- `scripts/` contains production/shared build, SQLite, Git, and COW helpers.
- `scripts/windows/` and `installer/windows/` contain the Windows runtime bundle
and click-through setup packaging.
- `tests/` contains production COW PHP tests.
- `experiments/branchfs/` contains the experimental BranchFS schema, PHP
extension, runtime files, scripts, Git adapter, and tests.
Expand Down
35 changes: 29 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ database file, and branch creation uses filesystem copy-on-write when the
machine can provide it.

No Docker, no system PHP, no MySQL daemon, no FUSE service, and no helper
daemon. The release artifact is one `forkpress` binary per target.
daemon. The release artifact is one `forkpress` binary on macOS/Linux and a
click-through installer on Windows.

## Quick Start

Expand Down Expand Up @@ -45,6 +46,22 @@ Stop the site server and detach any mount-backed COW storage:
Download the archive for your machine from a release, unpack it, and run the
binary.

Windows:

1. Download `ForkPressSetup.exe` from a release.
2. Open it and follow the prompts.
3. Accept the Windows permission prompt.
4. Reboot only if Windows asks.
5. Open **Start ForkPress Site** from the desktop or Start Menu.

The Windows installer installs protected program files under
`%ProgramFiles%\ForkPress`, creates a ReFS Dev Drive VHDX at
`%ProgramData%\ForkPress\Storage\forkpress-dev-drive.vhdx`, mounts it at
`%USERPROFILE%\ForkPressDevDrive`, adds `forkpress.exe` to the user PATH, creates
`%USERPROFILE%\ForkPressDevDrive\Sites\My ForkPress Site`, runs `forkpress init`
there, and creates shortcuts. It does not require WSL, Docker, FUSE, WinFsp, or
manual Windows feature setup.

macOS:

```bash
Expand All @@ -64,6 +81,7 @@ chmod +x forkpress

Release targets:

- `x86_64-pc-windows-msvc`
- `aarch64-apple-darwin`
- `x86_64-apple-darwin`
- `aarch64-unknown-linux-musl`
Expand Down Expand Up @@ -312,14 +330,18 @@ writes and do not participate in that lock.
ForkPress tries the cheapest ordinary-file view first:

1. **Native filesystem cloning in the project directory.** On macOS this uses
APFS `clonefile`; on Linux this uses `FICLONE` reflinks. New branches share
unchanged file blocks with the source branch. Writes to a branch path do not
mutate the source path.
APFS `clonefile`; on Linux this uses `FICLONE` reflinks; on Windows this
uses ReFS block cloning when the project lives on a ReFS/Dev Drive volume.
New branches share unchanged file blocks with the source branch. Writes to a
branch path do not mutate the source path.
2. **Rootless APFS sparsebundle on macOS.** If the project volume cannot clone files,
ForkPress creates `.forkpress/macos-cow/branches.sparsebundle`, mounts it at
`.forkpress/macos-cow/mount`, stores the physical branch trees there, and
exposes public branch directories like `./main` and `./marketing`.
3. **Full file copy.** This is the final fallback when COW storage is not
3. **Guided ReFS Dev Drive setup on Windows.** If a Windows project is not on
clone-capable storage, the Windows installer runs the Dev Drive setup flow
and creates ForkPress shortcuts into `%USERPROFILE%\ForkPressDevDrive`.
4. **Full file copy.** This is the final fallback when COW storage is not
available.

Inspect the selected file view:
Expand Down Expand Up @@ -412,7 +434,8 @@ Production Rust packages live under `crates/`:
- `forkpress-cli`: binaries and high-level command routing;
- `forkpress-core`: shared layout, manifest, path, and strategy types;
- `forkpress-storage`: production COW branch storage, including APFS
`clonefile`, APFS sparsebundle, Linux `FICLONE`, and file-copy fallback;
`clonefile`, APFS sparsebundle, Linux `FICLONE`, Windows ReFS block cloning,
and file-copy fallback;
- `forkpress-runtime`: embedded PHP/WordPress runtime preparation and PHP
script execution;
- `forkpress-server`: server registry, stop/list, and TCP readiness helpers;
Expand Down
Loading
Loading