diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..328969bc --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,350 @@ +name: Release + +on: + workflow_dispatch: + inputs: + release_date: + description: "Override date (UTC YYYY.MM.DD). Leave empty for today." + required: false + type: string + dry_run: + description: "Do not push/tag/publish (build only)" + required: false + type: boolean + default: false + +permissions: + contents: write + packages: write + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +env: + CARGO_TERM_COLOR: always + +jobs: + validate: + name: Validate (fmt, clippy, tests) + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Format check + run: cargo fmt --all -- --check + + - name: Clippy + run: cargo clippy --workspace --all-targets -- -D warnings + + - name: Tests + run: cargo test --workspace -- --nocapture + + prepare_version: + name: Prepare version, tag, push + needs: validate + runs-on: ubuntu-latest + outputs: + version: ${{ steps.setver.outputs.version }} + tag: ${{ steps.setver.outputs.tag }} + commit_sha: ${{ steps.commit.outputs.sha }} + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-workspaces + run: cargo install cargo-workspaces --locked + + - name: Compute date-based version + id: setver + shell: bash + run: | + set -euo pipefail + BASE="${{ inputs.release_date }}" + if [[ -z "${BASE}" ]]; then + BASE="$(date -u +%Y.%m.%d)" + fi + git fetch --tags --quiet + LAST=$(git tag -l "v${BASE}*" | sed 's/^v//' | sort -V | tail -n1 || true) + if [[ -z "${LAST}" ]]; then + VERSION="${BASE}" + else + if [[ "${LAST}" == "${BASE}" ]]; then + N=1 + else + SUF="${LAST#${BASE}-}" + N=$((10#${SUF} + 1)) + fi + VERSION=$(printf "%s-%02d" "${BASE}" "${N}") + fi + echo "version=${VERSION}" | tee -a "$GITHUB_OUTPUT" + echo "tag=v${VERSION}" | tee -a "$GITHUB_OUTPUT" + echo "${VERSION}" > VERSION + + - name: Bump workspace versions + if: ${{ !inputs.dry_run }} + shell: bash + run: | + set -euo pipefail + # Update all crate versions and internal dependency requirements + cargo workspaces version ${{ steps.setver.outputs.version }} \ + --exact --force --yes --no-git-tag --no-git-commit + # Align lockfile + cargo update -w + + - name: Commit and tag + id: commit + if: ${{ !inputs.dry_run }} + shell: bash + run: | + set -euo pipefail + git add -A + git commit -m "[release] v${{ steps.setver.outputs.version }}" + git tag "v${{ steps.setver.outputs.version }}" + git push origin HEAD:${{ github.ref_name }} + git push origin "v${{ steps.setver.outputs.version }}" + echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + + publish_crates: + name: Publish to crates.io + needs: prepare_version + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + ref: ${{ needs.prepare_version.outputs.tag }} + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Install cargo-workspaces + run: cargo install cargo-workspaces --locked + + - name: Publish workspace + run: | + set -euo pipefail + # Publish in dependency order, skipping those already on the registry + cargo workspaces publish --from-git --yes --skip-published + + build: + name: Build artifacts (${{ matrix.os_slug }}) + needs: prepare_version + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + os_slug: linux + archive: tar.gz + - os: macos-latest + os_slug: macos + archive: tar.gz + - os: windows-latest + os_slug: windows + archive: zip + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: true + ref: ${{ needs.prepare_version.outputs.tag }} + + - name: Rust toolchain + uses: dtolnay/rust-toolchain@stable + with: + profile: minimal + components: clippy,rustfmt + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + + - name: Install jq + if: ${{ runner.os != 'Windows' }} + shell: bash + run: | + if [ "${{ runner.os }}" = "Linux" ]; then + sudo apt-get update && sudo apt-get install -y jq + else + brew update && brew install jq + fi + + - name: Install jq (Windows) + if: ${{ runner.os == 'Windows' }} + shell: pwsh + run: choco install jq -y + + - name: Build binaries + shell: bash + run: | + set -euo pipefail + cargo build --workspace --release --bins + + - name: Stage files + id: stage + shell: bash + run: | + set -euo pipefail + VERSION='${{ needs.prepare_version.outputs.version }}' + OUTDIR="stage/lambda-${VERSION}-${{ matrix.os_slug }}" + mkdir -p "${OUTDIR}/bin" + # List workspace binary targets + bins=$(cargo metadata --format-version 1 --no-deps | jq -r '.packages[].targets[] | select(.kind[]=="bin") | .name' | sort -u) + for b in $bins; do + if [[ "${{ runner.os }}" == "Windows" ]]; then + src="target/release/${b}.exe" + else + src="target/release/${b}" + fi + if [[ -f "$src" ]]; then + cp "$src" "${OUTDIR}/bin/" + fi + done + # Include example 'minimal' if present + if [[ -f target/release/examples/minimal ]]; then + mkdir -p "${OUTDIR}/examples" + cp target/release/examples/minimal "${OUTDIR}/examples/" + elif [[ -f target/release/examples/minimal.exe ]]; then + mkdir -p "${OUTDIR}/examples" + cp target/release/examples/minimal.exe "${OUTDIR}/examples/" + fi + # Include assets if present + if [[ -d crates/lambda-rs/assets ]]; then + mkdir -p "${OUTDIR}/assets" + cp -R crates/lambda-rs/assets/* "${OUTDIR}/assets/" || true + fi + # Top-level docs + for f in LICENSE LICENSE.md README.md README; do + [[ -f "$f" ]] && cp "$f" "${OUTDIR}/" || true + done + echo "${VERSION}" > "${OUTDIR}/VERSION" + + - name: Package (tar.gz) + if: ${{ matrix.archive == 'tar.gz' }} + shell: bash + run: | + set -euo pipefail + cd stage + tar -czf "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}.tar.gz" "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}" + if command -v shasum >/dev/null 2>&1; then + shasum -a 256 "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}.tar.gz" > "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}.tar.gz.sha256" + elif command -v sha256sum >/dev/null 2>&1; then + sha256sum "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}.tar.gz" > "lambda-${{ needs.prepare_version.outputs.version }}-${{ matrix.os_slug }}.tar.gz.sha256" + fi + + - name: Package (zip) + if: ${{ matrix.archive == 'zip' }} + shell: pwsh + run: | + $v = "${{ needs.prepare_version.outputs.version }}" + $slug = "${{ matrix.os_slug }}" + $src = "stage/lambda-$v-$slug" + $zip = "stage/lambda-$v-$slug.zip" + Compress-Archive -Path "$src\*" -DestinationPath $zip -CompressionLevel Optimal + (Get-FileHash $zip -Algorithm SHA256).Hash + " *$(Split-Path -Leaf $zip)" | Out-File "$zip.sha256" -Encoding ascii + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: release-${{ matrix.os_slug }} + path: stage/* + if-no-files-found: error + + release: + name: Create GitHub release + needs: [prepare_version, build, publish_crates] + if: ${{ !inputs.dry_run }} + runs-on: ubuntu-latest + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + lfs: false + ref: ${{ needs.prepare_version.outputs.tag }} + + - name: Generate changelog + id: changelog + shell: bash + run: | + set -euo pipefail + TAG='${{ needs.prepare_version.outputs.tag }}' + VERSION='${{ needs.prepare_version.outputs.version }}' + REPO_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}" + git fetch --tags --quiet + PREV=$(git tag -l 'v*' | sort -V | awk -v t="$TAG" '$0==t{print last; exit} {last=$0}') + if [[ -n "${PREV}" ]]; then + RANGE="${PREV}..${TAG}" + COMPARE_URL="${REPO_URL}/compare/${PREV}...${TAG}" + else + RANGE="${TAG}" + COMPARE_URL="${REPO_URL}/commits/${TAG}" + fi + FILE="CHANGELOG-v${VERSION}.md" + { + echo "# Changelog"; + echo; + echo "Version ${TAG}"; + echo; + if [[ -n "${PREV}" ]]; then + echo "Changes since ${PREV}"; + else + echo "Initial release"; + fi + echo; + echo "Compare: ${COMPARE_URL}"; + echo; + echo "Commits:"; + echo; + } > "${FILE}" + git log --no-merges --pretty=format:"- [%h](${REPO_URL}/commit/%H) %s" ${RANGE} >> "${FILE}" + echo "path=${FILE}" >> "$GITHUB_OUTPUT" + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: dl + merge-multiple: true + + - name: Create release and upload assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ needs.prepare_version.outputs.tag }} + name: "lambda v${{ needs.prepare_version.outputs.version }}" + draft: false + prerelease: false + body_path: ${{ steps.changelog.outputs.path }} + files: | + dl/*.tar.gz + dl/*.tar.gz.sha256 + dl/*.zip + dl/*.zip.sha256 + ${{ steps.changelog.outputs.path }} diff --git a/README.md b/README.md index 910d8671..feb72f56 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@

[![Cross Platform builds & tests](https://github.com/lambda-sh/lambda/actions/workflows/compile_lambda_rs.yml/badge.svg)](https://github.com/lambda-sh/lambda/actions/workflows/compile_lambda_rs.yml) +[![Release](https://github.com/lambda-sh/lambda/actions/workflows/release.yml/badge.svg)](https://github.com/lambda-sh/lambda/actions/workflows/release.yml) ![lambda-rs](https://img.shields.io/crates/d/lambda-rs) ![lambda-rs](https://img.shields.io/crates/v/lambda-rs) @@ -18,6 +19,7 @@ 1. [Getting started](#get_started) 1. [Examples](#examples) 1. [Planned additions](#plans) +1. [Releases & Publishing](#publishing) 1. [How to contribute](#contribute) 1. [Resources](#resources) ## Description @@ -177,6 +179,13 @@ cargo run --example triangles - [ ] Unit tests. - [ ] Nightly builds. +## Releases & Publishing +For cutting releases, publishing crates to crates.io, and attaching +multi-platform artifacts to GitHub Releases, see: + +- docs/publishing.md + + ## How to contribute Fork the current repository and then make the changes that you'd like to said fork. Stable releases will happen within the main branch requiring that diff --git a/docs/publishing.md b/docs/publishing.md new file mode 100644 index 00000000..ba563519 --- /dev/null +++ b/docs/publishing.md @@ -0,0 +1,149 @@ +--- +title: "Release and Publishing Guide" +document_id: "release-guide-2025-09-28" +status: "living" +created: "2025-09-28T00:00:00Z" +last_updated: "2025-09-28T00:00:00Z" +version: "0.1.0" +engine_workspace_version: "2023.1.30" +wgpu_version: "26.0.1" +shader_backend_default: "naga" +winit_version: "0.29.10" +repo_commit: "fda8ee236986" +owners: ["lambda-sh"] +reviewers: ["engine", "rendering"] +tags: ["guide", "releases", "publishing", "ci"] +--- + +# Release and Publishing Guide + +This guide explains how to cut a release, publish the Rust crates to +crates.io, and attach compiled artifacts to a GitHub Release using the +automated workflow in `.github/workflows/release.yml`. + +## Versioning Scheme + +- Format: `YYYY.MM.DD` for daily releases, with patch suffix `-NN` for + same‑day follow‑ups (e.g., `2025.09.28`, `2025.09.28-01`). +- The workflow auto‑computes the next version by scanning existing tags. +- Tags are created as `v` (e.g., `v2025.09.28-01`). + +## One‑Time Setup + +- Add repository secret `CARGO_REGISTRY_TOKEN` with a crates.io token that has + publish rights for all workspace crates. +- Ensure branch protections allow the GitHub Actions bot to push to the main + branch, or ask us to switch the workflow to open a PR instead of pushing. +- Run `scripts/setup.sh` once locally to install hooks and LFS, and prefer + storing large assets through git‑lfs. + +## How the Workflow Works + +Jobs run in this order when manually triggered: + +1) Validate +- Runs `cargo fmt --check`, `cargo clippy -D warnings`, and `cargo test` on + Ubuntu. + +2) Prepare version, tag, push +- Computes version from the provided date (or UTC today), auto‑increments + `-NN` if a same‑day release already exists. +- Bumps all workspace crate versions and their internal dependency pins using + `cargo workspaces version --exact`. +- Commits `[release] v` and pushes to the current branch; pushes tag + `v`. + +3) Publish to crates.io +- Uses `cargo workspaces publish --from-git --skip-published` to push all + changed crates in dependency order. Already published versions are skipped. + +4) Build tri‑platform artifacts +- Builds all binary targets in release mode for Linux, macOS, and Windows. +- Packages archives with: + - `bin/` containing all workspace bin targets + - `examples/minimal` if present + - `crates/lambda-rs/assets/` if present + - `README`, `LICENSE` when present + - `VERSION` file and `.sha256` checksums + +5) Create GitHub Release +- Creates a release for the tag and uploads the archives and checksums. +- Generates a markdown changelog with a compare link and a bullet list of + commit links between the previous and current tag; used as the release body + and attached as an asset. + +## Running a Release + +1) Pre‑flight +- Ensure CI is green on main. Locally, you can run: + - `cargo fmt --all` + - `cargo clippy --workspace --all-targets -- -D warnings` + - `cargo test --workspace` + +2) Dry run (safe test) +- Go to GitHub → Actions → `Release` → `Run workflow`. +- Set `dry_run: true` and leave the date blank to use today. This builds and + packages artifacts but does not bump, tag, push, or publish. + +3) Real release +- Run the workflow with `dry_run: false` (default). Optionally set + `release_date` in `YYYY.MM.DD` if you need to back/forward‑date. +- The workflow updates versions on the branch you run it from (typically + `main`), pushes the release commit and tag, publishes crates, builds, and + creates a GitHub Release. + +## Hotfix / Patch Releases + +- To ship a same‑day fix, re‑run the workflow the same day. It will detect the + previous tag and produce the next `-NN` suffix (e.g., `-01`, `-02`). +- Fixes should be ordinary commits on the branch; the release job will include + them in the new tag. + +## Changelog Details + +- The workflow determines the previous `v*` tag and compares it with the new + tag. If none exists, it marks the release as initial. +- The generated markdown contains: + - A compare link (`/compare/prev...new`) + - A list of commit subjects with links to each commit +- You can edit the release notes on GitHub after the run if you want to add + highlights or screenshots. + +## Troubleshooting + +- Branch protections reject pushes + - Symptom: The prepare step fails on `git push`. + - Fix: Allow GitHub Actions to push to the branch, or switch the workflow to + open a PR for the version bump. + +- crates.io publish failures + - Symptom: Network/registry hiccups yield partial publish. + - Fix: Re‑run the `publish_crates` job or the entire workflow. Already + published crates are skipped. + +- Packaging misses a binary + - Ensure the target is declared as a `[[bin]]` in its crate. The packager + enumerates binary targets via `cargo metadata`. + +- Assets not included + - Only `crates/lambda-rs/assets` is packaged by default. If you need more + assets included, expand the staging step in the workflow. + +## Releasing New Crates in the Workspace + +- Make sure new crates have `license`, `repository`, and `categories` fields + set in `Cargo.toml`, and are members of the workspace. +- The workflow bumps versions for all workspace crates and publishes only those + with changes present in the tag (`--from-git`). + +## Manual Verification (optional) + +- Before cutting a release, you can verify examples locally: + - `cargo run --example minimal` +- For native engine builds: + - `scripts/compile_lambda.sh --build Debug` + - `scripts/compile_and_test.sh --os MacOS` + +## Changelog + +- 0.1.0 – Initial authoring of the guide and workflow documentation.