name: Publish Release Assets
on:
push:
tags:
- "v*"
jobs:
validate-tag:
name: Validate release tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.resolve-version.outputs.version }}
is_prerelease: ${{ steps.resolve-version.outputs.is_prerelease }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Resolve release version
id: resolve-version
shell: bash
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
if [[ -z "${version}" ]]; then
echo "Failed to parse version from tag: ${GITHUB_REF_NAME}"
exit 1
fi
is_prerelease="true"
if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
is_prerelease="false"
fi
echo "version=${version}" >> "${GITHUB_OUTPUT}"
echo "is_prerelease=${is_prerelease}" >> "${GITHUB_OUTPUT}"
build-native:
name: Build native binary for ${{ matrix.platform.target }}
needs: validate-tag
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
binary: asfml
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
binary: asfml
- os: macos-15-intel
target: x86_64-apple-darwin
binary: asfml
- os: macos-14
target: aarch64-apple-darwin
binary: asfml
- os: windows-latest
target: x86_64-pc-windows-msvc
binary: asfml.exe
- os: windows-11-arm
target: aarch64-pc-windows-msvc
binary: asfml.exe
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Inject release version into Cargo manifests
shell: bash
run: python3 scripts/prepare_cargo_release.py --version "${{ needs.validate-tag.outputs.version }}"
- name: Refresh lockfile for injected version
shell: bash
run: cargo generate-lockfile
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Build release binary
shell: bash
run: cargo build --locked --release -p asfml --target ${{ matrix.platform.target }}
- name: Prepare vendor payload
shell: bash
run: |
set -euo pipefail
mkdir -p "dist/vendor/${{ matrix.platform.target }}/asfml"
cp "target/${{ matrix.platform.target }}/release/${{ matrix.platform.binary }}" \
"dist/vendor/${{ matrix.platform.target }}/asfml/${{ matrix.platform.binary }}"
- name: Package release archive
shell: bash
run: |
set -euo pipefail
version="${{ needs.validate-tag.outputs.version }}"
archive_name="asfml-${version}-${{ matrix.platform.target }}.tar.gz"
mkdir -p dist/release
tar -czf "dist/release/${archive_name}" \
-C "target/${{ matrix.platform.target }}/release" \
"${{ matrix.platform.binary }}"
- name: Upload release archive artifact
uses: actions/upload-artifact@v6
with:
name: release-${{ matrix.platform.target }}
path: dist/release/*.tar.gz
if-no-files-found: error
- name: Upload vendor artifact
uses: actions/upload-artifact@v6
with:
name: vendor-${{ matrix.platform.target }}
path: dist/vendor/${{ matrix.platform.target }}
if-no-files-found: error
publish-release:
name: Publish GitHub release assets
runs-on: ubuntu-latest
needs:
- validate-tag
- build-native
permissions:
contents: write
steps:
- name: Download release archives
uses: actions/download-artifact@v7
with:
path: dist/release
pattern: release-*
merge-multiple: true
- name: Generate checksums and manifest
shell: bash
run: |
set -euo pipefail
version="${{ needs.validate-tag.outputs.version }}"
release_tag="v${version}"
cd dist/release
shopt -s nullglob
archives=(asfml-"${version}"-*.tar.gz)
if [[ ${#archives[@]} -eq 0 ]]; then
echo "No release archives found."
exit 1
fi
sha256sum "${archives[@]}" | sort > checksums.txt
python3 - <<'PY'
import json
import os
from pathlib import Path
version = os.environ["VERSION"]
release_tag = os.environ["RELEASE_TAG"]
repo = os.environ["GITHUB_REPOSITORY"]
checksums_path = Path("checksums.txt")
artifacts: dict[str, dict[str, str]] = {}
for line in checksums_path.read_text(encoding="utf-8").splitlines():
sha256, filename = line.split(" ", maxsplit=1)
prefix = f"asfml-{version}-"
suffix = ".tar.gz"
if not filename.startswith(prefix) or not filename.endswith(suffix):
continue
target = filename[len(prefix) : -len(suffix)]
artifacts[target] = {
"filename": filename,
"sha256": sha256,
"url": f"https://github.com/{repo}/releases/download/{release_tag}/{filename}",
}
if not artifacts:
raise RuntimeError("No artifacts generated from checksums.txt.")
manifest = {
"version": version,
"release_tag": release_tag,
"artifacts": artifacts,
}
Path("manifest.json").write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
PY
env:
VERSION: ${{ needs.validate-tag.outputs.version }}
RELEASE_TAG: v${{ needs.validate-tag.outputs.version }}
- name: Upload release metadata artifact
uses: actions/upload-artifact@v6
with:
name: release-metadata
path: |
dist/release/checksums.txt
dist/release/manifest.json
if-no-files-found: error
- name: Publish release assets
shell: bash
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
version="${{ needs.validate-tag.outputs.version }}"
tag="v${version}"
shopt -s nullglob
archives=(dist/release/asfml-"${version}"-*.tar.gz)
if [[ ${#archives[@]} -eq 0 ]]; then
echo "No release archives found for ${tag}"
exit 1
fi
files=("${archives[@]}" dist/release/checksums.txt dist/release/manifest.json)
prerelease_flag=()
if [[ "${{ needs.validate-tag.outputs.is_prerelease }}" == "true" ]]; then
prerelease_flag+=(--prerelease)
fi
if gh release view "${tag}" >/dev/null 2>&1; then
gh release upload "${tag}" "${files[@]}" --clobber
else
gh release create "${tag}" "${files[@]}" \
--title "${tag}" \
--verify-tag \
"${prerelease_flag[@]}"
fi
publish-crates:
name: Publish crates.io packages
runs-on: ubuntu-latest
needs:
- validate-tag
- publish-release
environment:
name: crates-io
permissions:
id-token: write
contents: read
env:
RELEASE_VERSION: ${{ needs.validate-tag.outputs.version }}
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Inject release version into Cargo manifests
shell: bash
run: python3 scripts/prepare_cargo_release.py --version "${RELEASE_VERSION}"
- name: Refresh lockfile for injected version
shell: bash
run: cargo generate-lockfile
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Cache cargo artifacts
uses: Swatinem/rust-cache@v2
- name: Authenticate with crates.io
id: crates-auth
uses: rust-lang/crates-io-auth-action@v1.0.4
- name: Verify publishable asfml-core
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
run: cargo package --allow-dirty --locked -p asfml-core
- name: Publish asfml-core
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
shell: bash
run: |
set -euo pipefail
publish_output="$(mktemp)"
if cargo publish --allow-dirty --locked -p asfml-core >"${publish_output}" 2>&1; then
cat "${publish_output}"
exit 0
fi
cat "${publish_output}"
if grep -qiE "already uploaded|already exists|previously uploaded" "${publish_output}"; then
echo "asfml-core ${RELEASE_VERSION} is already published."
exit 0
fi
exit 1
- name: Wait for asfml-core index visibility
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import json
import os
import sys
import time
import urllib.request
crate = "asfml-core"
version = os.environ["RELEASE_VERSION"]
deadline = time.time() + 600
url = f"https://crates.io/api/v1/crates/{crate}"
while time.time() < deadline:
with urllib.request.urlopen(url, timeout=30) as response:
payload = json.load(response)
versions = [item["num"] for item in payload["versions"]]
if version in versions:
print(f"{crate} {version} is visible in crates.io index.")
sys.exit(0)
print(f"Waiting for {crate} {version} to appear on crates.io...")
time.sleep(10)
raise SystemExit(f"Timed out waiting for {crate} {version} to appear on crates.io")
PY
- name: Verify publishable asfml
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
run: cargo package --allow-dirty --locked -p asfml
- name: Publish asfml
env:
CARGO_REGISTRY_TOKEN: ${{ steps.crates-auth.outputs.token }}
shell: bash
run: |
set -euo pipefail
publish_output="$(mktemp)"
if cargo publish --allow-dirty --locked -p asfml >"${publish_output}" 2>&1; then
cat "${publish_output}"
exit 0
fi
cat "${publish_output}"
if grep -qiE "already uploaded|already exists|previously uploaded" "${publish_output}"; then
echo "asfml ${RELEASE_VERSION} is already published."
exit 0
fi
exit 1