From 1c70a5c503ce02babd8747db311cd6c599371c83 Mon Sep 17 00:00:00 2001 From: zawakin Date: Sun, 28 Jun 2026 22:26:35 +0900 Subject: [PATCH] ci: add release flow modeled on git-workflow Add a release flow in the same style as the git-workflow repo, adapted to rep (single binary, no crates.io publish): - /release skill: bump Cargo.toml, run rep:verify, commit, tag vX.X.X, create the GitHub release. - .github/workflows/release.yml: on a v* tag, verify the tag matches the Cargo.toml version, build binaries for macOS/Linux/Windows (x64+ARM64), and attach them plus install.sh to the GitHub release. - .github/workflows/ci.yml: test/fmt/clippy/build/lockfile on push and PR. - install.sh: platform-detecting installer for the prebuilt binaries. - README install section; CLAUDE.md release section + write-exception note; tasks.md release pointer. Co-Authored-By: Claude Opus 4.8 (1M context) --- .claude/skills/release/SKILL.md | 72 ++++++++++++++++++++++ .github/workflows/ci.yml | 78 +++++++++++++++++++++++ .github/workflows/release.yml | 106 ++++++++++++++++++++++++++++++++ CLAUDE.md | 21 ++++++- README.md | 18 ++++++ docs/operations/tasks.md | 6 ++ install.sh | 96 +++++++++++++++++++++++++++++ 7 files changed, 394 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/release/SKILL.md create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml create mode 100755 install.sh diff --git a/.claude/skills/release/SKILL.md b/.claude/skills/release/SKILL.md new file mode 100644 index 0000000..99b34e1 --- /dev/null +++ b/.claude/skills/release/SKILL.md @@ -0,0 +1,72 @@ +--- +name: release +description: Release a new version of rep +argument-hint: +allowed-tools: Bash(mise*), Bash(git*), Bash(gh*), Read, Edit, Grep +--- + +# rep Release + +Release version `$ARGUMENTS`. + +## Steps + +First, gather current state: +- Run `grep '^version' Cargo.toml` to check the current version +- Run `git describe --tags --abbrev=0` to get the latest tag (may be empty for the first release) +- Run `git log ..HEAD --oneline` to see commits since the last release + +1. **Validate version** + - Verify the version format (e.g., 0.1.1) + - Ensure the tag `vX.X.X` does not already exist + - Ensure you are on `main` with a clean tree, synced with `origin/main` + +2. **Update Cargo.toml** + - Update `version = "X.X.X"` to the new version + +3. **Quality checks** + ```bash + mise run rep:verify + ``` + This runs fmt, check, lint, test, and build. + +4. **Commit** + - Message: `chore: release vX.X.X` + +5. **Generate release notes** + Analyze commits since the last release and produce notes in this format: + + ```markdown + ## What's Changed + + ### Features + - Commits starting with feat: + + ### Bug Fixes + - Commits starting with fix: + + ### Other Changes + - Other commits (chore, docs, refactor, etc.) + + **Full Changelog**: https://github.com/lanegrid/rep/compare/v{prev}...v{new} + ``` + +6. **Create tag and push** + ```bash + git tag vX.X.X + git push origin main + git push origin vX.X.X + ``` + +7. **Create GitHub release** + ```bash + gh release create vX.X.X --title "vX.X.X" --notes "release notes here" + ``` + +## Notes + +- Pushing the `vX.X.X` tag triggers the Release workflow, which builds + multi-target binaries and attaches them (plus `install.sh`) to the GitHub + release created in step 7. +- The release flow does not publish to crates.io. +- Highlight breaking changes if any. Keep release notes concise but informative. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..33dd625 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,78 @@ +name: CI + +on: + push: + branches: [main] + paths: + - "src/**" + - "tests/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/ci.yml" + pull_request: + branches: [main] + paths: + - "src/**" + - "tests/**" + - "Cargo.toml" + - "Cargo.lock" + - ".github/workflows/ci.yml" + +env: + CARGO_TERM_COLOR: always + +jobs: + test: + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Run tests + run: cargo test --verbose + + fmt: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Check formatting + run: cargo fmt --all -- --check + + lockfile: + name: Cargo.lock + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - name: Check Cargo.lock is up to date + run: cargo check --locked + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + components: clippy + - uses: Swatinem/rust-cache@v2 + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + build: + name: Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: Swatinem/rust-cache@v2 + - name: Build release + run: cargo build --release --verbose diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..7c99149 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,106 @@ +name: Release + +on: + push: + tags: + - 'v*' + +env: + CARGO_TERM_COLOR: always + +jobs: + verify-version: + name: Verify version matches tag + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Verify Cargo.toml version matches tag + run: | + TAG_VERSION=${GITHUB_REF#refs/tags/v} + CARGO_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/') + if [ "$TAG_VERSION" != "$CARGO_VERSION" ]; then + echo "Tag version ($TAG_VERSION) does not match Cargo.toml version ($CARGO_VERSION)" + exit 1 + fi + + build: + name: Build ${{ matrix.target }} + needs: verify-version + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - target: x86_64-apple-darwin + os: macos-latest + - target: aarch64-apple-darwin + os: macos-latest + - target: x86_64-unknown-linux-gnu + os: ubuntu-latest + - target: aarch64-unknown-linux-gnu + os: ubuntu-latest + - target: x86_64-pc-windows-msvc + os: windows-latest + steps: + - uses: actions/checkout@v4 + + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install cross-compilation tools (Linux ARM64) + if: matrix.target == 'aarch64-unknown-linux-gnu' + run: | + sudo apt-get update + sudo apt-get install -y gcc-aarch64-linux-gnu + + - name: Build + run: cargo build --release --target ${{ matrix.target }} + env: + CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc + + - name: Package (Unix) + if: runner.os != 'Windows' + run: | + cd target/${{ matrix.target }}/release + tar czvf ../../../rep-${{ matrix.target }}.tar.gz rep + cd ../../.. + + - name: Package (Windows) + if: runner.os == 'Windows' + run: | + cd target/${{ matrix.target }}/release + 7z a ../../../rep-${{ matrix.target }}.zip rep.exe + cd ../../.. + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: rep-${{ matrix.target }} + path: rep-${{ matrix.target }}.* + + github-release: + name: Create GitHub Release + runs-on: ubuntu-latest + needs: build + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Prepare release assets + run: | + mkdir -p release + find artifacts -type f \( -name "*.tar.gz" -o -name "*.zip" \) -exec mv {} release/ \; + cp install.sh release/ + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + files: release/* diff --git a/CLAUDE.md b/CLAUDE.md index 363d166..e9245c6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,9 +26,10 @@ call must follow these rules. `docs/operations/tasks.md`. 3. **Raw bash is allowed only when unavoidable, and only read-only.** Observation commands (`ls`, `cat`, `grep`, `git status`, `git log`, …) may be raw. Raw - bash that writes or has side effects is forbidden. The sole write exception is - `commit` / `push` / `gh pr create`, managed by the git-workflow skill — follow - `/git-workflow`. + bash that writes or has side effects is forbidden. The write exceptions are + the operations owned by a managed flow: `commit` / `push` / `gh pr create` + (the git-workflow skill — follow `/git-workflow`) and the version bump / tag / + `gh release create` of a release (the release skill — follow `/release`). 4. **Do not make output lossy.** Do not suppress with `2>/dev/null` / `|| true`, and do not trim with `tail` / `head` / pipes. Read the full output of a `mise run` (including errors) and judge from it. @@ -82,6 +83,20 @@ defined in `mise.toml` and documented in `docs/operations/tasks.md`. Always run `mise run rep:verify` before committing to ensure formatting, clippy, tests, and the build all pass. +### Releasing + +Use the `/release` skill to cut a new version: + +``` +/release +``` + +It bumps `Cargo.toml`, runs `mise run rep:verify`, commits `chore: release +vX.X.X`, tags `vX.X.X`, and creates the GitHub release. Pushing the tag triggers +`.github/workflows/release.yml`, which builds multi-target binaries and attaches +them (plus `install.sh`) to the release. CI (`.github/workflows/ci.yml`) runs +test / fmt / clippy / build / lockfile on every push and PR to `main`. + ## Project structure ```text diff --git a/README.md b/README.md index c6c1d76..35067c4 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,24 @@ flow, the JSON explains it. Failures emit a `rep.error.v1` envelope on stdout: A `plan` that would change nothing exits `2` and writes no artifacts. +## Installation + +```bash +# Quick install (macOS/Linux) +curl -fsSL https://github.com/lanegrid/rep/releases/latest/download/install.sh | sh + +# From source +git clone https://github.com/lanegrid/rep.git +cd rep +cargo install --path . +``` + +Pre-built binaries are attached to each [GitHub release](https://github.com/lanegrid/rep/releases) for: + +- macOS (Intel & Apple Silicon) +- Linux (x64 & ARM64) +- Windows (x64) + ## Development This repository uses [mise](https://mise.jdx.dev/) for task running; see diff --git a/docs/operations/tasks.md b/docs/operations/tasks.md index c3081a9..8fe9d8f 100644 --- a/docs/operations/tasks.md +++ b/docs/operations/tasks.md @@ -27,6 +27,12 @@ Tasks are named `:`: | `mise run rep:build:release` | Build the release binary | | `mise run rep:install` | Build and install `rep` locally (dogfooding) | +## Releasing + +Releases are not a mise task — use the `/release` skill (`/release `). +It bumps the version, runs `mise run rep:verify`, commits, tags `vX.X.X`, and +creates the GitHub release; pushing the tag triggers the binary-build workflow. + ## Adding a task When you need a command that no task covers, add a task rather than running raw diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..8d3c2c3 --- /dev/null +++ b/install.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# rep installer +# Usage: curl -fsSL https://github.com/lanegrid/rep/releases/latest/download/install.sh | sh + +set -e + +REPO="lanegrid/rep" +INSTALL_DIR="${INSTALL_DIR:-$HOME/.local/bin}" + +# Detect OS and architecture +detect_platform() { + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + + case "$OS" in + darwin) + case "$ARCH" in + x86_64) PLATFORM="x86_64-apple-darwin" ;; + arm64) PLATFORM="aarch64-apple-darwin" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + ;; + linux) + case "$ARCH" in + x86_64) PLATFORM="x86_64-unknown-linux-gnu" ;; + aarch64) PLATFORM="aarch64-unknown-linux-gnu" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + ;; + *) + echo "Unsupported OS: $OS" + echo "For Windows, download manually from:" + echo " https://github.com/$REPO/releases/latest" + exit 1 + ;; + esac +} + +# Get latest release version +get_latest_version() { + curl -fsSL "https://api.github.com/repos/$REPO/releases/latest" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/' +} + +main() { + echo "Installing rep..." + + detect_platform + VERSION=$(get_latest_version) + + if [ -z "$VERSION" ]; then + echo "Failed to get latest version" + exit 1 + fi + + echo " Version: $VERSION" + echo " Platform: $PLATFORM" + echo " Install to: $INSTALL_DIR" + + # Create install directory + mkdir -p "$INSTALL_DIR" + + # Download and extract + ARCHIVE="rep-$PLATFORM.tar.gz" + URL="https://github.com/$REPO/releases/download/$VERSION/$ARCHIVE" + + echo " Downloading from $URL..." + + TEMP_DIR=$(mktemp -d) + trap 'rm -rf "$TEMP_DIR"' EXIT + + curl -fsSL "$URL" -o "$TEMP_DIR/$ARCHIVE" + tar -xzf "$TEMP_DIR/$ARCHIVE" -C "$TEMP_DIR" + + # Install binary + mv "$TEMP_DIR/rep" "$INSTALL_DIR/" + chmod +x "$INSTALL_DIR/rep" + + echo "" + echo "Installed successfully!" + echo "" + + # Check if install dir is in PATH + case ":$PATH:" in + *":$INSTALL_DIR:"*) ;; + *) + echo "Add $INSTALL_DIR to your PATH:" + echo "" + echo " export PATH=\"\$PATH:$INSTALL_DIR\"" + echo "" + ;; + esac + + echo "Run 'rep --help' to get started." +} + +main