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
66 changes: 66 additions & 0 deletions .github/workflows/prepare-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
name: Prepare release

# Manually triggered. Computes the next version (or uses an explicit one), writes it
# into every package via `just set-version`, regenerates the CHANGELOG, and opens a
# release PR. Merging that PR to master triggers release.yml (build + GitHub Release).
#
# Requires a repo secret `RELEASE_PAT` (a PAT with `contents` + `pull-requests` write):
# PRs opened with the default GITHUB_TOKEN do NOT trigger CI, so the release PR's checks
# ("all checks before release") would never run. The PAT makes the PR trigger CI.
on:
workflow_dispatch:
inputs:
version:
description: "Explicit version X.Y.Z (leave blank to compute from commits)"
required: false
type: string

permissions:
contents: write
pull-requests: write

jobs:
prepare:
name: Open release PR
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.RELEASE_PAT }}
- name: Set up mise tools (just, convco)
uses: jdx/mise-action@v2
- name: Resolve version
id: v
run: |
set -euo pipefail
version="${{ inputs.version }}"
if [ -z "$version" ]; then
version="$(convco version --bump)"
fi
echo "Resolved release version: $version"
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Apply version + regenerate changelog
run: |
set -euo pipefail
just set-version "${{ steps.v.outputs.version }}"
just changelog "${{ steps.v.outputs.version }}"
- name: Open release PR
env:
GH_TOKEN: ${{ secrets.RELEASE_PAT }}
run: |
set -euo pipefail
version="${{ steps.v.outputs.version }}"
branch="release/v${version}"
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -c "$branch"
git commit -am "chore(release): v${version}"
git push -u origin "$branch" --force-with-lease
gh pr create --base master --head "$branch" \
--title "chore(release): v${version}" \
--body "Automated release PR for **v${version}**.

