Skip to content

Merge pull request #50 from nynrathod/0.4.2 #13

Merge pull request #50 from nynrathod/0.4.2

Merge pull request #50 from nynrathod/0.4.2 #13

# ──────────────────────────────────────────────────────────────────────────────
# Stable Release Pipeline (Linux · macOS Intel · macOS Apple Silicon · Windows)
#
# Triggered: every push to main.
#
# What happens automatically (you just merge/push to main):
# 1. Matrix build: compile native binaries on ubuntu / macos-15-intel (Intel) /
# macos-14 (Apple Silicon) / windows-latest — all using LLVM 18.
# 2. Each platform zips its own bundle with a consistent name:
# doo-linux-{version}.zip
# doo-darwin-x86_64-{version}.zip
# doo-darwin-arm64-{version}.zip
# doo-windows-{version}.zip
# 3. All zip artifacts are uploaded as workflow artifacts.
# 4. A single "publish" job (needs all builders) collects every zip, deletes
# the dev pre-release, creates the stable GitHub release, and uploads all
# four zips in one transactional step.
# 5. The "latest" GHCR image (:latest + :vX.Y.Z) is built from the Linux
# bundle in the same publish job.
# 6. The dev GHCR tag is deleted to keep things clean.
#
# Zip naming convention (single source of truth):
# doo-<platform>-<version>.zip
# platform values: linux | darwin-x86_64 | darwin-arm64 | windows
#
# Cache strategy:
# - Rust/Cargo deps → Swatinem/rust-cache (all platforms, auto key from Cargo.lock)
# - LLVM 18 → actions/cache (all platforms, key includes LLVM version)
# - vcpkg libxml2 → actions/cache (Windows only, key is static)
# Cache keys include a version string (e.g. llvm-18.1.8) so bumping
# LLVM automatically busts the cache — no manual cleanup needed.
# ──────────────────────────────────────────────────────────────────────────────
name: Stable Release
on:
push:
branches:
- main
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/doo-runtime
LLVM_VERSION: "18"
# llvm-sys crate env var name encodes major+minor: 18.1 → 181
LLVM_SYS_ENV: "LLVM_SYS_181_PREFIX"
CARGO_INCREMENTAL: 0
CARGO_TERM_COLOR: always
# ── Matrix build jobs ─────────────────────────────────────────────────────────
jobs:
build:
name: Build · ${{ matrix.label }}
runs-on: ${{ matrix.os }}
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
# ── Linux (x86_64) ─────────────────────────────────────────────────
- os: ubuntu-latest
label: linux
platform: linux
rust_target: x86_64-unknown-linux-gnu
# ── macOS Intel (x86_64) ───────────────────────────────────────────
- os: macos-15-intel
label: macOS-intel
platform: darwin-x86_64
rust_target: x86_64-apple-darwin
# ── macOS Apple Silicon (arm64) ────────────────────────────────────
- os: macos-14
label: macOS-arm64
platform: darwin-arm64
rust_target: aarch64-apple-darwin
# ── Windows (x86_64) ───────────────────────────────────────────────
- os: windows-latest
label: windows
platform: windows
rust_target: x86_64-pc-windows-msvc
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── Read version (shared across all platforms) ──────────────────────────
- name: Read version
id: version
shell: bash
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/' | tr -d '\r')
ZIP_NAME="doo-${{ matrix.platform }}-${VERSION}.zip"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "zip_name=${ZIP_NAME}" >> $GITHUB_OUTPUT
echo "Version: ${VERSION} Zip: ${ZIP_NAME}"
# ── Install Rust ────────────────────────────────────────────────────────
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
toolchain: stable
targets: ${{ matrix.rust_target }}
# ── Rust + Cargo cache ──────────────────────────────────────────────────
# Key is per-platform + LLVM version. Swatinem appends a hash of
# Cargo.lock internally, so changing dependencies auto-busts this cache.
# "Only showing restore" in logs = cache hit, nothing changed = correct.
- name: Rust cache
uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.platform }}-llvm18
# ════════════════════════════════════════════════════════════════════════
# LINUX — LLVM 18 via apt.llvm.org
# Cache: /usr/lib/llvm-18 (headers + libs + llvm-config all live here)
# Key never changes unless you bump the version string → auto-invalidates.
# ════════════════════════════════════════════════════════════════════════
- name: Cache LLVM 18 (Linux)
if: matrix.os == 'ubuntu-latest'
id: cache-llvm-linux
uses: actions/cache@v4
with:
path: /usr/lib/llvm-18
key: llvm-18-linux-apt-v1
- name: "[Linux] Install LLVM 18"
if: matrix.os == 'ubuntu-latest' && steps.cache-llvm-linux.outputs.cache-hit != 'true'
run: |
wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key \
| sudo tee /etc/apt/trusted.gpg.d/apt.llvm.org.asc
echo "deb http://apt.llvm.org/$(lsb_release -cs)/ llvm-toolchain-$(lsb_release -cs)-18 main" \
| sudo tee /etc/apt/sources.list.d/llvm18.list
sudo apt-get update -qq
sudo apt-get install -y --no-install-recommends \
llvm-18-dev libpolly-18-dev libclang-18-dev clang-18 lld-18
# System deps (fast, from default Ubuntu repo — always run) + set env
- name: "[Linux] Install system deps + set env"
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt-get install -y --no-install-recommends \
libssl-dev libpq-dev zlib1g-dev pkg-config
echo "${{ env.LLVM_SYS_ENV }}=/usr/lib/llvm-18" >> $GITHUB_ENV
# ════════════════════════════════════════════════════════════════════════
# macOS — LLVM 18 via Homebrew
# Cache: /tmp/llvm18 (brew prefix copied here with symlinks resolved)
# Separate keys for Intel vs ARM so caches never cross-contaminate.
# ════════════════════════════════════════════════════════════════════════
- name: Cache LLVM 18 (macOS)
if: runner.os == 'macOS'
id: cache-llvm-mac
uses: actions/cache@v4
with:
path: /tmp/llvm18
key: llvm-18-${{ runner.os }}-${{ runner.arch }}-v1
- name: "[macOS] Install LLVM 18 via Homebrew"
if: runner.os == 'macOS' && steps.cache-llvm-mac.outputs.cache-hit != 'true'
run: |
brew install llvm@18
# Copy to a stable fixed path (resolves brew symlinks with -L)
cp -rL "$(brew --prefix llvm@18)" /tmp/llvm18
# Always set env — whether we just installed or restored from cache
- name: "[macOS] Set LLVM env"
if: runner.os == 'macOS'
run: |
echo "LLVM_SYS_181_PREFIX=/tmp/llvm18" >> $GITHUB_ENV
echo "LIBCLANG_PATH=/tmp/llvm18/lib" >> $GITHUB_ENV
echo "/tmp/llvm18/bin" >> $GITHUB_PATH
# ════════════════════════════════════════════════════════════════════════
# Windows — LLVM 18 from official clang+llvm tarball
# Cache: C:\llvm18 (extracted tarball, includes llvm-config.exe)
# Key includes exact version so bumping 18.1.8 → 18.1.9 auto-busts.
# ════════════════════════════════════════════════════════════════════════
- name: Cache LLVM 18 (Windows)
if: runner.os == 'Windows'
id: cache-llvm-win
uses: actions/cache@v4
with:
path: C:\llvm18
key: llvm-18.1.8-windows-x64-v1
- name: "[Windows] Download and extract LLVM 18"
if: runner.os == 'Windows' && steps.cache-llvm-win.outputs.cache-hit != 'true'
shell: pwsh
run: |
$url = "https://github.com/llvm/llvm-project/releases/download/llvmorg-18.1.8/clang+llvm-18.1.8-x86_64-pc-windows-msvc.tar.xz"
$tarXz = "$env:TEMP\llvm18.tar.xz"
$tar = "$env:TEMP\llvm18.tar"
Write-Host "Downloading LLVM 18.1.8 dev tarball..."
Invoke-WebRequest -Uri $url -OutFile $tarXz -UseBasicParsing
Write-Host "Extracting..."
7z e $tarXz -o"$env:TEMP" -y | Out-Null
7z x $tar -o"C:\llvm18" -y | Out-Null
Write-Host "LLVM extracted to C:\llvm18"
# Always set env + copy lld-link — whether from cache or fresh install
- name: "[Windows] Set LLVM env"
if: runner.os == 'Windows'
shell: pwsh
run: |
$llvmDir = (Get-ChildItem "C:\llvm18" -Directory | Select-Object -First 1).FullName
Write-Host "LLVM dir: $llvmDir"
echo "LLVM_SYS_181_PREFIX=$llvmDir" >> $env:GITHUB_ENV
echo "LIBCLANG_PATH=$llvmDir\bin" >> $env:GITHUB_ENV
echo "$llvmDir\bin" >> $env:GITHUB_PATH
# lld-link.exe embedded at compile time
New-Item -ItemType Directory -Path "linkers" -Force | Out-Null
Copy-Item "$llvmDir\bin\lld-link.exe" "linkers\lld-link.exe" -Force
# ════════════════════════════════════════════════════════════════════════
# Windows — libxml2 (required by LLVM 18, not bundled in tarball)
# Cache: C:\vcpkg\installed (vcpkg build output)
# ════════════════════════════════════════════════════════════════════════
- name: Cache vcpkg libxml2 (Windows)
if: runner.os == 'Windows'
id: cache-vcpkg-win
uses: actions/cache@v4
with:
path: C:\vcpkg\installed
key: vcpkg-libxml2-x64-windows-static-v1
- name: "[Windows] Install libxml2 via vcpkg"
if: runner.os == 'Windows' && steps.cache-vcpkg-win.outputs.cache-hit != 'true'
shell: pwsh
run: vcpkg install libxml2:x64-windows-static
# Always set LIB + create libxml2s.lib alias — whether from cache or fresh
- name: "[Windows] Set libxml2 env"
if: runner.os == 'Windows'
shell: pwsh
run: |
$vcpkgLib = "C:\vcpkg\installed\x64-windows-static\lib"
# LLVM 18 linker asks for libxml2s.lib (static naming convention)
# vcpkg produces libxml2.lib — create the alias
Copy-Item "$vcpkgLib\libxml2.lib" "$vcpkgLib\libxml2s.lib" -Force
echo "LIB=$env:LIB;$vcpkgLib" >> $env:GITHUB_ENV
# ── Build ───────────────────────────────────────────────────────────────
- name: Build release binary
run: cargo build --release --workspace
# ════════════════════════════════════════════════════════════════════════
# Assemble platform bundles
# Naming: doo-<platform>-<version>.zip
# ════════════════════════════════════════════════════════════════════════
# ── Linux bundle ────────────────────────────────────────────────────────
- name: "[Linux] Assemble bundle"
if: matrix.os == 'ubuntu-latest'
run: |
BUNDLE="doo-linux-${{ steps.version.outputs.version }}"
mkdir -p "${BUNDLE}/lib" "${BUNDLE}/std" "${BUNDLE}/packages"
cp target/release/doo "${BUNDLE}/doo"
chmod +x "${BUNDLE}/doo"
find target/release -maxdepth 1 -name '*.a' -exec cp {} "${BUNDLE}/lib/" \; 2>/dev/null || true
[ -d std ] && cp -r std/. "${BUNDLE}/std/"
[ -d packages ] && cp -r packages/. "${BUNDLE}/packages/"
zip -r "${{ steps.version.outputs.zip_name }}" "${BUNDLE}"
echo "Bundle contents:"; ls -lh "${BUNDLE}/"
# ── macOS bundle (both Intel and ARM share identical shell logic) ────────
- name: "[macOS] Assemble bundle"
if: runner.os == 'macOS'
run: |
BUNDLE="doo-${{ matrix.platform }}-${{ steps.version.outputs.version }}"
mkdir -p "${BUNDLE}/lib" "${BUNDLE}/std" "${BUNDLE}/packages"
cp target/release/doo "${BUNDLE}/doo"
chmod +x "${BUNDLE}/doo"
find target/release -maxdepth 1 -name '*.a' -exec cp {} "${BUNDLE}/lib/" \; 2>/dev/null || true
find target/release -maxdepth 1 -name '*.dylib' -exec cp {} "${BUNDLE}/lib/" \; 2>/dev/null || true
[ -d std ] && cp -r std/. "${BUNDLE}/std/"
[ -d packages ] && cp -r packages/. "${BUNDLE}/packages/"
zip -r "${{ steps.version.outputs.zip_name }}" "${BUNDLE}"
echo "Bundle contents:"; ls -lh "${BUNDLE}/"
# ── Windows bundle ──────────────────────────────────────────────────────
- name: "[Windows] Assemble bundle"
if: runner.os == 'Windows'
shell: pwsh
run: |
$version = "${{ steps.version.outputs.version }}"
$bundle = "doo-windows-${version}"
$zipName = "${{ steps.version.outputs.zip_name }}"
New-Item -ItemType Directory -Path "${bundle}\lib","${bundle}\std","${bundle}\packages" -Force | Out-Null
Copy-Item "target\release\doo.exe" "${bundle}\doo.exe"
# Static import libs (.lib)
Get-ChildItem "target\release" -Filter "*.lib" -File |
Copy-Item -Destination "${bundle}\lib\" -ErrorAction SilentlyContinue
# Runtime DLLs (.dll) — FFI crates may produce these
Get-ChildItem "target\release" -Filter "*.dll" -File |
Copy-Item -Destination "${bundle}\lib\" -ErrorAction SilentlyContinue
# Stdlib + packages
if (Test-Path "std") { Copy-Item -Recurse "std\*" "${bundle}\std\" -Force }
if (Test-Path "packages") { Copy-Item -Recurse "packages\*" "${bundle}\packages\" -Force }
Compress-Archive -Path "${bundle}" -DestinationPath $zipName -Force
Write-Host "Bundle contents:"; Get-ChildItem $bundle | Format-Table Name, Length
# ── Upload zip as workflow artifact (consumed by the publish job) ────────
- name: Upload zip artifact
uses: actions/upload-artifact@v4
with:
name: doo-${{ matrix.platform }}-${{ steps.version.outputs.version }}
path: ${{ steps.version.outputs.zip_name }}
retention-days: 1
# ════════════════════════════════════════════════════════════════════════════
# Publish — runs once, after ALL platform builds succeed
# Downloads every zip, creates the stable release, uploads all assets,
# builds the GHCR image, and cleans up the dev pre-release.
# ════════════════════════════════════════════════════════════════════════════
publish:
name: Publish stable release
needs: build
runs-on: ubuntu-latest
permissions:
contents: write
packages: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
# ── Re-derive version (same grep, same result) ──────────────────────────
- name: Read version
id: version
run: |
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/' | tr -d '\r')
TAG="v${VERSION}"
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "linux_zip=doo-linux-${VERSION}.zip" >> $GITHUB_OUTPUT
echo "darwin_x86_zip=doo-darwin-x86_64-${VERSION}.zip" >> $GITHUB_OUTPUT
echo "darwin_arm_zip=doo-darwin-arm64-${VERSION}.zip" >> $GITHUB_OUTPUT
echo "windows_zip=doo-windows-${VERSION}.zip" >> $GITHUB_OUTPUT
echo "Version: ${VERSION} Tag: ${TAG}"
# ── Download all four platform zips ─────────────────────────────────────
- name: Download all platform zips
uses: actions/download-artifact@v4
with:
merge-multiple: true
# ── List what we have (sanity check) ────────────────────────────────────
- name: List downloaded assets
run: ls -lh *.zip
# ── Delete dev pre-release (clean slate) ────────────────────────────────
- name: Delete dev pre-release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release delete dev --yes --cleanup-tag 2>/dev/null || true
echo "dev pre-release cleaned up"
# ── Create stable GitHub release with all four zips ─────────────────────
- name: Create stable release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
TAG="${{ steps.version.outputs.tag }}"
REPO="${{ github.repository }}"
VERSION="${{ steps.version.outputs.version }}"
# Delete if same tag already exists (re-release same version)
gh release delete "${TAG}" --yes --cleanup-tag 2>/dev/null || true
sleep 2
printf '## Install\n\n**Linux**\n```bash\ncurl -fsSL https://raw.githubusercontent.com/%s/main/install.sh | bash\n```\n\n**macOS (Intel)**\n```bash\ncurl -fsSL https://raw.githubusercontent.com/%s/main/install.sh | bash\n```\n\n**macOS (Apple Silicon)**\n```bash\ncurl -fsSL https://raw.githubusercontent.com/%s/main/install.sh | bash\n```\n\n**Windows**\n```powershell\nirm https://raw.githubusercontent.com/%s/main/install.ps1 | iex\n```\n\n## Assets\n\n| Platform | File |\n|---|---|\n| Linux x86_64 | `doo-linux-%s.zip` |\n| macOS Intel | `doo-darwin-x86_64-%s.zip` |\n| macOS Apple Silicon | `doo-darwin-arm64-%s.zip` |\n| Windows x86_64 | `doo-windows-%s.zip` |\n' \
"$REPO" "$REPO" "$REPO" "$REPO" \
"$VERSION" "$VERSION" "$VERSION" "$VERSION" \
> /tmp/release_notes.md
gh release create "${TAG}" \
--latest \
--title "Doo ${VERSION}" \
--notes-file /tmp/release_notes.md \
--target ${{ github.sha }} \
"${{ steps.version.outputs.linux_zip }}" \
"${{ steps.version.outputs.darwin_x86_zip }}" \
"${{ steps.version.outputs.darwin_arm_zip }}" \
"${{ steps.version.outputs.windows_zip }}"
# ── Log in to GHCR ──────────────────────────────────────────────────────
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# ── Build and push GHCR :latest + :vX.Y.Z (uses Linux bundle) ───────────
- name: Build and push :latest image
uses: docker/build-push-action@v6
with:
context: .
push: true
build-args: |
DOO_LOCAL_BUNDLE=${{ steps.version.outputs.linux_zip }}
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.tag }}
# ── Delete :dev GHCR tag ─────────────────────────────────────────────────
- name: Delete :dev GHCR tag
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OWNER="${{ github.repository_owner }}"
PKG_NAME="doo-runtime"
VERSION_ID=$(gh api \
"/users/${OWNER}/packages/container/${PKG_NAME}/versions" \
--jq '.[] | select(.metadata.container.tags[] == "dev") | .id' 2>/dev/null || echo "")
if [ -n "$VERSION_ID" ]; then
gh api --method DELETE \
"/users/${OWNER}/packages/container/${PKG_NAME}/versions/${VERSION_ID}" 2>/dev/null || true
echo ":dev GHCR tag deleted (id=${VERSION_ID})"
else
echo ":dev GHCR tag not found — nothing to delete"
fi