openlatch-client 0.1.8

The open-source security layer for AI agents — client forwarder
# 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 | Items | Effort |
|-------|-------|--------|
| **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`).