mirror of
https://github.com/vercel-labs/fx.git
synced 2026-10-28 10:15:17 +08:00
- Run the full deterministic E2E suite on Linux in four shards on every pull request, plus the MCP conformance baseline, all against one shared build of fx. - Run macOS arm64 unit tests and platform E2E files only when a change touches macOS-specific code, build.zig, macOS signing, or the native SDK addon, or carries the ci:macos label. Otherwise the macOS arm64 check passes without a macOS runner. - Remove the four-platform Full CI workflow and the separate cross-target compile jobs. - Smoke-test the Linux arm64, macOS x86_64, and macOS arm64 binaries in Binary Size, and reuse the base build for later pushes to the same pull request. - Build the SDK WASM surfaces and Node-API addons once and share them across the SDK test jobs. - Run PGSO on pull requests only when the PGSO pipeline changes, and keep release PGSO evidence for 90 days. - Run the long-turn memory, libfx runtime, and live ACP jobs only on request. - Limit libfx publishing to an archive check, an install smoke test, and a registry byte comparison. - Add a warning when an E2E file passes only on retry. - Remove the unused Prepare Release workflow and document the new CI rules.
333 lines
13 KiB
YAML
333 lines
13 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch:
|
|
inputs:
|
|
validate_only:
|
|
description: Build all platforms and compare arm64 signing pages without publishing
|
|
type: boolean
|
|
default: false
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
concurrency:
|
|
group: stable-release
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
check-version:
|
|
if: github.ref == 'refs/heads/main'
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
needed: ${{ steps.check.outputs.needed }}
|
|
publish: ${{ steps.check.outputs.publish }}
|
|
version: ${{ steps.check.outputs.version }}
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Check if release needed
|
|
id: check
|
|
env:
|
|
VALIDATE_ONLY: ${{ github.event_name == 'workflow_dispatch' && inputs.validate_only }}
|
|
run: |
|
|
VERSION=$(grep 'pub const version' src/main.zig | head -1 | sed 's/.*"\(.*\)".*/\1/')
|
|
if ! [[ "$VERSION" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
|
echo "::error::src/main.zig version is not strict SemVer"
|
|
exit 1
|
|
fi
|
|
TAG="v${VERSION}"
|
|
echo "version=${TAG}" >> "$GITHUB_OUTPUT"
|
|
if [[ "$VALIDATE_ONLY" == true ]]; then
|
|
echo "needed=true" >> "$GITHUB_OUTPUT"
|
|
echo "publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Validating release artifacts without publishing"
|
|
elif git rev-parse "$TAG" >/dev/null 2>&1; then
|
|
echo "needed=false" >> "$GITHUB_OUTPUT"
|
|
echo "publish=false" >> "$GITHUB_OUTPUT"
|
|
echo "Tag ${TAG} exists, no release needed"
|
|
else
|
|
echo "needed=true" >> "$GITHUB_OUTPUT"
|
|
echo "publish=true" >> "$GITHUB_OUTPUT"
|
|
echo "Tag ${TAG} missing, release needed"
|
|
fi
|
|
|
|
build-linux:
|
|
needs: check-version
|
|
if: needs.check-version.outputs.needed == 'true'
|
|
name: Build ${{ matrix.name }}
|
|
runs-on: ubuntu-latest
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: linux-x86_64
|
|
target: x86_64-linux
|
|
- name: linux-aarch64
|
|
target: aarch64-linux
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
|
|
- name: Setup Zig
|
|
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
|
with:
|
|
version: "0.16.0"
|
|
|
|
- name: Build
|
|
run: zig build -Doptimize=ReleaseSafe -Dtarget=${{ matrix.target }}
|
|
|
|
- name: Package
|
|
run: |
|
|
python3 -c 'import pathlib; n = pathlib.Path("zig-out/bin/fx").stat().st_size; print(f"linux executable: {n:,} bytes ({n / 1048576:.6f} MiB)")' >> "$GITHUB_STEP_SUMMARY"
|
|
package_dir="$RUNNER_TEMP/fx-package-${{ matrix.name }}"
|
|
mkdir -p "$package_dir"
|
|
cp zig-out/bin/fx LICENSE THIRD_PARTY_NOTICES.md "$package_dir/"
|
|
tar -czf fx-${{ matrix.name }}.tar.gz -C "$package_dir" fx LICENSE THIRD_PARTY_NOTICES.md
|
|
sha256sum fx-${{ matrix.name }}.tar.gz > fx-${{ matrix.name }}.tar.gz.sha256
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: fx-${{ matrix.name }}
|
|
path: |
|
|
fx-${{ matrix.name }}.tar.gz
|
|
fx-${{ matrix.name }}.tar.gz.sha256
|
|
|
|
build-macos-x86_64:
|
|
needs: check-version
|
|
if: needs.check-version.outputs.needed == 'true'
|
|
name: Build macos-x86_64
|
|
runs-on: macos-15-intel
|
|
environment: apple-signing
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
|
|
- name: Setup Zig
|
|
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
|
|
with:
|
|
version: "0.16.0"
|
|
|
|
- name: Build
|
|
run: zig build -Doptimize=ReleaseSafe -Dtarget=x86_64-macos
|
|
|
|
- name: Sign and notarize
|
|
env:
|
|
APPLE_DEVELOPER_ID_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
|
|
APPLE_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
|
|
APPLE_NOTARY_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }}
|
|
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
run: scripts/sign-and-notarize-macos.sh zig-out/bin/fx
|
|
|
|
- name: Verify notarized Intel binary
|
|
if: github.event_name == 'workflow_dispatch' && inputs.validate_only
|
|
run: |
|
|
codesign --verify --strict --check-notarization -R=notarized ./zig-out/bin/fx
|
|
./zig-out/bin/fx --version
|
|
./zig-out/bin/fx help
|
|
|
|
- name: Package
|
|
run: |
|
|
python3 -c 'import pathlib; n = pathlib.Path("zig-out/bin/fx").stat().st_size; print(f"macOS Intel signed executable: {n:,} bytes ({n / 1048576:.6f} MiB)")' >> "$GITHUB_STEP_SUMMARY"
|
|
package_dir="$RUNNER_TEMP/fx-package-macos-x86_64"
|
|
mkdir -p "$package_dir"
|
|
cp zig-out/bin/fx LICENSE THIRD_PARTY_NOTICES.md "$package_dir/"
|
|
tar -czf fx-macos-x86_64.tar.gz -C "$package_dir" fx LICENSE THIRD_PARTY_NOTICES.md
|
|
shasum -a 256 fx-macos-x86_64.tar.gz > fx-macos-x86_64.tar.gz.sha256
|
|
|
|
- name: Upload artifact
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: fx-macos-x86_64
|
|
path: |
|
|
fx-macos-x86_64.tar.gz
|
|
fx-macos-x86_64.tar.gz.sha256
|
|
|
|
build-macos-arm64:
|
|
needs: check-version
|
|
if: needs.check-version.outputs.needed == 'true'
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
uses: ./.github/workflows/pgso-macos-arm64.yml
|
|
with:
|
|
# Keep release evidence long enough to serve as the main control for
|
|
# manual PGSO comparisons.
|
|
evidence-retention-days: 90
|
|
|
|
sign-macos-arm64:
|
|
needs: [check-version, build-macos-arm64]
|
|
if: needs.check-version.outputs.needed == 'true'
|
|
name: Sign macos-arm64
|
|
runs-on: macos-15
|
|
environment: apple-signing
|
|
timeout-minutes: 20
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
|
|
- name: Require native arm64 host
|
|
run: test "$(uname -m)" = arm64
|
|
|
|
- name: Download eligible candidate
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
artifact-ids: ${{ needs.build-macos-arm64.outputs.artifact-id }}
|
|
merge-multiple: true
|
|
path: ${{ runner.temp }}/fx-pgso-aggregate
|
|
|
|
- name: Restore executable mode
|
|
run: chmod +x "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx"
|
|
|
|
- name: Confirm release eligibility
|
|
run: python3 -m scripts.pgso report --output-dir "$RUNNER_TEMP/fx-pgso-aggregate" >/dev/null
|
|
|
|
- name: Sign and notarize stable release candidate
|
|
env:
|
|
VALIDATE_ONLY: ${{ github.event_name == 'workflow_dispatch' && inputs.validate_only }}
|
|
APPLE_DEVELOPER_ID_P12_BASE64: ${{ secrets.APPLE_DEVELOPER_ID_P12_BASE64 }}
|
|
APPLE_DEVELOPER_ID_P12_PASSWORD: ${{ secrets.APPLE_DEVELOPER_ID_P12_PASSWORD }}
|
|
APPLE_NOTARY_KEY_P8_BASE64: ${{ secrets.APPLE_NOTARY_KEY_P8_BASE64 }}
|
|
APPLE_NOTARY_KEY_ID: ${{ secrets.APPLE_NOTARY_KEY_ID }}
|
|
APPLE_NOTARY_ISSUER_ID: ${{ secrets.APPLE_NOTARY_ISSUER_ID }}
|
|
run: |
|
|
if [[ "$VALIDATE_ONLY" == true ]]; then
|
|
control_dir="$RUNNER_TEMP/fx-signing-control"
|
|
mkdir -p "$control_dir"
|
|
cp "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx" "$control_dir/fx"
|
|
scripts/sign-and-notarize-macos.sh "$control_dir/fx" 4096
|
|
scripts/sign-and-notarize-macos.sh "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx" 16384
|
|
else
|
|
scripts/sign-and-notarize-macos.sh "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx"
|
|
fi
|
|
|
|
- name: Verify notarized arm64 binaries
|
|
if: github.event_name == 'workflow_dispatch' && inputs.validate_only
|
|
run: |
|
|
for binary in "$RUNNER_TEMP/fx-signing-control/fx" "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx"; do
|
|
codesign --verify --strict --check-notarization -R=notarized "$binary"
|
|
"$binary" --version
|
|
"$binary" help
|
|
python3 -c 'import pathlib, sys; n = pathlib.Path(sys.argv[1]).stat().st_size; print(f"{sys.argv[1]}: {n:,} bytes ({n / 1048576:.6f} MiB)")' "$binary" >> "$GITHUB_STEP_SUMMARY"
|
|
shasum -a 256 "$binary" >> "$GITHUB_STEP_SUMMARY"
|
|
done
|
|
tar -czf "$RUNNER_TEMP/fx-signing-control.tar.gz" -C "$RUNNER_TEMP/fx-signing-control" fx
|
|
|
|
- name: Upload arm64 signing control
|
|
if: github.event_name == 'workflow_dispatch' && inputs.validate_only
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: signing-control-macos-arm64
|
|
path: ${{ runner.temp }}/fx-signing-control.tar.gz
|
|
if-no-files-found: error
|
|
|
|
- name: Package stable release candidate
|
|
run: |
|
|
archive="$RUNNER_TEMP/fx-macos-aarch64.tar.gz"
|
|
package_dir="$RUNNER_TEMP/fx-release-package"
|
|
mkdir -p "$package_dir"
|
|
cp "$RUNNER_TEMP/fx-pgso-aggregate/candidate/fx" LICENSE THIRD_PARTY_NOTICES.md "$package_dir/"
|
|
tar -czf "$archive" -C "$package_dir" fx LICENSE THIRD_PARTY_NOTICES.md
|
|
cd "$RUNNER_TEMP"
|
|
shasum -a 256 fx-macos-aarch64.tar.gz > fx-macos-aarch64.tar.gz.sha256
|
|
|
|
- name: Upload stable release artifact
|
|
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
|
with:
|
|
name: fx-macos-aarch64
|
|
path: |
|
|
${{ runner.temp }}/fx-macos-aarch64.tar.gz
|
|
${{ runner.temp }}/fx-macos-aarch64.tar.gz.sha256
|
|
if-no-files-found: error
|
|
|
|
release:
|
|
needs: [check-version, build-linux, build-macos-x86_64, sign-macos-arm64]
|
|
if: needs.check-version.outputs.publish == 'true' && !(github.event_name == 'workflow_dispatch' && inputs.validate_only)
|
|
runs-on: ubuntu-latest
|
|
environment: release
|
|
permissions:
|
|
contents: write
|
|
steps:
|
|
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
|
|
|
|
- name: Download artifacts
|
|
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
|
|
with:
|
|
pattern: fx-*
|
|
merge-multiple: true
|
|
|
|
- name: Create git tag
|
|
env:
|
|
RELEASE_TAG: ${{ needs.check-version.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$RELEASE_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
|
|
git tag -- "$RELEASE_TAG"
|
|
git push origin "refs/tags/${RELEASE_TAG}"
|
|
|
|
- name: Extract changelog entry
|
|
run: |
|
|
awk '/<!-- release:start -->/{found=1; next} /<!-- release:end -->/{found=0} found{print}' CHANGELOG.md > /tmp/release-notes.md
|
|
LINES=$(wc -l < /tmp/release-notes.md | tr -d ' ')
|
|
if [ "$LINES" -lt 2 ]; then
|
|
echo "Error: No release notes found between <!-- release:start --> and <!-- release:end --> markers in CHANGELOG.md"
|
|
exit 1
|
|
fi
|
|
echo "Extracted release notes ($LINES lines)"
|
|
|
|
- name: Create GitHub Release
|
|
uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2
|
|
with:
|
|
tag_name: ${{ needs.check-version.outputs.version }}
|
|
body_path: /tmp/release-notes.md
|
|
files: |
|
|
fx-*.tar.gz
|
|
fx-*.tar.gz.sha256
|
|
|
|
- name: Publish to CDN
|
|
env:
|
|
BLOB_READ_WRITE_TOKEN: ${{ secrets.BLOB_READ_WRITE_TOKEN }}
|
|
VERSION: ${{ needs.check-version.outputs.version }}
|
|
run: |
|
|
set -euo pipefail
|
|
[[ "$VERSION" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]
|
|
CDN_BASE="cli"
|
|
|
|
for FILE in fx-*.tar.gz fx-*.tar.gz.sha256; do
|
|
[ -f "$FILE" ] || continue
|
|
CONTENT_TYPE="application/octet-stream"
|
|
[[ "$FILE" == *.sha256 ]] && CONTENT_TYPE="text/plain"
|
|
[[ "$FILE" == *.tar.gz ]] && [[ "$FILE" != *.sha256 ]] && CONTENT_TYPE="application/gzip"
|
|
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X PUT "https://blob.vercel-storage.com/${CDN_BASE}/${VERSION}/${FILE}" \
|
|
-H "Authorization: Bearer ${BLOB_READ_WRITE_TOKEN}" \
|
|
-H "x-api-version: 7" \
|
|
-H "x-content-type: ${CONTENT_TYPE}" \
|
|
-H "x-add-random-suffix: 0" \
|
|
-H "x-cache-control-max-age: 31536000" \
|
|
--data-binary "@${FILE}")
|
|
echo "Uploaded ${FILE} -> ${CDN_BASE}/${VERSION}/${FILE} ($HTTP_CODE)"
|
|
if [ "$HTTP_CODE" -lt 200 ] || [ "$HTTP_CODE" -ge 300 ]; then
|
|
echo "::error::Failed to upload ${FILE} to CDN ($HTTP_CODE)"
|
|
exit 1
|
|
fi
|
|
done
|
|
|
|
echo -n "$VERSION" > /tmp/latest.txt
|
|
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" \
|
|
-X PUT "https://blob.vercel-storage.com/${CDN_BASE}/latest.txt" \
|
|
-H "Authorization: Bearer ${BLOB_READ_WRITE_TOKEN}" \
|
|
-H "x-api-version: 7" \
|
|
-H "x-content-type: text/plain" \
|
|
-H "x-add-random-suffix: 0" \
|
|
-H "x-cache-control-max-age: 60" \
|
|
--data-binary "@/tmp/latest.txt")
|
|
echo "Updated latest.txt to $VERSION ($HTTP_CODE)"
|