Review the CHANGELOG and version bumps, then **merge** to tag and publish the release (release.yml builds the \`capsule\` CLI binaries and creates the GitHub Release).

Part of the release automation (#18)."
139 changes: 139 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
name: Release

# Fires when a release commit (`chore(release): vX.Y.Z`, produced by prepare-release.yml
# and merged via its PR) lands on master. It builds the `capsule` CLI for each target and
# publishes a GitHub Release. `gh release create` also creates the tag, so the whole
# build+publish happens in this one run — no PAT or tag-push re-trigger needed.
on:
push:
branches: [master]

permissions:
contents: write

jobs:
detect:
name: Detect release
runs-on: ubuntu-latest
outputs:
release: ${{ steps.d.outputs.release }}
version: ${{ steps.d.outputs.version }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- id: d
run: |
set -euo pipefail
version="$(grep -m1 '^version = ' Cargo.toml | sed -E 's/^version = "(.*)"/\1/')"
echo "version=$version" >> "$GITHUB_OUTPUT"
if git rev-parse -q --verify "refs/tags/v${version}" >/dev/null; then
echo "Tag v${version} already exists — nothing to release."
echo "release=false" >> "$GITHUB_OUTPUT"; exit 0
fi
if git log --format='%s' '${{ github.event.before }}..${{ github.event.after }}' \
| grep -qx "chore(release): v${version}"; then
echo "Release commit for v${version} detected."
echo "release=true" >> "$GITHUB_OUTPUT"
else
echo "No release commit in this push; skipping."
echo "release=false" >> "$GITHUB_OUTPUT"
fi

build:
name: Build capsule (${{ matrix.target }})
needs: detect
if: ${{ needs.detect.outputs.release == 'true' }}
runs-on: ${{ matrix.os }}
# Windows is best-effort (mirrors CI's Tier-2 treatment); a failure there doesn't
# block the release of the other binaries.
continue-on-error: ${{ matrix.optional }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
optional: false
- target: aarch64-apple-darwin
os: macos-14
optional: false
- target: x86_64-apple-darwin
os: macos-14
optional: false
- target: x86_64-pc-windows-msvc
os: windows-latest
optional: true
steps:
- uses: actions/checkout@v4
- name: Install protoc
uses: arduino/setup-protoc@v3
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
- name: Install Rust toolchain
run: |
rustup toolchain install
rustup target add ${{ matrix.target }}
- name: Cache cargo
uses: Swatinem/rust-cache@v2
with:
key: release-${{ matrix.target }}
- name: Build release binary
run: cargo build -p capsule-cli --release --target ${{ matrix.target }}
- name: Package (unix)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
dist="capsule-v${{ needs.detect.outputs.version }}-${{ matrix.target }}"
mkdir -p "$dist"
cp "target/${{ matrix.target }}/release/capsule" "$dist/"
cp README.md LICENSE CHANGELOG.md "$dist/" 2>/dev/null || true
tar -czf "${dist}.tar.gz" "$dist"
echo "ASSET=${dist}.tar.gz" >> "$GITHUB_ENV"
- name: Package (windows)
if: runner.os == 'Windows'
shell: bash
run: |
set -euo pipefail
dist="capsule-v${{ needs.detect.outputs.version }}-${{ matrix.target }}"
mkdir -p "$dist"
cp "target/${{ matrix.target }}/release/capsule.exe" "$dist/"
7z a "${dist}.zip" "$dist" >/dev/null
echo "ASSET=${dist}.zip" >> "$GITHUB_ENV"
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: capsule-${{ matrix.target }}
path: ${{ env.ASSET }}
if-no-files-found: error

publish:
name: Publish GitHub Release
needs: [detect, build]
if: ${{ needs.detect.outputs.release == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up mise tools (convco)
uses: jdx/mise-action@v2
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
path: dist
merge-multiple: true
- name: Generate release notes
run: convco changelog -m 1 --unreleased "${{ needs.detect.outputs.version }}" > NOTES.md
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -euo pipefail
version="${{ needs.detect.outputs.version }}"
gh release create "v${version}" \
--target "${{ github.sha }}" \
--title "v${version}" \
--notes-file NOTES.md \
dist/*
34 changes: 33 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ First off, thank you for considering contributing! It’s people like you who ma
3. **Tests:** Ensure all existing tests pass and add new ones for any new features or bug fixes.
4. **Pull Request:** Open a PR against the `master` branch. Clearly describe *what* changed and *why*. You may use available PR templates as necessary. Keeping code changes within reasonable size will help us get it reviewed better and merged sooner.

## Local Setup

Tooling versions are pinned with [mise](https://mise.jdx.dev). From the repo root:

```sh
mise install # installs just, convco, lefthook (+ language tools)
just hooks-install # wires up the git hooks (lefthook)
```

The hooks run formatters/linters on commit and validate that every commit message is a
[Conventional Commit](https://www.conventionalcommits.org) (via `convco`) — so `convco`
must be on your `PATH` (hence `mise install`). The same check runs on `pre-push` and on
every PR in CI.

## Baseline for Ownership & Provenance

To maintain high security and legal standards, we require all merge commits to be signed at minimum. However, it is still strongly recommend that you sign all your Git commits if you don't already (takes 5 minutes to setup)!
Expand All @@ -18,11 +32,29 @@ While we will still accept PRs with unsigned commits, to maintain transparent ow
## Coding Standards

* Linting: If you have LSPs configured in your editor for the various languages in your repo, it should lint using the appropriate tool with correct versions by default.
* Commit messaging: Please use **Semantic Commits** (e.g., `feat:`, `fix:`, `docs:`, `test:`) to help us automate our changelog.
* Commit messaging: **Semantic Commits** (e.g., `feat:`, `fix:`, `docs:`, `test:`) are **required** — they drive version bumps and the changelog, and are enforced by `convco` on commit, push, and in CI.
* Development Patterns: Refer to [Development](/capsule-docs/src/content/docs/development/).
* AI usage: Refer to [AI.md](./AI.md).

### Contributor Checklist

* [ ] I have signed the [Contributor License Agreement](CLA.md).
* [ ] My code follows the project's style guidelines.

## Releasing

Releases are automated from Conventional Commits; one version is kept in sync across every
package (`just set-version` / the `xtask` crate).

1. **Prepare** — run the **Prepare release** workflow (`workflow_dispatch`). It computes the
next version with `convco` (or takes an explicit one), writes it into every package,
regenerates `CHANGELOG.md`, and opens a `release/vX.Y.Z` PR.
2. **Review** — review/edit the CHANGELOG and version bumps on that PR. The full CI gate
runs on it ("all checks before release").
3. **Merge** — merging the PR lands `chore(release): vX.Y.Z` on `master`, which triggers
**release.yml**: it builds the `capsule` CLI binaries (Linux/macOS, Windows best-effort)
and publishes a GitHub Release (the tag is created as part of this).

**One-time setup:** add a repo secret `RELEASE_PAT` (a PAT with `contents` + `pull-requests`
write). It's used only to open the release PR — PRs opened with the default token don't
trigger CI, so without it the release PR's checks wouldn't run.
6 changes: 4 additions & 2 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -429,9 +429,11 @@ set-version version:
cargo run -q -p xtask -- set-version {{ version }}

# Regenerate CHANGELOG.md from Conventional Commits. Hand-edits land in the release PR.
# Pass the version when cutting a release so its section is titled (e.g. `just changelog 0.2.0`);
# the default leaves the newest section as "Unreleased".
[group('release')]
changelog:
convco changelog > CHANGELOG.md
changelog title="Unreleased":
convco changelog --unreleased "{{ title }}" > CHANGELOG.md

# ── Setup ────────────────────────────────────────────────────────────────────

Expand Down
Loading