diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 5884e18..ea79ea2 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -48,12 +48,12 @@ Go module cache is persisted in a Docker volume (`aethel-gomod`) for fast repeat ## Release Process -Two-workflow split in GitHub Actions: +Single workflow (`release.yml`) with two jobs: -1. **`release.yml`** — triggers on push to master. Analyzes conventional commits since last tag, computes version bump (major/minor/patch), updates `VERSION` + `CHANGELOG.md`, commits `chore(release): vX.Y.Z`, creates git tag, pushes. Does NOT build binaries or create GitHub Release. -2. **`goreleaser.yml`** — triggers on tag push (`v*`). Runs GoReleaser to cross-compile 5 platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64), creates `.tar.gz` (Unix) / `.zip` (Windows) archives with both `aethel` + `aetheld`, publishes GitHub Release with SHA256 checksums. +1. **`release` job** — triggers on push to master. Analyzes conventional commits since last tag, computes version bump (major/minor/patch), updates `VERSION` + `CHANGELOG.md`, commits `chore(release): vX.Y.Z`, creates git tag, pushes. Outputs version to the next job. +2. **`goreleaser` job** — runs after `release` job. Checks out the tagged commit, runs GoReleaser to cross-compile 5 platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64), creates `.tar.gz` (Unix) / `.zip` (Windows) archives with both `aethel` + `aetheld`, publishes GitHub Release with SHA256 checksums. -GoReleaser config: `.goreleaser.yml` (version 2). Version injected via `-ldflags "-s -w -X main.version={{.Version}}"` on both binaries. +GoReleaser config: `.goreleaser.yml` (version 2). Version injected via `-ldflags "-s -w -X main.version={{.Version}}"` on both binaries. Note: both jobs are in one workflow because tags pushed with `GITHUB_TOKEN` don't trigger other workflows. Install script: `scripts/install.sh` — POSIX shell, detects OS/arch, downloads from GitHub Releases, verifies checksum, installs to `~/.local/bin/`. diff --git a/.claude/agent-memory/code-reviewer/MEMORY.md b/.claude/agent-memory/code-reviewer/MEMORY.md new file mode 100644 index 0000000..928fe81 --- /dev/null +++ b/.claude/agent-memory/code-reviewer/MEMORY.md @@ -0,0 +1,3 @@ +# Code Reviewer Memory Index + +- [feedback_env_interpolation.md](feedback_env_interpolation.md) — security feedback: pass version strings via `env:` blocks, not raw `${{ }}` interpolation in run scripts diff --git a/.claude/agent-memory/code-reviewer/feedback_env_interpolation.md b/.claude/agent-memory/code-reviewer/feedback_env_interpolation.md new file mode 100644 index 0000000..5eafd85 --- /dev/null +++ b/.claude/agent-memory/code-reviewer/feedback_env_interpolation.md @@ -0,0 +1,11 @@ +--- +name: env_interpolation_security +description: Pass untrusted values via env: blocks in GitHub Actions run steps, not raw ${{ }} interpolation +type: feedback +--- + +Version strings and other potentially untrusted values must be passed through `env:` blocks in GitHub Actions `run:` steps rather than being interpolated directly with `${{ }}` inside shell script bodies. + +**Why:** Script injection risk — if an attacker controls the value (e.g., via a crafted tag), raw interpolation can break out of the shell expression. Env vars are safe because the shell treats them as data, not code. + +**How to apply:** Any time a `${{ }}` expression would appear inside the body of a `run:` block (not in `with:` or `if:` expressions), lift it to an `env:` block and reference it as `$ENV_VAR_NAME` in the script. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b95fbc6..30c8b74 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,9 +18,9 @@ jobs: if: inputs.dry_run == true run: echo "::notice::DRY RUN — this is a manual test of the CI workflow" - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25' diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml deleted file mode 100644 index e6ced11..0000000 --- a/.github/workflows/goreleaser.yml +++ /dev/null @@ -1,40 +0,0 @@ -name: GoReleaser - -on: - push: - tags: ["v*"] - -permissions: - contents: write - -jobs: - goreleaser: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version: '1.25' - - - name: Extract release notes from CHANGELOG.md - run: | - VERSION="${GITHUB_REF_NAME#v}" - echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { - echo "::error::Invalid version format: $VERSION" - exit 1 - } - sed -n "/^## \[${VERSION}\]/,/^## \[/{ /^## \[${VERSION}\]/d; /^## \[/d; p; }" CHANGELOG.md \ - | tr -d '\r' > /tmp/release-notes.md - echo "::notice::Release notes for v${VERSION}:" - cat /tmp/release-notes.md - - - uses: goreleaser/goreleaser-action@v6 - with: - distribution: goreleaser - version: "~> v2" - args: release --clean --release-notes /tmp/release-notes.md - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cc4fb56..55f2e1a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -10,23 +10,26 @@ on: type: boolean default: true -permissions: - contents: write - env: DRY_RUN: ${{ inputs.dry_run || false }} jobs: release: runs-on: ubuntu-latest + permissions: + contents: write # commit + tag version bump # Skip release commits to prevent infinite loop if: "!startsWith(github.event.head_commit.message, 'chore(release):')" + outputs: + version: ${{ steps.bump.outputs.version }} + skip: ${{ steps.bump.outputs.skip }} + dry_run: ${{ env.DRY_RUN }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25' @@ -121,4 +124,46 @@ jobs: git commit -m "chore(release): v${VERSION}" git tag "v${VERSION}" git push origin master --tags - echo "::notice::Tagged v${VERSION} — GoReleaser workflow will build and publish the release" + echo "::notice::Tagged v${VERSION}" + + goreleaser: + needs: release + if: needs.release.outputs.skip != 'true' && needs.release.outputs.dry_run != 'true' + runs-on: ubuntu-latest + permissions: + contents: write # publish GitHub release assets + steps: + - name: Validate version + env: + VERSION: ${{ needs.release.outputs.version }} + run: | + echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$' || { + echo "::error::Invalid or empty version: '$VERSION'" + exit 1 + } + + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + ref: v${{ needs.release.outputs.version }} + fetch-depth: 1 + + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 + with: + go-version: '1.25' + + - name: Extract release notes from CHANGELOG.md + env: + VERSION: ${{ needs.release.outputs.version }} + run: | + sed -n "/^## \[${VERSION}\]/,/^## \[/{ /^## \[${VERSION}\]/d; /^## \[/d; p; }" CHANGELOG.md \ + | tr -d '\r' > /tmp/release-notes.md + echo "::notice::Release notes for v${VERSION}:" + cat /tmp/release-notes.md + + - uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6 + with: + distribution: goreleaser + version: "~> v2" + args: release --clean --release-notes /tmp/release-notes.md + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e3902e..7dcbb38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- **GoReleaser workflow not triggering** — tags pushed with `GITHUB_TOKEN` don't trigger other workflows; merged goreleaser into `release.yml` as a second job with `needs: release` +- **Dry run executing goreleaser** — boolean vs string comparison bug in job `if:` condition; `DRY_RUN` now forwarded through job outputs as string +- **Actions pinned to commit SHAs** — `actions/checkout`, `actions/setup-go`, `goreleaser/goreleaser-action` pinned to immutable SHAs for supply-chain security +- **Per-job permissions** — `contents: write` moved from workflow-level to per-job blocks for least-privilege + ## [0.10.0] - 2026-03-24 ### Added @@ -14,7 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - **Roadmap PRDs** — 11 detailed Product Requirements Documents in `docs/roadmap/`: workspace files, MCP server, command palette, notification center, pre-built binaries, demo GIF, community plugins, process health, tmux migration, cross-pane events, session sharing - **Restructured ROADMAP.md** — organized into Core/Growth/Advanced categories with priority matrix, strategic pain-layer analysis, and feature synergy notes - **Notification center concept (M12)** — centralized event sidebar with pane navigation and history stack; PRD covers process exit detection, plugin notification handlers, and incremental integration path -- **Pre-built binaries & release infrastructure** — GoReleaser config for 5 platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64); two-workflow CI split: `release.yml` handles version bump + tag, `goreleaser.yml` builds + publishes GitHub Release with `.tar.gz`/`.zip` archives and SHA256 checksums +- **Pre-built binaries & release infrastructure** — GoReleaser config for 5 platforms (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64); `release.yml` handles version bump + tag + GoReleaser build, publishes GitHub Release with `.tar.gz`/`.zip` archives and SHA256 checksums - **One-line install script** — `scripts/install.sh` detects OS/arch, fetches latest release from GitHub API, verifies SHA256 checksum, installs to `~/.local/bin/`; supports `AETHEL_VERSION` for pinned installs and `GITHUB_TOKEN` for API auth - **Daemon version reporting** — `aetheld version` subcommand, version logged at startup; consistent `-ldflags` injection across all build paths (GoReleaser, dev.sh, dev.ps1, rebuild.ps1, Makefile) diff --git a/ROADMAP.md b/ROADMAP.md index c8cc764..d6bc863 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -147,7 +147,7 @@ Processes emit events when they finish or need attention. A non-modal sidebar sh > `curl -sSfL .../install.sh | sh` — zero friction install. -GoReleaser cross-compiles 5 platform pairs (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64) with SHA256 checksums. Two-workflow split: `release.yml` handles version bump + tag, `goreleaser.yml` builds + publishes GitHub Release. Install script for Linux/macOS. **Homebrew tap, Scoop, Winget deferred** (need external repos). +GoReleaser cross-compiles 5 platform pairs (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64, windows/amd64) with SHA256 checksums. Single `release.yml` workflow: version bump + tag job, then GoReleaser build + publish job. Install script for Linux/macOS. **Homebrew tap, Scoop, Winget deferred** (need external repos). ### The "Holy Shit" Demo — [PRD](docs/roadmap/demo-gif.md)