Skip to content

ci: release apple-mail as a packaged CLI - #8

Merged
furedea merged 4 commits into
mainfrom
ci/release-packaged-cli
Oct 20, 2026
Merged

furedea merged 4 commits into
mainfrom
ci/release-packaged-cli

Conversation

@furedea-agent

@furedea-agent furedea-agent Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

概要

  • release-please がタグと draft の Release を作り,そのタグ上で Release Artifacts ワークフローを起動する.macOS ランナーで aarch64-apple-darwin の cargo-dist アーカイブをビルド,検証,attestation,アップロードし終えてから Release を公開する.
  • 成果物ワークフローは自動でも手動でも常にタグ上で動き,draft の Release にしか触れない.公開済みの Release は作り直さず,修正は新しい版で出す.
  • マージ前にリポジトリシークレット RELEASE_PLEASE_TOKEN の登録が必要.未登録のままマージすると release-please ジョブが失敗する.
flowchart LR
  A[main へ push] --> B[release-please が Release PR を作成]
  B -->|マージ| C[タグ apple-mail-cli-vX.Y.Z と draft Release]
  C --> W[gh workflow run release_artifacts.yml --ref タグ]
  M[失敗した draft の再開も同じコマンド] --> W
  W --> V[ref,HEAD,draft の検証]
  V --> D[macos-15 で cargo test --locked と pnpm run check]
  D --> E[dist build とアーカイブのスモークテスト]
  E --> G[attest-build-provenance]
  G --> H[gh release upload --clobber]
  H --> P[gh release edit --draft=false で公開]
Loading
項目 変更前 変更後
バージョン管理 Cargo.toml 0.1.0,flake は直書き release-please が Cargo.toml と CHANGELOG を更新,flake は Cargo.toml から読む
Release の公開 なし draft で作成し,全ステップ成功後に公開.失敗時は draft のまま
成果物のビルド なし release_artifacts.yml をタグ上で起動(自動・手動で同じ経路)
配布物 なし tar.xz,.sha256,sha256.sum,source.tar.gz,シェルインストーラー
来歴証明 なし actions/attest-build-provenance.常にタグのコミットを記録
インストール方法 cargo install --path .,nix run curl インストーラー,cargo install --locked --git,nix profile install
CI のピン checkout v6.0.2,rust-toolchain v1.16.0,stable,cargo test checkout v6.1.0,rust-toolchain v1.17.0,1.98.1,cargo test --locked

受け入れ条件

  • マージ前に RELEASE_PLEASE_TOKEN シークレットを登録する(下記「シークレット登録」参照)
  • release-please-config.json と .release-please-manifest.json(0.1.0)を追加
  • main への push で Release PR が作られる(シークレット登録とマージ後に確認)
  • リリース時に macOS ランナーで cargo test --locked とビルドが通り,アーカイブで apple-mail --version が成功する(ローカルの dist build では確認済み)
  • Release は成果物のアップロード後に公開され,アーカイブ,.sha256,インストーラーが添付され,gh attestation verify が通る
  • 失敗した draft は gh workflow run release_artifacts.yml --ref <tag> -f tag_name=<tag> で再開できる.タグ以外の ref や公開済みの Release では失敗する
  • flake の version が Cargo.toml から読まれる
  • README のインストール節と CONTRIBUTING.md を更新・追加する
詳細

シークレット登録

GITHUB_TOKEN で作った Release PR では CI が起動しないため,agent-harness と同じく PAT を使う.

  • 名前:RELEASE_PLEASE_TOKEN(リポジトリシークレット)
  • 種類:fine-grained personal access token,対象は furedea/apple-mail-cli のみ
  • 権限:Contents,Pull requests,Issues をそれぞれ Read and write(Metadata: Read は自動付与)
  • 登録:gh secret set RELEASE_PLEASE_TOKEN --repo furedea/apple-mail-cli

成果物ワークフローの起動には GITHUB_TOKEN(actions: write)を使う.workflow_dispatch は GITHUB_TOKEN からでも新しい実行を作れるので,PAT の権限を増やす必要はない.

