Skip to content
Open
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
118 changes: 35 additions & 83 deletions .github/workflows/release-artifacts.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ jobs:
name: Build ${{ matrix.target }}
needs: prepare
runs-on: ${{ matrix.os }}
environment: ${{ contains(matrix.target, 'pc-windows') && 'release' || '' }}
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -134,94 +138,33 @@ jobs:
--target=${{ matrix.target }}
${{ steps.options.outputs.cargo-build-options }}

- name: Package binary
id: package
run: pixi run -e release package-binary --target ${{ matrix.target }}

- name: Upload archive artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: ${{ steps.package.outputs.prefix }}-${{ steps.package.outputs.pkg-name }}
path: staging/

# macOS code signing and notarization
sign-macos:
name: Sign macOS binaries
needs: build-binaries
runs-on: macos-latest
strategy:
matrix:
target:
- x86_64-apple-darwin
- aarch64-apple-darwin
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: prefix-dev/setup-pixi@1b2de7f3351f171c8b4dfeb558c639cb58ed4ec0 # v0.9.5
with:
environments: release
- name: Download macOS artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-rattler-build-${{ matrix.target }}.zip
path: artifacts/
- name: Sign and notarize
- name: Codesign macOS binary
if: contains(matrix.target, 'apple-darwin')
env:
CODESIGN_CERTIFICATE: ${{ secrets.CODESIGN_CERTIFICATE }}
CODESIGN_CERTIFICATE_PASSWORD: ${{ secrets.CODESIGN_CERTIFICATE_PASSWORD }}
CODESIGN_IDENTITY: ${{ vars.CODESIGN_IDENTITY }}
APPLEID_USERNAME: ${{ secrets.APPLEID_USERNAME }}
APPLEID_PASSWORD: ${{ secrets.APPLEID_PASSWORD }}
APPLEID_TEAMID: ${{ vars.APPLEID_TEAMID }}
run: pixi run -e release sign-macos --artifacts-dir artifacts/
- name: Upload signed artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: release-rattler-build-${{ matrix.target }}.zip
path: artifacts/
run: >-
pixi run -e release sign-macos
--binary target/${{ matrix.target }}/release/rattler-build

# Windows code signing via Azure Trusted Signing
sign-windows:
name: Sign Windows binaries
needs: build-binaries
runs-on: windows-latest
environment: release
permissions:
contents: read
id-token: write
strategy:
matrix:
include:
- target: x86_64-pc-windows-msvc
- target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Configure pixi for win-arm64
if: matrix.target == 'aarch64-pc-windows-msvc'
shell: bash
run: |
mkdir -p ~/.pixi
echo 'tool-platform = "win-64"' > ~/.pixi/config.toml
- uses: prefix-dev/setup-pixi@1b2de7f3351f171c8b4dfeb558c639cb58ed4ec0 # v0.9.5
with:
environments: release
- name: Download Windows artifact
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: build-rattler-build-${{ matrix.target }}.zip
path: artifacts/
- name: Azure login (OIDC)
if: contains(matrix.target, 'pc-windows')
uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0
with:
client-id: ${{ vars.AZURE_CLIENT_ID }}
tenant-id: ${{ vars.AZURE_TENANT_ID }}
allow-no-subscriptions: true
- name: Extract executables for signing
run: pixi run -e release sign-windows-extract --artifacts-dir artifacts/ --sign-dir ${{ runner.temp }}/to-sign

- name: Stage Windows binary for signing
if: contains(matrix.target, 'pc-windows')
shell: bash
run: |
mkdir -p "${RUNNER_TEMP}/to-sign"
cp "target/${{ matrix.target }}/release/rattler-build.exe" "${RUNNER_TEMP}/to-sign/"

