diff --git a/.github/workflows/release-artifacts.yml b/.github/workflows/release-artifacts.yml deleted file mode 100644 index 6ffca8f..0000000 --- a/.github/workflows/release-artifacts.yml +++ /dev/null @@ -1,195 +0,0 @@ -name: Release artifacts - -on: - workflow_call: - inputs: - ref: - description: Git revision containing the release packages - required: true - type: string - tag: - description: Canonical GitHub Release tag created by the release job - required: true - type: string - commit: - description: Release commit used in the release notes - required: true - type: string - -permissions: - contents: write - -concurrency: - group: release-artifacts-${{ inputs.tag }} - cancel-in-progress: false - -env: - CARGO_TERM_COLOR: always - -jobs: - build: - name: Build ${{ matrix.target }} - runs-on: ${{ matrix.runner }} - strategy: - fail-fast: false - matrix: - include: - - target: x86_64-unknown-linux-gnu - runner: ubuntu-latest - - target: aarch64-unknown-linux-gnu - runner: ubuntu-latest - - target: x86_64-apple-darwin - runner: macos-13 - - target: aarch64-apple-darwin - runner: macos-14 - - target: x86_64-pc-windows-msvc - runner: windows-latest - steps: - - name: Checkout release tag - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - - - name: Install Linux cross linker - if: matrix.target == 'aarch64-unknown-linux-gnu' - run: | - sudo apt-get update - sudo apt-get install --yes gcc-aarch64-linux-gnu - - - name: Install Rust target - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - targets: ${{ matrix.target }} - - - name: Build CLI - env: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - run: >- - cargo build --locked --release -p workshop-rs-cli - --target ${{ matrix.target }} - - - name: Package Unix artifact - if: runner.os != 'Windows' - env: - VERSION: ${{ inputs.tag }} - TARGET: ${{ matrix.target }} - run: | - set -euo pipefail - version="${VERSION#v}" - mkdir -p "dist/workshop-rs-cli-${version}-${TARGET}" - cp "target/${TARGET}/release/workshop-rs-cli" \ - "dist/workshop-rs-cli-${version}-${TARGET}/" - tar -C dist -czf \ - "dist/workshop-rs-cli-${version}-${TARGET}.tar.gz" \ - "workshop-rs-cli-${version}-${TARGET}" - - - name: Package Windows artifact - if: runner.os == 'Windows' - shell: pwsh - env: - VERSION: ${{ inputs.tag }} - TARGET: ${{ matrix.target }} - run: | - $version = $env:VERSION.Substring(1) - $directory = "workshop-rs-cli-$version-$env:TARGET" - New-Item -ItemType Directory -Path "dist/$directory" | Out-Null - $binary = "target/$env:TARGET/release/workshop-rs-cli.exe" - Copy-Item $binary "dist/$directory/" - Compress-Archive -Path "dist/$directory" ` - -DestinationPath "dist/workshop-rs-cli-$version-$env:TARGET.zip" - - - name: Upload platform artifact - uses: actions/upload-artifact@v4 - with: - name: release-${{ matrix.target }} - path: | - dist/*.tar.gz - dist/*.zip - if-no-files-found: error - - publish: - name: Attach artifacts and publish release - needs: build - runs-on: ubuntu-latest - steps: - - name: Checkout release tag - uses: actions/checkout@v7 - with: - ref: ${{ inputs.ref }} - - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - - - name: Download platform artifacts - uses: actions/download-artifact@v4 - with: - pattern: release-* - path: dist - merge-multiple: true - - - name: Generate checksums and catalog identity - run: | - set -euo pipefail - cd dist - sha256sum *.tar.gz *.zip > SHA256SUMS.txt - cd .. - cargo run --locked -p workshop-rs-cli -- version --json \ - > dist/catalog-identity.json - - - name: Verify the draft GitHub Release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ inputs.tag }} - run: | - set -euo pipefail - release="$(gh release view "${TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --json tagName,isDraft,isImmutable,url)" - echo "${release}" - test "$(jq -r .tagName <<<"${release}")" = "${TAG}" - test "$(jq -r .isDraft <<<"${release}")" = "true" - - - name: Add catalog identity to release notes - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ inputs.tag }} - COMMIT: ${{ inputs.commit }} - run: | - set -euo pipefail - marker='' - body="$(gh release view "${TAG}" \ - --repo "${GITHUB_REPOSITORY}" --json body --jq .body)" - if [[ "${body}" != *"${marker}"* ]]; then - { - printf '%s\n\n' "${body}" - printf '%s\n\n' "${marker}" - printf '%s\n\n' '## Release identity' - printf '%s\n\n' "- Revision: \`${COMMIT}\`" - printf '%s\n' '### Catalog identity' - printf '%s\n' '```json' - cat dist/catalog-identity.json - printf '%s\n' '```' - } > release-notes.md - gh release edit "${TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --notes-file release-notes.md - fi - - - name: Upload artifacts and checksums - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ inputs.tag }} - run: >- - gh release upload "${TAG}" dist/* --clobber - --repo "${GITHUB_REPOSITORY}" - - - name: Publish the completed GitHub Release - env: - GH_TOKEN: ${{ github.token }} - TAG: ${{ inputs.tag }} - run: >- - gh release edit "${TAG}" --draft=false - --repo "${GITHUB_REPOSITORY}" diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index 2fe420d..c90f0e8 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -3,220 +3,59 @@ name: Release-plz on: push: branches: [main] - pull_request: - types: [closed] - -env: - CARGO_TERM_COLOR: always jobs: - release-pr: - name: Maintain release PR - if: github.event_name == 'push' + release: + name: Publish crates and release tag + if: github.repository_owner == 'wrightkit' runs-on: ubuntu-latest + environment: release permissions: contents: write - pull-requests: write - concurrency: - group: release-plz-pr-${{ github.ref }} - cancel-in-progress: false + pull-requests: read steps: - name: Checkout repository - uses: actions/checkout@v7 + uses: actions/checkout@v6 with: fetch-depth: 0 persist-credentials: false token: ${{ secrets.GH_TOKEN }} - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable + uses: dtolnay/rust-toolchain@stable - - name: Run release-plz release-pr - uses: >- - MarcoIeni/release-plz-action@476794ede164c5137bfc3a1dc6ed3675275690f9 + - name: Publish unpublished crates + uses: release-plz/action@v0.5 with: - command: release-pr - version: "0.3.160" + command: release env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - release: - name: Publish merged release PR - if: >- - github.event_name == 'pull_request' && - github.event.pull_request.merged == true && - github.event.pull_request.base.ref == 'main' && - contains(github.event.pull_request.labels.*.name, 'release') && - github.event.pull_request.head.repo.full_name == github.repository + release-pr: + name: Maintain release PR + if: github.repository_owner == 'wrightkit' runs-on: ubuntu-latest - environment: release - outputs: - release_tag: ${{ steps.release-identity.outputs.tag }} - artifacts_required: ${{ steps.release-state.outputs.artifacts_required }} permissions: contents: write - pull-requests: read + pull-requests: write concurrency: - group: release-plz-release-${{ github.event.pull_request.base.ref }} + group: release-plz-pr-${{ github.ref }} cancel-in-progress: false steps: - - name: Checkout merged release - uses: actions/checkout@v7 + - name: Checkout repository + uses: actions/checkout@v6 with: - ref: ${{ github.event.pull_request.merge_commit_sha }} fetch-depth: 0 persist-credentials: false token: ${{ secrets.GH_TOKEN }} - - name: Attach release checkout to main upstream - run: | - set -euo pipefail - git switch --create release-plz-release - git branch --set-upstream-to=origin/main release-plz-release - - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@master - with: - toolchain: stable - components: rustfmt, clippy - - - name: Run release gates - run: | - set -euo pipefail - cargo fmt --all --check - cargo clippy --locked --workspace --all-targets -- -D warnings - cargo test --locked --workspace --all-targets - cargo run --locked -p workshop-rs --bin workshop-catalog-gen -- \ - check - cargo package --locked -p workshop-rs + uses: dtolnay/rust-toolchain@stable - - name: Resolve release identity - id: release-identity - run: | - set -euo pipefail - metadata="$(cargo metadata --locked --no-deps --format-version 1)" - version="$(jq -r '.packages[] | select(.name == "workshop-rs") | .version' \ - <<<"${metadata}")" - cli_version="$(jq -r '.packages[] | select(.name == "workshop-rs-cli") | .version' \ - <<<"${metadata}")" - [[ -n "${version}" && "${version}" != "null" ]] || { - echo 'Could not resolve workshop-rs version.' >&2 - exit 1 - } - [[ "${version}" == "${cli_version}" ]] || { - echo "workshop-rs ${version} and workshop-rs-cli ${cli_version} are not version-aligned." >&2 - exit 1 - } - echo "version=${version}" >>"${GITHUB_OUTPUT}" - echo "tag=v${version}" >>"${GITHUB_OUTPUT}" - - - name: Run release-plz release - id: release-plz - uses: >- - MarcoIeni/release-plz-action@476794ede164c5137bfc3a1dc6ed3675275690f9 + - name: Update release PR + uses: release-plz/action@v0.5 with: - command: release - version: "0.3.160" + command: release-pr env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} - GH_TOKEN: ${{ secrets.GH_TOKEN }} - CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} - - - name: Ensure canonical release tag - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - TAG: ${{ steps.release-identity.outputs.tag }} - RELEASE_COMMIT: ${{ github.event.pull_request.merge_commit_sha }} - run: | - set -euo pipefail - if tag_commit="$(gh api \ - "repos/${GITHUB_REPOSITORY}/commits/${TAG}" --jq .sha 2>/dev/null)"; then - [[ "${tag_commit}" == "${RELEASE_COMMIT}" ]] || { - echo "${TAG} points to ${tag_commit}, expected ${RELEASE_COMMIT}." >&2 - exit 1 - } - echo "${TAG} already points to the release commit." - else - gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ - -f "ref=refs/tags/${TAG}" \ - -f "sha=${RELEASE_COMMIT}" >/dev/null - echo "Created ${TAG} at ${RELEASE_COMMIT}." - fi - - - name: Prepare GitHub Release state - id: release-state - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - TAG: ${{ steps.release-identity.outputs.tag }} - VERSION: ${{ steps.release-identity.outputs.version }} - run: | - set -euo pipefail - set +e - release="$(gh release view "${TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --json tagName,isDraft,isImmutable,assets,url 2>&1)" - status=$? - set -e - - if [[ "${status}" -eq 0 ]]; then - echo "${release}" - [[ "$(jq -r .tagName <<<"${release}")" == "${TAG}" ]] || { - echo "GitHub Release tag identity does not match ${TAG}." >&2 - exit 1 - } - if [[ "$(jq -r .isDraft <<<"${release}")" == "true" ]]; then - echo "Reusing draft GitHub Release ${TAG}." - echo "artifacts_required=true" >>"${GITHUB_OUTPUT}" - exit 0 - fi - - expected=( - "workshop-rs-cli-${VERSION}-x86_64-unknown-linux-gnu.tar.gz" - "workshop-rs-cli-${VERSION}-aarch64-unknown-linux-gnu.tar.gz" - "workshop-rs-cli-${VERSION}-x86_64-apple-darwin.tar.gz" - "workshop-rs-cli-${VERSION}-aarch64-apple-darwin.tar.gz" - "workshop-rs-cli-${VERSION}-x86_64-pc-windows-msvc.zip" - "SHA256SUMS.txt" - "catalog-identity.json" - ) - for asset in "${expected[@]}"; do - jq -e --arg asset "${asset}" \ - '[.assets[].name] | index($asset) != null' \ - <<<"${release}" >/dev/null || { - echo "Published GitHub Release ${TAG} is incomplete; missing ${asset}." >&2 - exit 1 - } - done - echo "Published GitHub Release ${TAG} is already complete." - echo "artifacts_required=false" >>"${GITHUB_OUTPUT}" - exit 0 - fi - - if [[ "${release}" != *"release not found"* ]]; then - printf '%s\n' "${release}" >&2 - exit "${status}" - fi - - gh release create "${TAG}" \ - --repo "${GITHUB_REPOSITORY}" \ - --draft \ - --verify-tag \ - --title "${TAG}" \ - --generate-notes >/dev/null - echo "Created draft GitHub Release ${TAG}." - echo "artifacts_required=true" >>"${GITHUB_OUTPUT}" - - release-artifacts: - name: Build and publish release artifacts - needs: release - if: needs.release.outputs.artifacts_required == 'true' - uses: ./.github/workflows/release-artifacts.yml - with: - ref: ${{ github.event.pull_request.merge_commit_sha }} - tag: ${{ needs.release.outputs.release_tag }} - commit: ${{ github.event.pull_request.merge_commit_sha }} - permissions: - contents: write diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..1dfcd0f --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,296 @@ +# This file was autogenerated by dist: https://axodotdev.github.io/cargo-dist +# +# Copyright 2022-2024, axodotdev +# SPDX-License-Identifier: MIT or Apache-2.0 +# +# CI that: +# +# * checks for a Git Tag that looks like a release +# * builds artifacts with dist (archives, installers, hashes) +# * uploads those artifacts to temporary workflow zip +# * on success, uploads the artifacts to a GitHub Release +# +# Note that the GitHub Release will be created with a generated +# title/body based on your changelogs. + +name: Release +permissions: + "contents": "write" + +# This task will run whenever you push a git tag that looks like a version +# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc. +# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where +# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION +# must be a Cargo-style SemVer Version (must have at least major.minor.patch). +# +# If PACKAGE_NAME is specified, then the announcement will be for that +# package (erroring out if it doesn't have the given version or isn't dist-able). +# +# If PACKAGE_NAME isn't specified, then the announcement will be for all +# (dist-able) packages in the workspace with that version (this mode is +# intended for workspaces with only one dist-able package, or with all dist-able +# packages versioned/released in lockstep). +# +# If you push multiple tags at once, separate instances of this workflow will +# spin up, creating an independent announcement for each one. However, GitHub +# will hard limit this to 3 tags per commit, as it will assume more tags is a +# mistake. +# +# If there's a prerelease-style suffix to the version, then the release(s) +# will be marked as a prerelease. +on: + pull_request: + push: + tags: + - '**[0-9]+.[0-9]+.[0-9]+*' + +jobs: + # Run 'dist plan' (or host) to determine what tasks we need to do + plan: + runs-on: "ubuntu-22.04" + outputs: + val: ${{ steps.plan.outputs.manifest }} + tag: ${{ !github.event.pull_request && github.ref_name || '' }} + tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} + publishing: ${{ !github.event.pull_request }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive + - name: Install dist + # we specify bash to get pipefail; it guards against the `curl` command + # failing. otherwise `sh` won't catch that `curl` returned non-0 + shell: bash + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.32.0/cargo-dist-installer.sh | sh" + - name: Cache dist + uses: actions/upload-artifact@v7 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/dist + # sure would be cool if github gave us proper conditionals... + # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible + # functionality based on whether this is a pull_request, and whether it's from a fork. + # (PRs run on the *source* but secrets are usually on the *target* -- that's *good* + # but also really annoying to build CI around when it needs secrets to work right.) + - id: plan + run: | + dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json > plan-dist-manifest.json + echo "dist ran successfully" + cat plan-dist-manifest.json + echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v7 + with: + name: artifacts-plan-dist-manifest + path: plan-dist-manifest.json + + # Build and packages all the platform-specific things + build-local-artifacts: + name: build-local-artifacts (${{ join(matrix.targets, ', ') }}) + # Let the initial task tell us to not run (currently very blunt) + needs: + - plan + if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }} + strategy: + fail-fast: false + # Target platforms/runners are computed by dist in create-release. + # Each member of the matrix has the following arguments: + # + # - runner: the github runner + # - dist-args: cli flags to pass to dist + # - install-dist: expression to run to install dist on the runner + # + # Typically there will be: + # - 1 "global" task that builds universal installers + # - N "local" tasks that build each platform's binaries and platform-specific installers + matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }} + runs-on: ${{ matrix.runner }} + container: ${{ matrix.container && matrix.container.image || null }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json + steps: + - name: enable windows longpaths + run: | + git config --global core.longpaths true + - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive + - name: Install Rust non-interactively if not already installed + if: ${{ matrix.container }} + run: | + if ! command -v cargo > /dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + echo "$HOME/.cargo/bin" >> $GITHUB_PATH + fi + - name: Install dist + run: ${{ matrix.install_dist.run }} + # Get the dist-manifest + - name: Fetch local artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - name: Install dependencies + run: | + ${{ matrix.packages_install }} + - name: Build artifacts + run: | + # Actually do builds and make zips and whatnot + dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json + echo "dist ran successfully" + - id: cargo-dist + name: Post-build + # We force bash here just because github makes it really hard to get values up + # to "real" actions without writing to env-vars, and writing to env-vars has + # inconsistent syntax between shell and powershell. + shell: bash + run: | + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v7 + with: + name: artifacts-build-local-${{ join(matrix.targets, '_') }} + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + + # Build and package all the platform-agnostic(ish) things + build-global-artifacts: + needs: + - plan + - build-local-artifacts + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v8 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Get all the local artifacts for the global tasks to use (for e.g. checksums) + - name: Fetch local artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: cargo-dist + shell: bash + run: | + dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json "--artifacts=global" > dist-manifest.json + echo "dist ran successfully" + + # Parse out what we just built and upload it to scratch storage + echo "paths<> "$GITHUB_OUTPUT" + jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + cp dist-manifest.json "$BUILD_MANIFEST_NAME" + - name: "Upload artifacts" + uses: actions/upload-artifact@v7 + with: + name: artifacts-build-global + path: | + ${{ steps.cargo-dist.outputs.paths }} + ${{ env.BUILD_MANIFEST_NAME }} + # Determines if we should publish/announce + host: + needs: + - plan + - build-local-artifacts + - build-global-artifacts + # Only run if we're "publishing", and only if plan, local and global didn't fail (skipped is fine) + if: ${{ always() && needs.plan.result == 'success' && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }} + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + runs-on: "ubuntu-22.04" + outputs: + val: ${{ steps.host.outputs.manifest }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive + - name: Install cached dist + uses: actions/download-artifact@v8 + with: + name: cargo-dist-cache + path: ~/.cargo/bin/ + - run: chmod +x ~/.cargo/bin/dist + # Fetch artifacts from scratch-storage + - name: Fetch artifacts + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: target/distrib/ + merge-multiple: true + - id: host + shell: bash + run: | + dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json + echo "artifacts uploaded and released successfully" + cat dist-manifest.json + echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT" + - name: "Upload dist-manifest.json" + uses: actions/upload-artifact@v7 + with: + # Overwrite the previous copy + name: artifacts-dist-manifest + path: dist-manifest.json + # Create a GitHub Release while uploading all files to it + - name: "Download GitHub Artifacts" + uses: actions/download-artifact@v8 + with: + pattern: artifacts-* + path: artifacts + merge-multiple: true + - name: Cleanup + run: | + # Remove the granular manifests + rm -f artifacts/*-dist-manifest.json + - name: Create GitHub Release + env: + PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}" + ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}" + ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}" + RELEASE_COMMIT: "${{ github.sha }}" + run: | + # Write and read notes from a file to avoid quoting breaking things + echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt + + gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/* + + announce: + needs: + - plan + - host + # use "always() && ..." to allow us to wait for all publish jobs while + # still allowing individual publish jobs to skip themselves (for prereleases). + # "host" however must run to completion, no skipping allowed! + if: ${{ always() && needs.host.result == 'success' }} + runs-on: "ubuntu-22.04" + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@v6 + with: + persist-credentials: false + submodules: recursive diff --git a/Cargo.toml b/Cargo.toml index 4c63174..3050b41 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,3 +27,7 @@ perf = { level = "warn", priority = -1 } dbg_macro = "deny" todo = "deny" unimplemented = "deny" + +[profile.dist] +inherits = "release" +lto = "thin" diff --git a/dist-workspace.toml b/dist-workspace.toml new file mode 100644 index 0000000..0fbb3cc --- /dev/null +++ b/dist-workspace.toml @@ -0,0 +1,28 @@ +[workspace] +members = ["cargo:."] + +[dist] +cargo-dist-version = "0.32.0" +ci = "github" +packages = ["workshop-rs-cli"] +installers = [] +targets = [ + "x86_64-unknown-linux-gnu", + "aarch64-unknown-linux-gnu", + "x86_64-apple-darwin", + "aarch64-apple-darwin", + "x86_64-pc-windows-msvc", +] +pr-run-mode = "plan" +checksum = "sha256" +unix-archive = ".tar.gz" +windows-archive = ".zip" +source-tarball = false +github-release = "host" + +[[dist.extra-artifacts]] +artifacts = ["target/distrib/catalog-identity.json"] +build = ["sh", "-c", "target_dir=$(cargo metadata --locked --no-deps --format-version 1 | jq -r .target_directory); mkdir -p \"$target_dir/distrib\"; cargo run --locked -p workshop-rs-cli -- version --json > \"$target_dir/distrib/catalog-identity.json\""] + +[dist.github-custom-runners] +aarch64-unknown-linux-gnu = "ubuntu-24.04-arm" diff --git a/docs/release.md b/docs/release.md index a50ac29..7c84cd1 100644 --- a/docs/release.md +++ b/docs/release.md @@ -1,73 +1,104 @@ # Release automation -`release-plz` maintains a Release PR from pushes to `main`. Merging that PR -is the normal release action. The merged Release PR runs the repository gates -and publishes `workshop-rs` before `workshop-rs-cli`. Repository-owned workflow -steps then establish the canonical `vX.Y.Z` tag and draft GitHub Release before -the artifact workflow builds the CLI archives, adds checksums and catalog -identity, and publishes the completed GitHub Release. +`workshop-rs` uses two upstream release tools with separate responsibilities: + +- `release-plz` maintains the Release PR, publishes the Rust crates to crates.io, + and creates the canonical `vX.Y.Z` tag. +- `dist` owns binary distribution from that tag: release planning, five-platform + CLI builds, per-artifact SHA-256 files, `catalog-identity.json`, and the final + GitHub Release. + +The repository does not maintain a second tag/Release state machine around +those tools. ## Repository configuration -Configure these repository resources before enabling the workflow: - -1. Provide a repository or organization `GH_TOKEN` secret with access to this - repository and permission to read/write contents and pull requests. The - release workflow uses this token for `release-plz-*` branches and Release - PRs, and for repository-owned tag and draft Release management. -2. Create an environment named `release` with required reviewers enabled. - Store `CARGO_REGISTRY_TOKEN` in that environment and grant it permission to - publish both crates. The environment is used only by the merged Release PR - publication job. -3. Normal development remains PR-only; no direct `main` push exception is - required. Release-management credentials do not bypass repository review - policy. -4. Artifact publication remains a `workflow_call` from the release workflow. - It does not depend on a tag event starting a second workflow. - -`release-plz` owns version calculation, Release PR maintenance, and crates.io -publication. It is deliberately configured not to create Git tags or GitHub -Releases. After publication succeeds, the repository workflow derives the -release version from the merged Cargo metadata, creates or validates the -canonical tag against the merge commit, and creates or reuses the draft GitHub -Release. The called artifact workflow uses its scoped `GITHUB_TOKEN` to attach -assets and publish the completed release. - -## Release identity and retries - -Both packages use the `workshop-rs` release-plz `version_group` and therefore -must have the same release version. There is one public `vX.Y.Z` tag and one -GitHub Release for the repository; the CLI does not get a second public tag or -release. Cargo's dependency order makes the library publish before the CLI. - -The release-plz release job is gated by format, clippy, tests, catalog check, -and `cargo package` for both crates. The workflow is recoverable across partial -publication: release-plz skips crate versions already present in the registry, -the repository workflow independently resolves the intended release identity, -validates or creates the tag, and creates or reuses an unpublished draft. The -artifact workflow uploads with `--clobber` before publishing the draft. - -If a GitHub Release is already public, the workflow does not move it back to -draft. A complete published release is treated as already finished; an -incomplete published release fails explicitly for maintainer recovery. This -keeps the normal path compatible with immutable-release semantics. - -Do not manually bump versions or run `cargo publish` for a normal release. -Retry the failed GitHub Actions job so the workflow can resume from the -externally visible registry/tag/draft state. +1. Configure `GH_TOKEN` as a repository or organization secret with permission + to update this repository and maintain pull requests. `release-plz` uses it + for the Release PR and canonical tag. A dedicated token is retained here so + the tag created by release automation can trigger the tag-based `dist` + workflow. +2. Keep `CARGO_REGISTRY_TOKEN` in the protected `release` environment with + permission to publish both `workshop-rs` and `workshop-rs-cli`. This PR keeps + the already-proven registry credential path while the release orchestration + is simplified. crates.io Trusted Publishing can replace this token in a + separate change after both crates have been configured and verified there. +3. Keep normal CI required on the Release PR. Release-specific orchestration + should not duplicate the repository's normal Rust test suite. + +## Release flow + +Normal pushes to `main` run the two standard `release-plz` jobs: + +1. `release-plz release` publishes any workspace versions that are present in + Git but not yet in crates.io. The library and CLI share the `workshop-rs` + version group; the library is published before the CLI. The `workshop-rs` + package owns the single public `vX.Y.Z` tag. `release-plz` does not create a + GitHub Release. +2. `release-plz release-pr` creates or refreshes the next Release PR. + +When the canonical tag reaches GitHub, the dist-generated `Release` workflow +runs: + +```text +vX.Y.Z + -> dist plan + -> build workshop-rs-cli for five targets + -> build global artifacts/checksums + -> create the GitHub Release with the complete artifact set + -> announce +``` + +The supported targets are: + +- `x86_64-unknown-linux-gnu` +- `aarch64-unknown-linux-gnu` +- `x86_64-apple-darwin` +- `aarch64-apple-darwin` +- `x86_64-pc-windows-msvc` + +Linux ARM64 uses GitHub's native `ubuntu-24.04-arm` runner rather than a +repository-maintained cross-linker setup. + +`dist` preserves the existing `.tar.gz`/`.zip` archive formats and emits a +`.sha256` file for each archive. `catalog-identity.json` is configured as a +`dist` extra artifact and is generated from `workshop-rs-cli version --json`. +The previous aggregate `SHA256SUMS.txt` file is replaced by dist's standard +per-artifact checksum files. + +## Pull-request validation + +The dist workflow runs `dist plan` on pull requests. This validates release tag +interpretation, the selected package, target matrix, and artifact plan without +performing publication side effects. The generated workflow should be updated +through `dist init`/`dist generate` when the pinned dist version or distribution +configuration changes; do not hand-maintain a parallel release implementation. + +## Failure and recovery + +The release systems are intentionally not treated as one atomic transaction. +crates.io, Git tags, and GitHub Releases are separate external states. + +- If crates.io publication fails, rerun the failed `release-plz` job; already + published crate versions are skipped. +- If crate publication succeeds but tag creation fails, repair/retry the + canonical tag before invoking distribution. Do not create a replacement + version solely to repair binary distribution. +- If dist fails before hosting, rerun the tag workflow after fixing the actual + build/configuration problem. GitHub Release creation remains owned by dist. +- Do not recreate repository-specific draft/published Release state machines to + automate rare recovery cases. Prefer explicit maintainer recovery when an + external service is left in an unusual partial state. ## Maintainer procedure 1. Merge normal changes through PRs using Conventional Commits. -2. Review the automatically maintained Release PR and its CI checks. -3. Merge the Release PR after the protected `release` environment is ready. -4. Approve the publication job when prompted. It publishes both crates, - establishes the canonical tag and draft GitHub Release, then invokes the - artifact workflow. -5. The artifact workflow attaches the five platform archives, - `SHA256SUMS.txt`, and `catalog-identity.json`, then publishes the completed +2. Review the automatically maintained Release PR and its normal CI checks. +3. Merge the Release PR when the version is ready. +4. Approve the protected `release` environment if required. +5. `release-plz` publishes the crates and creates `vX.Y.Z`. +6. The tag-triggered dist workflow builds the CLI artifacts and publishes the GitHub Release. -The resulting GitHub Release notes contain generated release notes, the exact -revision, and the CLI's machine-readable catalog version, digest, locale -coverage, and provenance identity. +For routine releases, maintainers should not manually bump versions, run +`cargo publish`, create GitHub Releases, or edit generated dist CI. diff --git a/release-plz.toml b/release-plz.toml index 1f0fb7c..43f27d1 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -12,6 +12,8 @@ semver_check = false name = "workshop-rs" version_group = "workshop-rs" changelog_include = ["workshop-rs-cli"] +git_tag_enable = true +git_tag_name = "v{{ version }}" [[package]] name = "workshop-rs-cli"