diff --git a/.github/workflows/release-pr.yml b/.github/workflows/release-pr.yml new file mode 100644 index 0000000..40fcab6 --- /dev/null +++ b/.github/workflows/release-pr.yml @@ -0,0 +1,39 @@ +name: Release PR + +# Creates a release pull request that bumps cmd/sava/version.txt. +# Merging that PR is the release approval: release.yml then builds, +# tags, and publishes using the PR body as release notes. +# +# Note: PRs created with GITHUB_TOKEN do not trigger other workflows, +# so no CI runs on the release PR itself. The diff is version.txt +# only, and release.yml runs go test before publishing. + +on: + workflow_dispatch: + inputs: + bump: + description: Version bump level + type: choice + options: + - patch + - minor + - major + default: patch + +permissions: + contents: write + pull-requests: write + +jobs: + release-pr: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Create release PR + env: + GH_TOKEN: ${{ github.token }} + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + make release-pr BUMP="${{ inputs.bump }}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5fd33df --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: Release + +# Runs when a release PR (created by release-pr.yml) is merged: +# builds binaries, creates the version tag, and publishes a GitHub +# Release using the merged PR body as release notes. + +on: + push: + branches: [main] + paths: ['cmd/sava/version.txt'] + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Read version + id: version + run: echo "tag=$(cat cmd/sava/version.txt)" >> "$GITHUB_OUTPUT" + + - name: Skip if already released + id: guard + env: + TAG: ${{ steps.version.outputs.tag }} + run: | + if git ls-remote --tags origin "refs/tags/$TAG" | grep -q .; then + echo "Tag $TAG already exists; skipping release." + echo "exists=true" >> "$GITHUB_OUTPUT" + fi + + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5.6.0 + if: steps.guard.outputs.exists != 'true' + with: + go-version-file: go.mod + + - name: Test + if: steps.guard.outputs.exists != 'true' + run: go test ./... + + - name: Build binaries + if: steps.guard.outputs.exists != 'true' + run: make release-build TAG="${{ steps.version.outputs.tag }}" + + - name: Publish release + if: steps.guard.outputs.exists != 'true' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.version.outputs.tag }} + run: | + # Release notes = body of the PR that introduced this commit. + gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \ + --jq '.[0].body // empty' > notes.md + if [ ! -s notes.md ]; then + echo "Release $TAG" > notes.md + fi + gh release create "$TAG" dist/* \ + --title "$TAG" --notes-file notes.md --target "$GITHUB_SHA" diff --git a/.gitignore b/.gitignore index 3d741e0..b6d15a0 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,6 @@ docs/.obsidian/ # Go build artifacts /sava + +# Release build artifacts +/dist/ diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4e5a2ae --- /dev/null +++ b/Makefile @@ -0,0 +1,29 @@ +.PHONY: build test fmt vet lint check release-pr release-build + +BUMP ?= patch +TAG ?= $(shell cat cmd/sava/version.txt) + +build: + go build -o sava ./cmd/sava + +test: + go test ./... + +fmt: + test -z "$$(gofmt -l .)" + +vet: + go vet ./... + +lint: + golangci-lint run + +check: fmt vet test lint + +# --- release (scripts run via bash: no exec permission needed) --- + +release-pr: + bash scripts/create-release-pr.sh $(BUMP) + +release-build: + bash scripts/build-release.sh $(TAG) diff --git a/README.md b/README.md index f04cfb1..6679b94 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,21 @@ https://gist.github.com/rn404/decf010fc48d7d8688116af0f4427b44 go install github.com/rn404/nippo-cli/cmd/sava@latest ``` +ビルド済みバイナリ (macOS / Linux) は +[Releases](https://github.com/rn404/nippo-cli/releases) からも取得できます. + +## Release lifecycle + +バージョンの単一の真実は `cmd/sava/version.txt` (`go:embed` でバイナリに埋め込み). + +1. Actions で「Release PR」workflow を dispatch し, bump レベル (patch / minor / major) を選択 +2. `version.txt` を更新した Release PR が自動で作られる (本文には変更点一覧が自動生成される) +3. PR 本文をリリースノートとして整えて, **マージ = リリース承認** +4. マージを検知して release workflow が起動し, テスト・バイナリビルド・タグ作成・GitHub Release 公開まで自動実行 (マージ時点の PR 本文がそのままリリースノートになる) + +リリース処理の実体は `scripts/` にあり, Makefile 経由でローカルでも実行できます +(例: `make release-build TAG=v0.1.0` で `dist/` にバイナリを生成). + ## Usage ``` @@ -64,11 +79,15 @@ sava clear -a # Run from source go run ./cmd/sava -# Test / format / lint -go test ./... -gofmt -l . -go vet ./... -golangci-lint run # version is pinned in .mise.toml (mise install) +# Test / format / vet / lint at once +make check + +# Individual targets +make test +make fmt +make vet +make lint # golangci-lint: version is pinned in .mise.toml (mise install) +make build ``` ## History diff --git a/cmd/sava/main.go b/cmd/sava/main.go index d9e23a3..a41a262 100644 --- a/cmd/sava/main.go +++ b/cmd/sava/main.go @@ -1,17 +1,23 @@ package main import ( + _ "embed" "fmt" "os" + "strings" "github.com/spf13/cobra" ) const appName = "sava" -// version is overridable at build time: -// go build -ldflags "-X main.version=vX.Y.Z" -var version = "v0.1.0" +// versionFile is the single source of truth for the version, bumped +// by the release PR workflow (.github/workflows/release-pr.yml). +// +//go:embed version.txt +var versionFile string + +var version = strings.TrimSpace(versionFile) func main() { root := newRootCommand() diff --git a/cmd/sava/version.txt b/cmd/sava/version.txt new file mode 100644 index 0000000..ae39fab --- /dev/null +++ b/cmd/sava/version.txt @@ -0,0 +1 @@ +v0.0.0 diff --git a/docs/go-migration-plan.md b/docs/go-migration-plan.md index 335f3cc..08c2e8e 100644 --- a/docs/go-migration-plan.md +++ b/docs/go-migration-plan.md @@ -238,7 +238,9 @@ type Log struct { - [x] **4.2** Phase 0 のサンプルと比較し、機能面のデグレがないことを確認 - 加えて hash バグ・`clear` バグの修正が意図通り機能することを実機確認 - [x] **4.3** README の Usage を Go 版に更新 -- [x] **4.4** `go install` 手順を README に整備(goreleaser は任意のため今回は見送り、必要になったら導入) +- [x] **4.4** `go install` 手順を README に整備 + - 追記: リリース用 CI を移行後に追加。goreleaser は Actions 許可リストへの追加が必要になるため採用せず、GitHub 製アクション + `go build` + `gh` のみで構成 + - ライフサイクル: `release-pr.yml`(dispatch でバージョンバンプ PR を自動作成)→ PR マージ = リリース承認 → `release.yml`(`cmd/sava/version.txt` の変更を検知してテスト・ビルド・タグ・Release 公開、PR 本文がリリースノート) ### Phase 5: Deno 資産の撤去 diff --git a/scripts/build-release.sh b/scripts/build-release.sh new file mode 100644 index 0000000..5c61220 --- /dev/null +++ b/scripts/build-release.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Cross-compile release binaries into dist/ with sha256 checksums. +# Usage: build-release.sh +set -euo pipefail + +cd "$(dirname "$0")/.." +tag="${1:?usage: build-release.sh }" + +rm -rf dist +mkdir -p dist + +for target in darwin/amd64 darwin/arm64 linux/amd64 linux/arm64; do + export GOOS="${target%/*}" GOARCH="${target#*/}" + name="sava_${tag}_${GOOS}_${GOARCH}" + CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" \ + -o "dist/${name}/sava" ./cmd/sava + tar -czf "dist/${name}.tar.gz" -C dist "$name" + rm -r "dist/${name:?}" +done + +(cd dist && shasum -a 256 ./* > checksums.txt) diff --git a/scripts/bump-version.sh b/scripts/bump-version.sh new file mode 100644 index 0000000..baaf4e0 --- /dev/null +++ b/scripts/bump-version.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash +# Print the next version for the given bump level. +# Usage: bump-version.sh +set -euo pipefail + +VERSION_FILE="$(dirname "$0")/../cmd/sava/version.txt" +bump="${1:?usage: bump-version.sh }" + +current="$(cat "$VERSION_FILE")" +IFS=. read -r major minor patch <<< "${current#v}" + +case "$bump" in + major) echo "v$((major + 1)).0.0" ;; + minor) echo "v${major}.$((minor + 1)).0" ;; + patch) echo "v${major}.${minor}.$((patch + 1))" ;; + *) echo "unknown bump level: $bump" >&2; exit 1 ;; +esac diff --git a/scripts/create-release-pr.sh b/scripts/create-release-pr.sh new file mode 100644 index 0000000..19ea6c0 --- /dev/null +++ b/scripts/create-release-pr.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Create a release PR that bumps cmd/sava/version.txt. +# Merging the PR is the release approval (see release.yml). +# Usage: create-release-pr.sh +# Requires: gh (authenticated), git push permission. +set -euo pipefail + +cd "$(dirname "$0")/.." +bump="${1:?usage: create-release-pr.sh }" + +next="$(bash scripts/bump-version.sh "$bump")" +base_sha="$(git rev-parse HEAD)" +repo="$(gh repo view --json nameWithOwner --jq .nameWithOwner)" + +git switch -c "release/$next" +echo "$next" > cmd/sava/version.txt +git commit -am "release: $next" +git push -u origin "release/$next" + +# Prefill the PR body with auto-generated notes; edit it before +# merging — the merged body becomes the release notes. +notes="$(gh api "repos/$repo/releases/generate-notes" \ + -f tag_name="$next" -f target_commitish="$base_sha" --jq .body)" +gh pr create --title "Release $next" --body "$notes"