- name: Sign with Azure Trusted Signing
if: contains(matrix.target, 'pc-windows')
uses: azure/trusted-signing-action@b443cf8ea4124818d2ea9f043cba29fc3ec47b16 # v1.2.0
with:
endpoint: ${{ vars.AZURE_SIGNING_ENDPOINT }}
Expand All @@ -232,13 +175,22 @@ jobs:
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Repackage signed binaries
run: pixi run -e release sign-windows-repackage --artifacts-dir artifacts/ --sign-dir ${{ runner.temp }}/to-sign
- name: Upload signed artifact

- name: Copy signed Windows binary back
if: contains(matrix.target, 'pc-windows')
shell: bash
run: |
cp "${RUNNER_TEMP}/to-sign/rattler-build.exe" "target/${{ matrix.target }}/release/rattler-build.exe"

- name: Package binary
id: package
run: pixi run -e release package-binary --target ${{ matrix.target }}

- name: Upload archive artifact
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: release-rattler-build-${{ matrix.target }}.zip
path: artifacts/
name: release-rattler-build-${{ matrix.target }}
path: staging/

# Build Python wheels (reusable workflow)
build-python-wheels:
Expand All @@ -252,14 +204,14 @@ jobs:
# PHASE 2: Publish (only after ALL Phase 1 jobs succeed)
# ============================================================