ワークフローの構成

  • release_please.yml(main への push):release-please で Release PR,タグ,draft Release を作る.release_created のときだけ gh workflow run release_artifacts.yml --ref "$TAG_NAME" -f tag_name="$TAG_NAME" を実行する
  • release_artifacts.yml(workflow_dispatch のみ):テスト,ビルド,スモークテスト,attestation,アップロード,公開.concurrency はタグごと
  • ジョブを分けた理由:一つのワークフロー内だと,Release PR のマージコミット R の待機中の実行が後続の push S に置き換わったとき,S の実行は HEAD=R,GITHUB_SHA=S となってガードで失敗する.release-please は PR を tagged 済みにしているので,以後 release_created は立たず draft が取り残される.タグ上で起動すれば GITHUB_SHA は常にタグのコミットになる

draft から公開までの流れ

  • release-please-config.json に "draft": true と "force-tag-creation": true を設定.draft でもタグがすぐ作られるため,成果物ワークフローをタグ上で起動でき,次回の release-please も前回リリースを見つけられる(force-tag-creation は release-please v17.2.0 で追加,固定中の action v4.4.1 は ^17.3.0 を同梱)
  • release-please はマージ済み Release PR のラベル(autorelease: pending → tagged)でリリース作成を判断するため,draft でも重複作成しない
  • ジョブの最後に gh release edit "$TAG_NAME" --draft=false を実行.途中で失敗すれば draft のまま残り,README の releases/latest の URL が欠けた成果物を指すことはない

Verify release target ステップ(checkout 直後,ビルド前)

  • GITHUB_REF が refs/tags/$TAG_NAME でなければ失敗し,正しいコマンドを表示する
  • checkout した HEAD が GITHUB_SHA と一致しなければ失敗する
  • gh release view "$TAG_NAME" --json isDraft が true でなければ失敗する.--clobber は各アセットを消してから上げ直すため,公開済みの Release を作り直すとネットワーク障害で欠けた Release が残りうる

変更ファイル

  • .github/workflows/release_please.yml,.github/workflows/release_artifacts.yml(ともに新規),release-please-config.json,.release-please-manifest.json
  • Cargo.toml:[profile.dist],[workspace.metadata.dist](target aarch64-apple-darwin,shell installer,cargo-dist 0.30.2)
  • flake.nix:version を Cargo.toml から読む
  • .github/workflows/{ci,codeql,dependency_review,gha_lint}.yml:ピンそろえ,cargo test --locked
  • .ls-lint.yml:慣習名の CONTRIBUTING.md,CHANGELOG.md,release-please-config.json を除外
  • README.md,CONTRIBUTING.md

agent-harness との差分

  • ランナーは macos-15,musl/apt 手順なし,リリースジョブで JXA チェック(pnpm run check)も実行
  • draft から公開する流れ,タグ上で動く成果物ワークフロー,draft のみを対象とするガードは agent-harness にはない(agent-harness にも同じ問題がある)
  • macOS ランナーの /bin/bash は 3.2 で mapfile がないため,アップロード一覧は while read で配列化
  • リリースジョブでは setup-node のキャッシュを使わない

ローカル検証

  • cargo test --locked,cargo fmt --check,cargo clippy --locked --all-targets -- -D warnings:成功
  • pnpm run check,nixfmt,statix,deadnix,ls-lint,commitlint:成功
  • nix flake check:成功,nix build .# で apple-mail-cli-0.1.0 をビルド
  • actionlint,zizmor --offline:指摘なし
  • Verify release target の本体を /bin/bash 3.2 と偽の gh で 6 ケース検証(ref が main,別タグ,SHA 不一致,公開済み,draft 状態が空は失敗,タグ上で SHA 一致かつ draft は成功)
  • nixpkgs の cargo-dist 0.31.0(一時 worktree で cargo-dist-version のみ合わせる)で dist plan と,ワークフローと同じ dist build 手順を実行し,アーカイブ展開後の apple-mail --version/--help と shasum -a 256 -c が成功.アップロード対象は 6 ファイル

CI や実リリースでしか確認できないこと

  • cargo-dist 0.30.2 のインストーラーと macos-15 ランナーでの実行
  • release-please の Release PR 作成,draft Release とタグの作成,タグ名(apple-mail-cli-vX.Y.Z を想定)
  • GITHUB_TOKEN による gh workflow run --ref <tag> の起動,draft Release への gh release view/upload/edit --draft=false,公開後の latest の切り替わり,attestation
  • 最初のリリース版:既存タグがないため,feat コミットと bump-patch-for-minor-pre-major により 0.1.1 が提案される見込み

