Skip to content

Release: publish

Release: publish #199

name: "Release: publish"
on:
workflow_dispatch:
inputs:
release_ref:
description: "Commit/ref to publish, for example trunk or 99ce78f"
required: true
type: string
pull_request:
types: [closed]
branches: ["trunk"]
paths:
- "crates/**"
- "runtime/cow/**"
- "runtime/wp.zip"
- "scripts/build-dist.sh"
- "scripts/release-*.mjs"
- "scripts/windows/**"
- "scripts/cow/**"
- "scripts/git/**"
- "scripts/shared/**"
- "tests/release/**"
- "installer/windows/**"
- "wp-plugin/**"
- "vendor/**"
- "Cargo.toml"
- "Cargo.lock"
- "package.json"
- ".github/workflows/release-prepare.yml"
- ".github/workflows/release-publish.yml"
- ".github/workflows/release-verify.yml"
permissions:
contents: read
jobs:
preflight:
name: "Release: publish metadata"
if: >-
github.event_name == 'workflow_dispatch' ||
(
startsWith(github.event.pull_request.head.ref, 'release/v') &&
github.event.pull_request.head.repo.full_name == github.repository &&
github.event.pull_request.merged == true
)
runs-on: ubuntu-latest
outputs:
version: ${{ steps.metadata.outputs.version }}
tag: ${{ steps.metadata.outputs.tag }}
branch: ${{ steps.metadata.outputs.branch }}
is_prerelease: ${{ steps.metadata.outputs.is_prerelease }}
checkout_ref: ${{ steps.resolved_ref.outputs.checkout_ref }}
steps:
- name: Select requested ref
id: requested_ref
env:
RELEASE_REF: ${{ inputs.release_ref }}
MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }}
run: |
if [ "$GITHUB_EVENT_NAME" = "workflow_dispatch" ]; then
printf 'ref=%s\n' "$RELEASE_REF" >> "$GITHUB_OUTPUT"
else
printf 'ref=%s\n' "$MERGE_COMMIT_SHA" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@v6
with:
ref: ${{ steps.requested_ref.outputs.ref }}
fetch-depth: 0
- name: Resolve checkout ref
id: resolved_ref
run: echo "checkout_ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Validate merged release commit
run: |
git fetch origin trunk:refs/remotes/origin/trunk
if ! git merge-base --is-ancestor HEAD origin/trunk; then
echo "Release commit is not reachable from origin/trunk." >&2
exit 1
fi
- uses: actions/setup-node@v6
with:
node-version: 24
- name: Validate release metadata
id: metadata
env:
RELEASE_BRANCH: ${{ github.event.pull_request.head.ref }}
run: |
args=(--github-output)
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ]; then
args=(--release-branch "$RELEASE_BRANCH" "${args[@]}")
fi
node scripts/release-validate.mjs "${args[@]}"
- name: Validate tag state
run: |
git fetch --force --tags origin
tag="${{ steps.metadata.outputs.tag }}"
sha="$(git rev-parse HEAD)"
existing="$(git rev-parse -q --verify "refs/tags/${tag}^{}" 2>/dev/null || true)"
if [ -n "$existing" ]; then
echo "Tag $tag already exists at $existing." >&2
exit 1
fi
build:
name: "Release: publish ${{ matrix.target }}"
needs: preflight
if: needs.preflight.result == 'success'
strategy:
fail-fast: false
matrix:
include:
- runner: macos-14
target: aarch64-apple-darwin
os: darwin
- runner: macos-15-intel
target: x86_64-apple-darwin
os: darwin
- runner: ubuntu-24.04
target: x86_64-unknown-linux-musl
os: linux
- 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 }}
environment: buildkite
env:
BUILDKITE_API_TOKEN: ${{ secrets.BUILDKITE_TOKEN }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_ENDPOINT: ${{ secrets.AZURE_ENDPOINT }}
AZURE_CODE_SIGNING_ACCOUNT: ${{ secrets.AZURE_CODE_SIGNING_ACCOUNT }}
AZURE_CERTIFICATE_PROFILE: ${{ secrets.AZURE_CERTIFICATE_PROFILE }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_ref }}
- uses: actions/setup-node@v6
if: matrix.os == 'darwin'
with:
node-version: 24
- name: Download signed forkpress from Buildkite
if: matrix.os == 'darwin'
run: |
node scripts/release-download-buildkite-artifact.mjs \
--commit "${{ needs.preflight.outputs.checkout_ref }}" \
--artifact-path "target/${{ matrix.target }}/release/forkpress" \
--output "target/${{ matrix.target }}/release/forkpress" \
--wait-seconds 10800
chmod +x "target/${{ matrix.target }}/release/forkpress"
- uses: actions/setup-node@v6
if: matrix.target == 'x86_64-pc-windows-msvc'
with:
node-version: 24
- name: Download signed forkpress.exe from Buildkite
if: matrix.target == 'x86_64-pc-windows-msvc'
shell: pwsh
run: |
node scripts/release-download-buildkite-artifact.mjs `
--commit "${{ needs.preflight.outputs.checkout_ref }}" `
--artifact-path "target/${{ matrix.target }}/release/forkpress.exe" `
--output "target/${{ matrix.target }}/release/forkpress.exe"
- name: Verify Buildkite forkpress.exe signature
if: matrix.target == 'x86_64-pc-windows-msvc'
shell: pwsh
run: |
$signature = Get-AuthenticodeSignature -LiteralPath "target/${{ matrix.target }}/release/forkpress.exe"
if ($signature.Status -ne 'Valid') {
throw "Buildkite forkpress.exe signature is $($signature.Status)."
}
- name: Release preflight checks
if: matrix.os != 'windows'
run: make test-release
- name: Install toolchain (linux)
if: matrix.os == 'linux'
run: |
sudo sed -i -E 's|http://ports.ubuntu.com/ubuntu-ports|https://ports.ubuntu.com/ubuntu-ports|g' /etc/apt/sources.list /etc/apt/sources.list.d/*.sources 2>/dev/null || true
sudo apt-get \
-o Acquire::Retries=5 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
update
sudo apt-get \
-o Acquire::Retries=5 \
-o Acquire::http::Timeout=30 \
-o Acquire::https::Timeout=30 \
install -y --no-install-recommends \
automake autopoint 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
# includes every input that affects the produced binaries.
- name: Cache .build
if: matrix.os != 'darwin' && matrix.target != 'x86_64-pc-windows-msvc'
uses: actions/cache@v5
with:
path: .build
key: >-
build-${{ matrix.target }}-${{ hashFiles(
'scripts/build-dist.sh',
'scripts/windows/build-dist.ps1',
'crates/forkpress-cli/build.rs'
) }}
- uses: dtolnay/rust-toolchain@stable
if: matrix.os != 'darwin' && matrix.target != 'x86_64-pc-windows-msvc'
with:
targets: ${{ matrix.target }}
- name: Build production dist bundle
if: matrix.os == 'linux'
run: scripts/build-dist.sh
env:
FORKPRESS_TARGET: ${{ matrix.target }}
GITHUB_TOKEN: ${{ github.token }}
- name: Build production dist bundle (windows)
if: matrix.os == 'windows' && matrix.target != 'x86_64-pc-windows-msvc'
shell: pwsh
run: scripts/windows/build-dist.ps1
env:
FORKPRESS_TARGET: ${{ matrix.target }}
- name: Build forkpress
if: matrix.os != 'darwin' && matrix.target != 'x86_64-pc-windows-msvc'
run: cargo build --release --target ${{ matrix.target }} -p forkpress-cli --bin forkpress
- name: Verify forkpress signature (mac)
if: matrix.os == 'darwin'
run: |
binary="target/${{ matrix.target }}/release/forkpress"
codesign --verify --strict --verbose=2 "$binary"
details="$(codesign -dv --verbose=4 "$binary" 2>&1)"
printf '%s\n' "$details"
if ! grep -Fq 'Authority=Developer ID Application: Automattic, Inc. (PZYM8XX95Q)' <<<"$details"; then
echo "forkpress is not signed by the Automattic Developer ID Application certificate." >&2
exit 1
fi
if ! grep -Eq 'flags=.*runtime' <<<"$details"; then
echo "forkpress is not signed with hardened runtime." >&2
exit 1
fi
- name: Warn when Windows installer signing is unavailable
if: matrix.os == 'windows' && env.AZURE_TENANT_ID == ''
shell: pwsh
run: |
Write-Output '::warning::Azure Trusted Signing env vars are not configured; publishing an unsigned Windows installer.'
- name: Sign forkpress.exe (windows)
if: matrix.os == 'windows' && matrix.target != 'x86_64-pc-windows-msvc' && env.AZURE_TENANT_ID != ''
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: Smoke packaged artifact
if: matrix.os != 'windows'
run: |
archive="${{ github.workspace }}/forkpress-${{ matrix.target }}.tar.gz"
test -f "$archive"
extract="$(mktemp -d)"
tar -xzf "$archive" -C "$extract"
test -x "$extract/forkpress"
"$extract/forkpress" --version
site="$extract/site"
mkdir -p "$site"
(
cd "$site"
"$extract/forkpress" init --work-dir "$site/.forkpress" --site-title "CI Smoke Site"
)
test -f "$site/.forkpress/site.toml"
- 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="${{ needs.preflight.outputs.version }}" `
/O"${{ github.workspace }}"
- name: Sign installer (windows)
if: matrix.os == 'windows' && env.AZURE_TENANT_ID != ''
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 `
-NoPauseOnError `
-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@v7
with:
name: forkpress-${{ matrix.target }}
path: |
forkpress-${{ matrix.target }}.tar.gz
forkpress-${{ matrix.target }}.zip
ForkPressSetup.exe
retention-days: 14
release:
name: "Release: publish"
needs:
- preflight
- build
runs-on: ubuntu-latest
if: needs.preflight.result == 'success'
permissions:
contents: write
env:
HOMEBREW_TAP_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }}
steps:
- uses: actions/checkout@v6
with:
ref: ${{ needs.preflight.outputs.checkout_ref }}
fetch-depth: 0
- name: Require Homebrew tap token
if: needs.preflight.outputs.is_prerelease != 'true'
run: |
if [ -z "$HOMEBREW_TAP_TOKEN" ]; then
echo "HOMEBREW_TAP_TOKEN is required to update Automattic/homebrew-tap." >&2
exit 1
fi
- uses: actions/download-artifact@v8
with:
pattern: forkpress-*
merge-multiple: true
- name: Verify release artifacts
run: |
expected=(
forkpress-aarch64-apple-darwin.tar.gz
forkpress-x86_64-apple-darwin.tar.gz
forkpress-aarch64-unknown-linux-musl.tar.gz
forkpress-x86_64-unknown-linux-musl.tar.gz
forkpress-x86_64-pc-windows-msvc.zip
ForkPressSetup.exe
)
for file in "${expected[@]}"; do
test -f "$file"
done
sha256sum "${expected[@]}" > SHA256SUMS
- name: Create release tag
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git fetch --force --tags origin
tag="${{ needs.preflight.outputs.tag }}"
sha="$(git rev-parse HEAD)"
existing="$(git rev-parse -q --verify "refs/tags/${tag}^{}" 2>/dev/null || true)"
if [ -z "$existing" ]; then
git tag -a "$tag" "$sha" -m "Release $tag"
git push origin "$tag"
else
echo "Tag $tag already exists at $existing." >&2
exit 1
fi
- uses: softprops/action-gh-release@v3
with:
tag_name: ${{ needs.preflight.outputs.tag }}
target_commitish: ${{ needs.preflight.outputs.checkout_ref }}
files: |
forkpress-*.tar.gz
forkpress-*.zip
ForkPressSetup.exe
SHA256SUMS
generate_release_notes: true
prerelease: ${{ needs.preflight.outputs.is_prerelease }}
- uses: actions/checkout@v6
if: needs.preflight.outputs.is_prerelease != 'true'
with:
repository: Automattic/homebrew-tap
ref: master
path: homebrew-tap
token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
- name: Update Homebrew tap
if: needs.preflight.outputs.is_prerelease != 'true'
run: |
node scripts/release-homebrew-formula.mjs "${{ needs.preflight.outputs.version }}" SHA256SUMS > homebrew-tap/forkpress.rb
ruby -c homebrew-tap/forkpress.rb
git -C homebrew-tap config user.name "github-actions[bot]"
git -C homebrew-tap config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git -C homebrew-tap add forkpress.rb
if git -C homebrew-tap diff --cached --quiet -- forkpress.rb; then
echo "Automattic/homebrew-tap already has forkpress.rb for ${{ needs.preflight.outputs.tag }}."
exit 0
fi
git -C homebrew-tap commit \
-m "Update ForkPress to ${{ needs.preflight.outputs.tag }}" \
-m "Release: https://github.com/Automattic/forkpress/releases/tag/${{ needs.preflight.outputs.tag }}"
git -C homebrew-tap push origin HEAD:master