# Create git tag only after all builds/signing/attestation/dry-runs pass
# Create git tag only after all builds pass
tag-release:
name: Tag release
if: >-
github.event_name == 'workflow_dispatch' &&
!inputs.dry-run &&
github.ref == 'refs/heads/main'
needs: [prepare, sign-macos, sign-windows, build-python-wheels]
needs: [prepare, build-binaries, build-python-wheels]
runs-on: ubuntu-latest
permissions:
contents: write
Expand Down
4 changes: 1 addition & 3 deletions pixi.toml
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,7 @@ release-plz-release = { cmd = "python scripts/release_plz_release.py", descripti
build-options = { cmd = "python scripts/build_options.py", description = "Determine cargo build/test options for a target" }
package-binary = { cmd = "python scripts/package_binary.py", description = "Package release binary into archive and stage for upload" }
create-tag = { cmd = "python scripts/create_tag.py", description = "Create and push a git tag idempotently" }
sign-macos = { cmd = "python scripts/sign_macos.py", description = "Sign and notarize macOS binaries" }
sign-windows-extract = { cmd = "python scripts/sign_windows.py extract", description = "Extract Windows executables for signing" }
sign-windows-repackage = { cmd = "python scripts/sign_windows.py repackage", description = "Repackage signed Windows executables" }
sign-macos = { cmd = "python scripts/sign_macos.py", description = "Codesign a macOS binary in place" }
save-attestation = { cmd = "python scripts/save_attestation.py", description = "Save attestation bundle" }
create-release = { cmd = "python scripts/create_release.py", description = "Create GitHub release with all artifacts" }

Expand Down
10 changes: 10 additions & 0 deletions scripts/create_release.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ def main() -> None:
tag: str = args.tag
assets_dir: Path = args.assets_dir

# Aggregate all per-asset .sha256 files into a single sha256.sum
aggregate = assets_dir / "sha256.sum"
checksum_files = sorted(f for f in assets_dir.glob("*.sha256") if f.is_file())
if checksum_files:
lines = []
for cf in checksum_files:
lines.append(cf.read_text().strip())
aggregate.write_text("\n".join(lines) + "\n")
print(f"Wrote {aggregate.name} with {len(checksum_files)} entries")

# Collect asset files
assets = sorted(f for f in assets_dir.iterdir() if f.is_file())
if not assets:
Expand Down
55 changes: 25 additions & 30 deletions scripts/package_binary.py
Original file line number Diff line number Diff line change
@@ -1,34 +1,38 @@
"""Package a release binary into a tarball/zip and stage artifacts for upload.

The binary at `target/<target>/release/rattler-build[.exe]` is expected to
already be codesigned (macOS) or Azure-signed (Windows) when this script runs.

Creates:
staging/<archive> - .tar.gz (unix) or .zip (windows) with binary + README + LICENSE
staging/<binary> - raw binary named rattler-build-<target>[.exe]
staging/<archive> - .zip (windows) or .tar.gz (linux/macos) with binary + README + LICENSE
staging/<binary> - raw binary named rattler-build-<target>[.exe]
staging/<archive>.sha256 - sha256 of the archive
staging/<binary>.sha256 - sha256 of the raw binary

Outputs:
pkg-name - archive filename (e.g. rattler-build-x86_64-unknown-linux-musl.tar.gz)
prefix - artifact prefix: "release" for Linux, "build" for macOS/Windows (needs signing)

Usage:
pixi run -e release package-binary --target x86_64-unknown-linux-musl
"""

import argparse
import hashlib
import os
import shutil
import subprocess
import tarfile
import zipfile
from pathlib import Path

ROOT = Path(__file__).resolve().parent.parent


def is_linux_target(target: str) -> bool:
return "linux" in target


def needs_signing(target: str) -> bool:
return "apple-darwin" in target or "pc-windows" in target
def write_sha256(path: Path) -> None:
h = hashlib.sha256()
with open(path, "rb") as f:
for chunk in iter(lambda: f.read(1024 * 1024), b""):
h.update(chunk)
(path.parent / f"{path.name}.sha256").write_text(f"{h.hexdigest()} {path.name}\n")


def main() -> None:
Expand All @@ -38,14 +42,12 @@ def main() -> None:

target: str = args.target
windows = "pc-windows" in target
linux = is_linux_target(target)
ext = ".exe" if windows else ""
archive_ext = ".tar.gz" if linux else ".zip"
archive_ext = ".zip" if windows else ".tar.gz"

pkg_basename = f"rattler-build-{target}"
pkg_name = f"{pkg_basename}{archive_ext}"

# Create archive directory with binary + docs
archive_dir = ROOT / "pkg" / pkg_basename
archive_dir.mkdir(parents=True, exist_ok=True)

Expand All @@ -54,44 +56,37 @@ def main() -> None:
shutil.copy2(ROOT / "README.md", archive_dir / "README.md")
shutil.copy2(ROOT / "LICENSE", archive_dir / "LICENSE")

# Create archive
archive_path = ROOT / "pkg" / pkg_name
if linux:
with tarfile.open(archive_path, "w:gz") as tf:
for item in sorted(archive_dir.iterdir()):
tf.add(item, arcname=f"{pkg_basename}/{item.name}")
elif windows:
# Use 7z on Windows for zip creation (handles paths better)
if windows:
subprocess.run(
["7z", "-y", "a", str(archive_path), f"{pkg_basename}/*"],
check=True,
cwd=ROOT / "pkg",
)
else:
with zipfile.ZipFile(archive_path, "w", zipfile.ZIP_DEFLATED) as zf:
with tarfile.open(archive_path, "w:gz") as tf:
for item in sorted(archive_dir.iterdir()):
zf.write(item, f"{pkg_basename}/{item.name}")
tf.add(item, arcname=f"{pkg_basename}/{item.name}")

# Stage everything flat for upload
staging = ROOT / "staging"
staging.mkdir(parents=True, exist_ok=True)
shutil.copy2(archive_path, staging / pkg_name)

binary_name = f"rattler-build-{target}{ext}"
shutil.copy2(binary_src, staging / binary_name)
staged_archive = staging / pkg_name
shutil.copy2(archive_path, staged_archive)
write_sha256(staged_archive)

# Determine artifact prefix
prefix = "build" if needs_signing(target) else "release"
binary_name = f"rattler-build-{target}{ext}"
staged_binary = staging / binary_name
shutil.copy2(binary_src, staged_binary)
write_sha256(staged_binary)

print(f"Archive: {pkg_name}")
print(f"Binary: {binary_name}")
print(f"Prefix: {prefix}")

github_output = os.environ.get("GITHUB_OUTPUT")
if github_output:
with open(github_output, "a") as f:
f.write(f"pkg-name={pkg_name}\n")
f.write(f"prefix={prefix}\n")


if __name__ == "__main__":
Expand Down
Loading
Loading