除外

  • Developer ID 署名と公証,Homebrew tap,crates.io 公開,SECURITY.md,Issue テンプレート,CI での Mail E2E テスト

Closes #7

🤖 Generated with Claude Code

furedea and others added 4 commits October 19, 2026 21:38
Pin actions/checkout to v6.1.0, setup-rust-toolchain to v1.17.0, and
Rust to 1.98.1, and run tests with --locked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Add Release Please config and a workflow that builds the cargo-dist
archive on a macOS runner, smoke-tests it, attests its provenance, and
uploads the archive, checksums, and shell installer to the release.
The flake now reads its version from Cargo.toml.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release Please now creates draft releases with forced tag creation, and
the artifact job publishes the release only after tests, build, smoke
test, attestation, and uploads succeed, so the latest installer URL never
points at missing files.

Manual rebuilds must run on the tag itself, and the job fails unless the
checked-out commit equals the run's commit, so attestations record the
tagged commit instead of main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@furedea
furedea merged commit 3c08dac into main Oct 20, 2026
16 checks passed
@furedea
furedea deleted the ci/release-packaged-cli branch October 20, 2026 02:22
furedea added a commit that referenced this pull request Oct 20, 2026
## 概要

- **Release PR #10(0.1.1)はこの PR より先にマージしないこと**.main
の現行ワークフローのままリリースすると,以下の問題を抱えたまま 0.1.1 が出る.
- 成果物のビルドを `release_artifacts.yml` に分け,release-please
がタグ上で起動する.自動でも手動でも同じ経路を通るため,draft が取り残されなくなる.成果物ワークフローは draft の Release
にしか触れない.
- 公開は REST API で `draft` を boolean の `false`,`make_latest` を `legacy`
として送り,古いタグの実行が後から終わっても新しい版の latest を奪わない.

```mermaid
flowchart LR
  A[main へ push] --> B[release-please]
  B -->|Release PR のマージ後| C[タグと draft Release]
  C --> W[gh workflow run release_artifacts.yml --ref タグ]
  M[失敗した draft の再開] --> W
  W --> V[ref,HEAD,draft の検証]
  V --> D[テスト,ビルド,スモークテスト,attestation,アップロード]
  D --> P[gh api PATCH -F draft=false -f make_latest=legacy で公開]
```

