Skip to content
Merged
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
179 changes: 179 additions & 0 deletions .github/workflows/release_artifacts.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
name: Release Artifacts

on:
workflow_dispatch:
inputs:
tag_name:
description: Draft release tag to build artifacts for; run the workflow on the same tag
required: true
type: string

permissions: {}

defaults:
run:
shell: bash

concurrency:
group: ${{ github.workflow }}-${{ inputs.tag_name }}
cancel-in-progress: false

jobs:
release-artifacts:
runs-on: macos-15
timeout-minutes: 30
permissions:
contents: write
id-token: write
attestations: write
env:
CARGO_DIST_VERSION: 0.30.2
TARGET: aarch64-apple-darwin
TAG_NAME: ${{ inputs.tag_name }}
steps:
- name: Checkout release tag
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
ref: ${{ env.TAG_NAME }}

- name: Verify release target
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# Provenance must name the tagged commit, so the workflow runs on the tag.
if [[ "$GITHUB_REF" != "refs/tags/$TAG_NAME" ]]; then
echo "Run this workflow on the release tag itself:"
echo " gh workflow run release_artifacts.yml --ref $TAG_NAME -f tag_name=$TAG_NAME"
exit 1
fi

head_sha="$(git rev-parse HEAD)"
if [[ "$head_sha" != "$GITHUB_SHA" ]]; then
echo "$TAG_NAME points to $head_sha, but this run is for $GITHUB_SHA."
exit 1
fi

# Re-uploading with --clobber deletes assets first, so never touch a published release.
is_draft="$(gh release view "$TAG_NAME" --json isDraft --jq .isDraft)"
if [[ "$is_draft" != "true" ]]; then
echo "$TAG_NAME is already published; ship fixes as a new release instead."
exit 1
fi

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: 1.98.1
target: ${{ env.TARGET }}
rustflags: ""

- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22

- name: Install cargo-dist
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
"https://github.com/axodotdev/cargo-dist/releases/download/v${CARGO_DIST_VERSION}/cargo-dist-installer.sh" \
| sh
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

- name: Run tests
run: cargo test --locked

- name: Install JXA check dependencies
run: pnpm install --frozen-lockfile

- name: Check embedded JXA
run: pnpm run check

- name: Build release artifacts
run: |
set -euo pipefail

dist build \
--tag "$TAG_NAME" \
--target "$TARGET" \
--artifacts=all \
--output-format=json \
> dist-manifest.json
dist print-upload-files-from-manifest \
--manifest dist-manifest.json \
> dist-upload-files.txt

- name: Smoke test packaged archive
run: |
set -euo pipefail

archive_path="$(
find target/distrib -maxdepth 1 -type f \
\( -name "apple-mail-cli-${TARGET}.tar.xz" -o -name "apple-mail-cli-${TARGET}.tar.gz" \) \
| head -n 1
)"

if [[ -z "$archive_path" ]]; then
echo "apple-mail-cli archive for ${TARGET} was not produced."
exit 1
fi

tmpdir="$(mktemp -d)"

case "$archive_path" in
*.tar.xz)
tar -xJf "$archive_path" -C "$tmpdir" --strip-components=1
;;
*.tar.gz)
tar -xzf "$archive_path" -C "$tmpdir" --strip-components=1
;;
*)
echo "unsupported archive format: $archive_path"
exit 1
;;
esac

"$tmpdir/apple-mail" --version
"$tmpdir/apple-mail" --help

