Merge pull request #50 from nynrathod/0.4.2 #13
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ────────────────────────────────────────────────────────────────────────────── | |
| # 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 |