| 項目 | 変更前(#8 時点の main) | 変更後 |
|---|---|---|
| 成果物のビルド | `release_please.yml` 内の後続ジョブ | `release_artifacts.yml` をタグ上で
`workflow_dispatch` 起動 |
| 自動と手動の経路 | 自動は push の実行内,手動は `release_please.yml` の
`workflow_dispatch` | どちらも `gh workflow run release_artifacts.yml --ref
<tag> -f tag_name=<tag>` |
| 後続 push との競合 | マージコミットの実行が後続 push に置き換わると,ガードで失敗して draft が残る |
タグ上で動くので `GITHUB_SHA` は常にタグのコミット |
| 公開済み Release の再ビルド | `--clobber` で上書きできてしまう | ビルド前に `isDraft`
を確認し,draft でなければ失敗 |
| 公開と latest | `gh release edit --draft=false`(make_latest は true 扱い) |
`gh api -X PATCH .../releases/<id> -F draft=false -f
make_latest=legacy`(`draft` は boolean) |

## 受け入れ条件

- [ ] この PR をマージしてから Release PR #10 をマージする
- [ ] #10 のマージ後,`release_please.yml` が `release_artifacts.yml`
をタグ上で起動し,成果物のアップロード後に Release が公開される
- [ ] 公開後の latest が最新の版を指し,README の `releases/latest/download`
のインストーラーが最新版を配る
- [x] `release_artifacts.yml` はタグ以外の ref,HEAD と `GITHUB_SHA` の不一致,公開済みの
Release でビルド前に失敗する
- [x] 公開ステップは `draft` を boolean(`-F draft=false`),`make_latest` を
`legacy` として送り,`gh release edit` を使わない
- [x] CONTRIBUTING.md に draft の再開手順を記載し,公開済みの Release は作り直さないことを明記する

<details>
<summary>詳細</summary>

**経緯**

#8 のレビュー(Codex)で見つかった 2 件と,この PR のレビューで見つかった 2 件の修正.#8 は修正前の `eee0747`
の時点で squash マージされたため,修正コミットを main の上に cherry-pick して出し直した.

**1. [high] 公開済み Release の作り直し**
- `gh release upload --clobber` は各アセットを消してから上げ直す.公開済みの Release
を手動で作り直すと,途中のネットワーク障害でファイルが欠けたまま公開状態になり,README の `releases/latest`
のインストーラー URL が壊れる
- `Verify release target` ステップで `gh release view "$TAG_NAME" --json
isDraft --jq .isDraft` が `true` でなければ失敗する

**2. [medium] draft の取り残し**
- 一つのワークフロー内では,Release PR のマージコミット R の待機中の実行が後続の push S に置き換わると,S の実行は
HEAD=R,`GITHUB_SHA`=S となってガードで失敗する.release-please は PR を tagged
済みにしているため以後 `release_created` は立たず,draft が残る
- `release_please.yml` は draft とタグを作るだけにし,`release_created` のときに `gh
workflow run release_artifacts.yml --ref "$TAG_NAME" -f
tag_name="$TAG_NAME"` を実行する.ジョブには既存の権限に加えて `actions: write`
を付け,`GITHUB_TOKEN` で起動する(`workflow_dispatch` は `GITHUB_TOKEN`
からでも新しい実行を作れる)
- `release_artifacts.yml` は `workflow_dispatch` のみで,concurrency
はタグごと.ref と SHA のガードは維持する

**3. [medium] 古い版が latest を奪う問題(`ef18660`)**
- タグごとの concurrency では,v0.1.1 の実行が v0.1.2 の公開後に終わりうる.`make_latest`
を指定せずに公開すると true 扱いになり,古い版が latest になって README のインストーラーが古い版を配る
- `Publish release` ステップで `gh release view "$TAG_NAME" --json
databaseId` から ID を取り,REST API の PATCH で `make_latest=legacy`
を指定して公開する.`legacy` では GitHub が作成日時と semver から latest を選ぶ

**4. [high] `draft` が文字列で送られる問題(`814caa9`)**
- `gh api -f draft=false` は JSON の文字列 `"false"` を送るが,Release API は
boolean を要求する
- `draft` は typed の `-F draft=false` で送る(`gh api` の `-F` は
`true`,`false`,`null`,整数を JSON の型に変換する).`make_latest` は文字列なので `-f
make_latest=legacy` のまま

**変更ファイル**
- `.github/workflows/release_please.yml`:成果物ジョブを外し,起動ステップを追加
- `.github/workflows/release_artifacts.yml`:新規(ガード,`-F draft=false -f
make_latest=legacy` の公開ステップを含む)
- `CONTRIBUTING.md`:Release 節

**ローカル検証**
- `Verify release target` の本体をコミット済みのワークフローから取り出し,`/bin/bash` 3.2 と偽の
`gh` で 6 ケース検証(ref が main,別タグ,SHA 不一致,公開済み,draft 状態が空は失敗,タグ上で SHA 一致かつ
draft は成功)
- `Publish release` の本体を同様に取り出し,呼び出しを記録する偽の `gh` で 4 ケース検証(終了コード 0,`api
-X PATCH repos/furedea/apple-mail-cli/releases/4242 -F draft=false -f
make_latest=legacy` が 1 回,`release edit` は 0 回,`-f draft=` は 0 回).`-f
draft=false` の時点では 2 件失敗,修正後は 4 件成功
- `actionlint`,`zizmor --offline`:指摘なし

**CI や実リリースでしか確認できないこと**
- `GITHUB_TOKEN` による `gh workflow run --ref <tag>` の起動
- draft Release への `gh release view`/`upload` と `gh api PATCH`
による公開,`make_latest=legacy` での latest の選ばれ方

**その他**
- `RELEASE_PLEASE_TOKEN` は登録済み(#10 が作られている).この PR で必要な権限の追加はない
- 古いリモートブランチ `ci/release-packaged-cli` は不要で,削除してよい

</details>

Refs #7, #8, #10

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: furedea <132188853+furedea@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ci: release apple-mail as a packaged CLI like agent-harness

1 participant