- name: Attest release artifacts
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: target/distrib/*

- name: Upload release artifacts
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# macOS runners ship Bash 3.2, which has no mapfile.
artifact_paths=()
while IFS= read -r artifact_path; do
artifact_paths+=("$artifact_path")
done < dist-upload-files.txt
if ((${#artifact_paths[@]} == 0)); then
echo "cargo-dist did not report uploadable files."
exit 1
fi

gh release upload "$TAG_NAME" "${artifact_paths[@]}" --clobber

- name: Publish release
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# Publishing without make_latest marks the release latest. An older tag's run
# can finish last, so let GitHub pick latest by date and semantic version.
release_id="$(gh release view "$TAG_NAME" --json databaseId --jq .databaseId)"
gh api -X PATCH "repos/$GH_REPO/releases/$release_id" -F draft=false -f make_latest=legacy
156 changes: 6 additions & 150 deletions .github/workflows/release_please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,6 @@ on:
push:
branches:
- main
workflow_dispatch:
inputs:
tag_name:
description: Existing release tag to rebuild artifacts for; run the workflow on the same tag
required: true
type: string

permissions: {}

Expand All @@ -23,16 +17,13 @@ concurrency:

jobs:
release-please:
if: ${{ github.event_name == 'push' }}
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
actions: write
contents: write
issues: write
pull-requests: write
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
steps:
- name: Check release token
env:
Expand All @@ -53,145 +44,10 @@ jobs:
config-file: release-please-config.json
manifest-file: .release-please-manifest.json

release-artifacts:
needs: release-please
if: ${{ always() && (needs.release-please.outputs.release_created == 'true' || github.event_name == 'workflow_dispatch') }}
runs-on: macos-15
timeout-minutes: 30
permissions:
contents: write
id-token: write
attestations: write
env:
CARGO_DIST_VERSION: 0.30.2
TARGET: aarch64-apple-darwin
TAG_NAME: ${{ github.event_name == 'workflow_dispatch' && inputs.tag_name || needs.release-please.outputs.tag_name }}
steps:
- name: Checkout release tag
uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6.1.0
with:
persist-credentials: false
ref: ${{ env.TAG_NAME }}

- name: Verify release ref
run: |
set -euo pipefail

# Provenance must name the tagged commit, so manual rebuilds run on the tag.
if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" && "$GITHUB_REF" != "refs/tags/$TAG_NAME" ]]; then
echo "Run manual rebuilds on the tag itself:"
echo " gh workflow run release_please.yml --ref $TAG_NAME -f tag_name=$TAG_NAME"
exit 1
fi

head_sha="$(git rev-parse HEAD)"
if [[ "$head_sha" != "$GITHUB_SHA" ]]; then
echo "$TAG_NAME points to $head_sha, but this run is for $GITHUB_SHA."
exit 1
fi

- name: Install Rust toolchain
uses: actions-rust-lang/setup-rust-toolchain@166cdcfd11aee3cb47222f9ddb555ce30ddb9659 # v1.17.0
with:
toolchain: 1.98.1
target: ${{ env.TARGET }}
rustflags: ""

- name: Setup pnpm
uses: pnpm/action-setup@fc06bc1257f339d1d5d8b3a19a8cae5388b55320 # v5.0.0

- name: Setup Node.js
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: 22

- name: Install cargo-dist
run: |
curl --proto '=https' --tlsv1.2 -LsSf \
"https://github.com/axodotdev/cargo-dist/releases/download/v${CARGO_DIST_VERSION}/cargo-dist-installer.sh" \
| sh
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"

- name: Run tests
run: cargo test --locked

- name: Install JXA check dependencies
run: pnpm install --frozen-lockfile

- name: Check embedded JXA
run: pnpm run check

- name: Build release artifacts
run: |
set -euo pipefail

dist build \
--tag "$TAG_NAME" \
--target "$TARGET" \
--artifacts=all \
--output-format=json \
> dist-manifest.json
dist print-upload-files-from-manifest \
--manifest dist-manifest.json \
> dist-upload-files.txt

- name: Smoke test packaged archive
run: |
set -euo pipefail

archive_path="$(
find target/distrib -maxdepth 1 -type f \
\( -name "apple-mail-cli-${TARGET}.tar.xz" -o -name "apple-mail-cli-${TARGET}.tar.gz" \) \
| head -n 1
)"

if [[ -z "$archive_path" ]]; then
echo "apple-mail-cli archive for ${TARGET} was not produced."
exit 1
fi

tmpdir="$(mktemp -d)"

case "$archive_path" in
*.tar.xz)
tar -xJf "$archive_path" -C "$tmpdir" --strip-components=1
;;
*.tar.gz)
tar -xzf "$archive_path" -C "$tmpdir" --strip-components=1
;;
*)
echo "unsupported archive format: $archive_path"
exit 1
;;
esac

"$tmpdir/apple-mail" --version
"$tmpdir/apple-mail" --help

- name: Attest release artifacts
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: target/distrib/*

- name: Upload release artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail

# macOS runners ship Bash 3.2, which has no mapfile.
artifact_paths=()
while IFS= read -r artifact_path; do
artifact_paths+=("$artifact_path")
done < dist-upload-files.txt
if ((${#artifact_paths[@]} == 0)); then
echo "cargo-dist did not report uploadable files."
exit 1
fi

gh release upload "$TAG_NAME" "${artifact_paths[@]}" --clobber

- name: Publish release
- name: Build artifacts on the release tag
if: ${{ steps.release.outputs.release_created == 'true' }}
env:
GH_REPO: ${{ github.repository }}
GH_TOKEN: ${{ github.token }}
run: gh release edit "$TAG_NAME" --draft=false
TAG_NAME: ${{ steps.release.outputs.tag_name }}
run: gh workflow run release_artifacts.yml --ref "$TAG_NAME" -f tag_name="$TAG_NAME"
15 changes: 9 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,18 +54,21 @@ dependency review, and CodeQL.

Releases are managed by Release Please. Merging the release PR updates
`Cargo.toml`, `Cargo.lock`, `.release-please-manifest.json`, and `CHANGELOG.md`,
tags the merge commit, and creates a draft GitHub Release. The release workflow
then tests and builds the `aarch64-apple-darwin` cargo-dist archive, attests its
tags the merge commit, and creates a draft GitHub Release. The `Release Please`
workflow then starts the `Release Artifacts` workflow on that tag, which tests
and builds the `aarch64-apple-darwin` cargo-dist archive, attests its
provenance, and uploads the archive, its checksum, and a shell installer. The
release is published only after every step succeeds; if one fails, it stays a
draft.

To rebuild the assets for an existing tag, run the workflow on that tag so the
attestation records the tagged commit:
To resume a draft release whose artifact run failed, start the same workflow on
the tag so the attestation records the tagged commit:

```bash
tag=apple-mail-cli-v0.1.1
gh workflow run release_please.yml --ref "$tag" -f tag_name="$tag"
gh workflow run release_artifacts.yml --ref "$tag" -f tag_name="$tag"
```

The workflow fails before building when it is started from any other ref.
The workflow fails before building when it is started from any other ref or
when the release is already published. A published release is never rebuilt in
place; ship a fix as a new release.
Loading