diff --git a/.github/workflows/release-plz.yml b/.github/workflows/release-plz.yml index cd12b909..1e866ac1 100644 --- a/.github/workflows/release-plz.yml +++ b/.github/workflows/release-plz.yml @@ -58,6 +58,48 @@ jobs: env: GITHUB_TOKEN: ${{ steps.app-token.outputs.token }} CARGO_REGISTRY_TOKEN: ${{ steps.app-token.outputs.cargo-registry-token }} + # Whole-repo CHANGELOG via git-cliff (cliff.toml). release-plz is + # package-path-scoped and skips release=false app crates; git-cliff is the + # GoReleaser-style "every conventional commit since last tag" path. + - name: Write root changelog with git-cliff + env: + GH_TOKEN: ${{ steps.app-token.outputs.token }} + run: | + set -euo pipefail + branch="$( + gh pr list \ + --repo "${{ github.repository }}" \ + --state open \ + --json headRefName \ + --jq '.[] | select(.headRefName | startswith("release-plz/")) | .headRefName' \ + | head -n1 + )" + if [[ -z "${branch}" ]]; then + echo "No open release-plz PR — nothing to write." + exit 0 + fi + git config user.name "aprilnea[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git remote set-url origin "https://x-access-token:${GH_TOKEN}@github.com/${{ github.repository }}.git" + # Script/config may not be on the release-plz branch yet — copy from master. + cp cliff.toml "${RUNNER_TEMP}/cliff.toml" + cp scripts/release/write-changelog.sh "${RUNNER_TEMP}/write-changelog.sh" + chmod +x "${RUNNER_TEMP}/write-changelog.sh" + git fetch origin "${branch}" + git checkout --force "origin/${branch}" + cp "${RUNNER_TEMP}/cliff.toml" cliff.toml + # git-cliff: same binary release-plz uses under the hood for formatting. + curl -sL "https://github.com/orhun/git-cliff/releases/download/v2.13.1/git-cliff-2.13.1-x86_64-unknown-linux-gnu.tar.gz" \ + | tar -xz -C "${RUNNER_TEMP}" + export PATH="${RUNNER_TEMP}/git-cliff-2.13.1:${PATH}" + "${RUNNER_TEMP}/write-changelog.sh" + if git diff --quiet -- CHANGELOG.md; then + echo "CHANGELOG already complete." + exit 0 + fi + git add CHANGELOG.md cliff.toml 2>/dev/null || git add CHANGELOG.md + git commit -m "chore(release): write whole-repo changelog" + git push origin "HEAD:refs/heads/${branch}" # `release-plz/action` swallows a release-pr HTTP 422 as a warning and # reports no PR, which silently stalls releases (it looks identical to a # quiet week of commits). Fail loudly when release-plz opened/updated no @@ -131,11 +173,16 @@ jobs: } core.info(`No release PR and no release-worthy commits since ${lastTag || "repo start"}; nothing to release.`); - # On every push to master, publishes any crate whose manifest version is not yet - # on crates.io — i.e. a no-op until the release PR is merged, at which point it - # publishes the whole workspace and cuts one `v{version}` tag + GitHub Release. + # Publishes crates + cuts `v{version}` only from the release PR merge commit + # (`chore: release v*`). release_always=false in release-plz.toml is the primary + # gate; this job-level filter is defense in depth so a later feature push cannot + # tag HEAD after a failed-then-retried crates.io publish. On publish failure, + # re-run this workflow on the release commit SHA — never on a later master tip. release: name: release-plz release + if: >- + github.event_name == 'workflow_dispatch' || + startsWith(github.event.head_commit.message, 'chore: release') runs-on: ubuntu-latest permissions: contents: write @@ -145,25 +192,80 @@ jobs: with: fetch-depth: 0 persist-credentials: false + # Pin to the version-bump commit even if workflow_dispatch is fired from a + # later master tip (or a re-run that somehow resolves to the wrong SHA). + - name: Check out the version-bump commit + shell: bash + run: | + set -euo pipefail + version="$( + python3 - <<'PY' + import pathlib, re, sys + text = pathlib.Path("Cargo.toml").read_text() + m = re.search( + r'(?ms)^\[workspace\.package\].*?^version\s*=\s*"([^"]+)"', + text, + ) + if not m: + sys.exit("workspace.package version not found in Cargo.toml") + print(m.group(1)) + PY + )" + tag="v${version}" + if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then + echo "Tag ${tag} already exists — nothing to release." + echo "skip=true" >> "$GITHUB_ENV" + exit 0 + fi + # Prefer the conventional release-PR squash subject; fall back to the + # first commit that set this workspace version in Cargo.toml. + bump_sha="$(git log -1 --format=%H --grep="^chore: release v${version}" || true)" + if [[ -z "${bump_sha}" ]]; then + bump_sha="$( + git log -G '^version = "' --format=%H -- Cargo.toml \ + | while read -r sha; do + if git show "${sha}:Cargo.toml" \ + | python3 -c "import pathlib,re,sys; t=sys.stdin.read(); m=re.search(r'(?ms)^\[workspace\.package\].*?^version\s*=\s*\"([^\"]+)\"', t); sys.exit(0 if m and m.group(1)==sys.argv[1] else 1)" \ + "${version}" + then + echo "${sha}" + break + fi + done + )" + fi + if [[ -z "${bump_sha}" ]]; then + echo "::error::Could not locate the version-bump commit for ${tag}" + exit 1 + fi + echo "Checking out version-bump commit ${bump_sha} for ${tag}" + git checkout --force "${bump_sha}" + echo "skip=false" >> "$GITHUB_ENV" + echo "release_sha=${bump_sha}" >> "$GITHUB_ENV" - uses: dtolnay/rust-toolchain@stable + if: env.skip != 'true' - uses: Swatinem/rust-cache@v2 + if: env.skip != 'true' with: prefix-key: v1-rust shared-key: linux-stable-debug # CI master is the canonical writer; release-plz only restores. save-if: false - name: Install Linux build deps + if: env.skip != 'true' run: | sudo apt-get update sudo apt-get install -y \ libudev-dev pkg-config gcc g++ clang libssl-dev libzstd-dev - name: Mint GitHub App token + if: env.skip != 'true' id: app-token uses: ./.github/actions/github-app-token-from-1password with: op-service-account-token: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }} op-github-app-item: ${{ secrets.OP_GITHUB_APP_ITEM }} - name: Run release-plz (release) + if: env.skip != 'true' uses: release-plz/action@v0.5 with: command: release diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 00000000..19f6601a --- /dev/null +++ b/cliff.toml @@ -0,0 +1,51 @@ +# Whole-repo changelog (git-cliff). release-plz only bumps versions; it is +# package-path-scoped and cannot see `release = false` app crates, so the root +# CHANGELOG is owned here — same model as GoReleaser's `changelog.use: git`. +# https://git-cliff.org/docs/configuration + +[changelog] +header = """# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +""" +# Keep-a-Changelog layout matching historical OpenLogi sections. +body = """ +## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + +{% for group, commits in commits | group_by(attribute="group") -%} +### {{ group | upper_first }} + +{% for commit in commits -%} +- {% if commit.scope %}*({{ commit.scope }})* {% endif %}{% if commit.breaking %}[**breaking**] {% endif %}{{ commit.message }} +{% endfor %} +{% endfor -%} +""" +trim = true + +[git] +conventional_commits = true +filter_unconventional = true +require_conventional = true +split_commits = false +commit_preprocessors = [ + { pattern = "\\(#([0-9]+)\\)", replace = "([#${1}](https://github.com/AprilNEA/OpenLogi/pull/${1}))" }, +] +commit_parsers = [ + { message = "^feat", group = "Added" }, + { message = "^fix", group = "Fixed" }, + { message = "^perf", group = "Changed" }, + { message = "^security", group = "Security" }, + { message = "^.*", skip = true }, +] +protect_breaking_commits = true +filter_commits = false +tag_pattern = "v[0-9].*" +sort_commits = "newest" +# No include_path / exclude_path — every conventional commit since the last tag +# counts, including gui/agent work that never touches a crates.io package. diff --git a/release-plz.toml b/release-plz.toml index 6605b7d6..ddb2a356 100644 --- a/release-plz.toml +++ b/release-plz.toml @@ -14,10 +14,11 @@ # could attach assets — "target_commitish cannot be changed when release is # immutable". Keep `git_release_enable = false` so release.yml owns the lifecycle.) # -# Single changelog: every crate points `changelog_path` at the repo-root -# CHANGELOG.md, so release-plz aggregates all crates' sections into that one file -# instead of scattering a CHANGELOG.md into each crate directory. (changelog_path -# is per-package only — it can't be set in [workspace].) +# Changelog: release-plz is package-path-scoped and skips `release = false` app +# crates (gui/agent), so it does NOT own CHANGELOG.md. Whole-repo notes are +# written by git-cliff via `cliff.toml` after each release-pr (same idea as +# GoReleaser's `changelog.use: git` — every conventional commit since the last +# tag, no per-crate path filter). [workspace] # Open release PRs from a `release-plz/`-prefixed branch. @@ -29,11 +30,16 @@ semver_check = false # Per-crate tags/releases are off; the root crate owns the one workspace release. git_tag_enable = false git_release_enable = false +# Version bumps only — root CHANGELOG.md is written by git-cliff (cliff.toml). +changelog_update = false +# Only publish/tag when the release PR merges (branch prefix above). A later +# master tip after a failed crates.io publish must not cut v{version} — re-run +# the failed release job on that same SHA once credentials are fixed. +release_always = false [[package]] name = "openlogi" version_group = "openlogi" -changelog_path = "CHANGELOG.md" git_tag_enable = true git_tag_name = "v{{ version }}" # GitHub Release is owned by release.yml (softprops), not release-plz — see header. @@ -42,62 +48,47 @@ git_release_enable = false [[package]] name = "openlogi-core" version_group = "openlogi" -changelog_path = "CHANGELOG.md" # OS input-event synthesis split out of openlogi-core (depends on it). Published # with the workspace under unified versioning. [[package]] name = "openlogi-inject" version_group = "openlogi" -changelog_path = "CHANGELOG.md" [[package]] name = "openlogi-hid" version_group = "openlogi" -changelog_path = "CHANGELOG.md" [[package]] name = "openlogi-assets" version_group = "openlogi" -changelog_path = "CHANGELOG.md" [[package]] name = "openlogi-cli" version_group = "openlogi" -changelog_path = "CHANGELOG.md" [[package]] name = "openlogi-hook" version_group = "openlogi" -changelog_path = "CHANGELOG.md" # Vendored fork of the `hidpp` crate (0BSD, from lus/logy). Published with the # workspace under unified versioning; upstream's 0.3.0 is provenance only. [[package]] name = "openlogi-hidpp" version_group = "openlogi" -changelog_path = "CHANGELOG.md" -# Not publishable (git-only gpui deps); keep release-plz out of it entirely. -# Its version still follows the shared workspace version via inheritance. -# `publish = false` must mirror the crate's Cargo.toml: release-plz validates -# publish consistency across *all* workspace packages before honoring `release`. +# App crates: not crates.io packages (git gpui deps / login-item binary). +# `publish = false` must mirror each crate's Cargo.toml. [[package]] name = "openlogi-gui" release = false publish = false -# Shared headless orchestration for the background agent. Not publishable -# (links git-only gpui-adjacent siblings); version follows the shared workspace -# version by inheritance. `publish = false` mirrors the crate's Cargo.toml — see -# the openlogi-gui note above. [[package]] name = "openlogi-agent-core" release = false publish = false -# The headless background agent binary — shipped as a login-item helper inside -# the .app, never published. publish=false mirrors its Cargo.toml. [[package]] name = "openlogi-agent" release = false diff --git a/scripts/release/write-changelog.sh b/scripts/release/write-changelog.sh new file mode 100755 index 00000000..2c4df7fa --- /dev/null +++ b/scripts/release/write-changelog.sh @@ -0,0 +1,57 @@ +#!/usr/bin/env bash +# Write the next workspace version section into CHANGELOG.md with git-cliff. +# Whole-repo conventional commits since the previous v* tag (cliff.toml). +set -euo pipefail + +root="$(git rev-parse --show-toplevel)" +cd "$root" + +version="$( + python3 - <<'PY' +import pathlib, re, sys +text = pathlib.Path("Cargo.toml").read_text() +m = re.search(r'(?ms)^\[workspace\.package\].*?^version\s*=\s*"([^"]+)"', text) +if not m: + sys.exit("workspace.package version not found in Cargo.toml") +print(m.group(1)) +PY +)" +tag="v${version}" + +last_tag="$( + git tag --list 'v*' \ + | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n1 +)" +if [[ -z "${last_tag}" ]]; then + echo "error: no previous vX.Y.Z tag" >&2 + exit 1 +fi +if [[ "${last_tag}" == "${tag}" ]]; then + echo "error: workspace version ${version} is already tagged as ${tag}" >&2 + exit 1 +fi + +# Drop a stale section for this version (idempotent re-runs / release-pr updates). +if grep -qE "^## \[${version}\]" CHANGELOG.md; then + python3 - "${version}" <<'PY' +from pathlib import Path +import re +import sys + +version = sys.argv[1] +text = Path("CHANGELOG.md").read_text() +pattern = re.compile( + rf"(?ms)^## \[{re.escape(version)}\].*?(?=^## \[|\Z)" +) +Path("CHANGELOG.md").write_text(pattern.sub("", text, count=1)) +PY +fi + +git cliff "${last_tag}.." \ + --config cliff.toml \ + --tag "${tag}" \ + --prepend CHANGELOG.md + +echo "wrote ${tag} changelog from ${last_tag}..HEAD" >&2