# PLAN: Release-Please Improvements for openlatch-client
**Date**: 2026-04-09
**Scope**: Harden the release automation pipeline based on 2026 best practices research.
---
## Current State
- **release-please v4** with `release-type: "rust"`, single root package at `.`
- **version-bump.yml**: Triggers on push to `main`, creates Release PR, tags on merge
- **publish.yml**: Triggers on `v*.*.*` tag push, builds 5-target binary matrix, publishes to GitHub Releases + npm (OIDC Trusted Publishing)
- **Token**: Uses `OPENLATCH_CLIENT_PR_CHECK_TOKEN` (PAT) — works but has bus factor risk
- **npm structure**: `@openlatch/client` (main) + 5 platform packages (`client-darwin-arm64`, etc.)
- **Pre-major guards**: `bump-minor-pre-major` and `bump-patch-for-minor-pre-major` both enabled (correct for 0.x)
### What's Working Well
- Clean two-stage pipeline (version-bump → tag → publish)
- Custom `changelog-sections` with sensible hidden/visible categories
- Pre-major bump guards prevent accidental 1.0.0
- Custom PR title pattern and labels
- Binary size gate (20MB limit on openlatch-hook)
- Separate cargo builds avoiding feature unification (Pitfall 3)
- npm OIDC Trusted Publishing (no NPM_TOKEN needed)
- Dependabot for cargo + github-actions
- `.github/release.yml` for GitHub Release notes categorization
---
## Improvements
### 1. Migrate from PAT to GitHub App Token
**Priority**: High
**Why**: The current PAT (`OPENLATCH_CLIENT_PR_CHECK_TOKEN`) is tied to a person. If that person leaves, rotates their token, or revokes access, releases break silently. GitHub App tokens are short-lived (1hr), org-owned, and survive personnel changes.
**Changes**:
- [ ] Create a GitHub App for the OpenLatch org with minimal permissions:
- `Contents: Read & Write` (create tags, push changelog commits)
- `Pull Requests: Read & Write` (create/update Release PRs)
- `Metadata: Read` (required baseline)
- [ ] Install the App on the `openlatch-client` repo
- [ ] Store `RELEASE_APP_ID` and `RELEASE_APP_PRIVATE_KEY` as repository secrets
- [ ] Update `version-bump.yml`:
```yaml
# Before
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.OPENLATCH_CLIENT_PR_CHECK_TOKEN }}
# After
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- uses: googleapis/release-please-action@v4
with:
token: ${{ steps.app-token.outputs.token }}
```
- [ ] Remove the `OPENLATCH_CLIENT_PR_CHECK_TOKEN` secret after verifying the App token works
- [ ] Reuse the same GitHub App across `openlatch-platform` (install on both repos)
**Files**: `.github/workflows/version-bump.yml`
---
### 2. Add `releases_created` Output Guard
**Priority**: High
**Why**: In release-please v4, the `releases_created` (plural) output can return `true` even when no release was actually created. This is a known bug that has caused unintended downstream triggers in production at multiple companies.
**Changes**:
- [ ] Add explicit per-component output from version-bump.yml:
```yaml
jobs:
release-please:
outputs:
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.tag_name }}
version: ${{ steps.release.outputs.version }}
```
- [ ] Use `release_created` (singular, for root package) instead of `releases_created` (plural) anywhere outputs are consumed
- [ ] If a downstream job depends on the release, gate it with:
```yaml
if: steps.release.outputs.release_created == 'true'
```
**Files**: `.github/workflows/version-bump.yml`
---
### 3. Add Post-Release Notifications
**Priority**: Medium
**Why**: When a new version is published, downstream consumers (the platform repo, Slack community, docs) should be notified. Currently the pipeline is a dead end after npm publish — no one knows a release happened unless they watch GitHub.
**Changes**:
- [ ] Add a `notify` job to `publish.yml` (after `npm-publish`):
```yaml
notify:
name: Post-Release Notifications
needs: [release, npm-publish]
runs-on: ubuntu-latest
if: success()
steps:
# Trigger platform repo to update its cross-repo E2E with the new client version
- name: Notify openlatch-platform
uses: peter-evans/repository-dispatch@v3
with:
token: ${{ steps.app-token.outputs.token }}
repository: openlatch/openlatch-platform
event-type: client-next-published
client-payload: '{"version": "${{ github.ref_name }}", "sha": "${{ github.sha }}"}'
# Slack notification (optional, if webhook exists)
- name: Post to Slack
if: ${{ secrets.SLACK_WEBHOOK_URL != '' }}
uses: slackapi/slack-github-action@v2
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "openlatch-client ${{ github.ref_name }} published :rocket:"
```
- [ ] This connects to the `cross-repo-e2e.yml` in openlatch-platform which already listens for `client-next-published`
**Files**: `.github/workflows/publish.yml`
---
### 4. Add crates.io Publishing
**Priority**: Medium
**Why**: The client is a Rust crate (`openlatch-client` in Cargo.toml) but the publish workflow only builds binaries and publishes to npm. Rust developers expect to find it on crates.io via `cargo install openlatch-client`.
**Changes**:
- [ ] Add a `crates-publish` job to `publish.yml` using Trusted Publishing (OIDC):
```yaml
crates-publish:
name: Publish to crates.io
needs: release # Can run in parallel with npm-publish
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for crates.io OIDC Trusted Publishing
steps:
- uses: actions/checkout@v5
- uses: ./.github/actions/setup-rust
- name: Publish to crates.io (Trusted Publishing)
run: |
OIDC_TOKEN=$(curl -sS \
-H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=crates.io" | jq -r '.value')
CRATES_TOKEN=$(curl -sS -X POST https://crates.io/api/v1/trusted_publishing/tokens \
-H "Content-Type: application/json" \
-d "{\"oidc_token\": \"$OIDC_TOKEN\"}" | jq -r '.token')
cargo publish --token "$CRATES_TOKEN"
```
- [ ] Configure Trusted Publishing on crates.io: Crate Settings → Trusted Publishers → Add GitHub Actions (owner: OpenLatch, repo: openlatch-client, workflow: publish.yml)
- [ ] Ensure `Cargo.toml` metadata is complete (`license`, `description`, `repository`, `homepage`, `readme`, `keywords`, `categories` — already present)
- [ ] Add `exclude` to Cargo.toml to keep the published crate lean:
```toml
[package]
exclude = [
"tools/",
"tests/",
".github/",
"npm/",
"docker-compose*.yml",
]
```
**Files**: `.github/workflows/publish.yml`, `Cargo.toml`
---
### 5. Add Supply Chain Security (SBOM + Signing)
**Priority**: Medium
**Why**: 2026 best practice for published binaries. Users downloading openlatch binaries should be able to verify authenticity. SBOM is increasingly required for enterprise adoption.
**Changes**:
- [ ] Add Cosign signing to the release job in `publish.yml`:
```yaml
- name: Install Cosign
uses: sigstore/cosign-installer@v3
- name: Sign release artifacts
run: |
for file in dist/*; do
cosign sign-blob --yes "$file" --output-signature "${file}.sig" --output-certificate "${file}.pem"
done
env:
COSIGN_EXPERIMENTAL: 1
```
- [ ] Add SBOM generation:
```yaml
- name: Generate SBOM
uses: anchore/sbom-action@v0
with:
path: .
output-file: dist/sbom-${{ matrix.target }}.spdx.json
```
- [ ] Upload `.sig`, `.pem`, and SBOM files alongside binaries in the GitHub Release
- [ ] Add verification instructions to README/docs
**Files**: `.github/workflows/publish.yml`
---
### 6. Add Release Smoke Test
**Priority**: Medium
**Why**: After npm publish, there's no verification that the published packages actually work. A post-publish smoke test catches packaging errors before users hit them.
**Changes**:
- [ ] Add a `verify-publish` job to `publish.yml` (after `npm-publish`):
```yaml
verify-publish:
name: Verify Published Package
needs: npm-publish
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
steps:
- name: Wait for npm propagation
run: sleep 30
- name: Install from npm
run: npm install -g @openlatch/client@${{ needs.release.outputs.version }}
- name: Verify binary runs
run: openlatch --version
- name: Verify hook binary exists
run: openlatch-hook --version
```
**Files**: `.github/workflows/publish.yml`
---
### 7. Harden Cargo.toml Version Sync
**Priority**: Low
**Why**: release-please updates `Cargo.toml` version, but `Cargo.lock` must also be updated. The `release-type: "rust"` handler does this, but it's worth adding an `extra-files` entry for any other files that embed the version.
**Changes**:
- [ ] Audit all files that contain version strings and add them to `release-please-config.json`:
```json
{
"packages": {
".": {
"extra-files": [
{
"type": "json",
"path": "npm/client/package.json",
"jsonpath": "$.version"
}
]
}
}
}
```
- [ ] Note: The npm package versions are stamped at publish time in `publish.yml` (the `Prepare npm packages` step), so this may be redundant. Evaluate whether release-please should own the npm version stamp or if the publish workflow should continue to do it dynamically.
**Decision**: Keep the publish-time stamping approach. It's more robust — release-please would need to stamp 6 package.json files (1 main + 5 platform), and the publish workflow already handles this with the `jq` version injection. No change needed here.
**Files**: `release-please-config.json` (no change after evaluation)
---
### 8. Add Workflow Concurrency Controls
**Priority**: Low
**Why**: If multiple pushes to `main` happen in quick succession, multiple version-bump and publish workflows can race. Concurrency controls prevent this.
**Changes**:
- [ ] Add concurrency groups to both workflows:
```yaml
# version-bump.yml
concurrency:
group: release-please
cancel-in-progress: false # Never cancel a release in progress
# publish.yml
concurrency:
group: publish-${{ github.ref }}
cancel-in-progress: false
```
**Files**: `.github/workflows/version-bump.yml`, `.github/workflows/publish.yml`
---
### 9. Add Permissions Hardening
**Priority**: Low
**Why**: Principle of least privilege. Both workflows should declare minimum required permissions at the workflow level, not rely on repo defaults.
**Changes**:
- [ ] `version-bump.yml` already has `permissions` at job level — move to workflow level and tighten:
```yaml
# Top of version-bump.yml (already has this at job level, move to workflow level)
permissions:
contents: write
pull-requests: write
```
- [ ] `publish.yml` has `permissions` scattered across jobs — add a restrictive workflow-level default:
```yaml
# Top of publish.yml
permissions: {} # Restrictive default, each job declares what it needs
jobs:
release:
permissions:
contents: write
npm-publish:
permissions:
contents: read
id-token: write
```
**Files**: `.github/workflows/version-bump.yml`, `.github/workflows/publish.yml`
---
## Implementation Order
| **Phase 1: Security** | #1 (GitHub App), #2 (output guard), #9 (permissions) | ~1 hour |
| **Phase 2: Reliability** | #8 (concurrency), #6 (smoke test) | ~30 min |
| **Phase 3: Distribution** | #4 (crates.io), #3 (notifications) | ~1 hour |
| **Phase 4: Supply Chain** | #5 (SBOM + signing) | ~1 hour |
Phase 1 is the most urgent — the PAT bus factor and the v4 output bug are real risks.
---
## Out of Scope
- **release-please-config.json restructuring** — The current config is well-structured for a single-package Rust repo. No changes needed.
- **PR checks pipeline changes** — The PR checks workflow is solid (change detection, quality/test/build-e2e/docker-e2e). Not part of this plan.
- **Changelog sections** — Already well-configured with custom sections and hidden types.
- **Pre-major guards** — Already correctly set (`bump-minor-pre-major: true`, `bump-patch-for-minor-pre-major: true`).