Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .agents/plans/2026-02-25-cargo-lock-release-workflow-design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
## Goal

Ensure each new release cycle refreshes `Cargo.lock` with the latest package versions used by the crosspack workspace crates, integrated directly into the GitHub release workflow.

## Chosen Approach

Implement lockfile refresh inside `.github/workflows/release-please.yml` (Release Please workflow), before running the release-please action.

This keeps the process tied to release automation and avoids separate operational workflows.

## Alternatives Considered

1. Dedicated scheduled/manual lockfile-refresh PR workflow.
- Pros: clear separation from release logic.
- Cons: not guaranteed to run per release; potential drift.
2. Post-process release PR branch to inject lockfile updates.
- Pros: lockfile lives directly inside release PR branch.
- Cons: branch/token orchestration complexity.

## Workflow Architecture

- File: `.github/workflows/release-please.yml`
- Add steps to:
- set up Rust,
- refresh lockfile,
- detect whether `Cargo.lock` changed,
- conditionally commit only `Cargo.lock` with bot credentials,
- continue to `googleapis/release-please-action@v4`.

## Data Flow

1. Workflow triggers on push to `main`.
2. Lockfile update command runs (targeted to crosspack crates, or workspace-wide if explicitly preferred).
3. If `Cargo.lock` is unchanged: skip commit path.
4. If changed: create a bot commit containing only `Cargo.lock`.
5. Run release-please so release artifacts and PR metadata reflect current lockfile state.

## Error Handling and Safety

- Fail fast if lockfile update command errors.
- Stage/commit only `Cargo.lock` to avoid accidental file inclusion.
- Reuse existing GitHub App token permissions model for write operations.
- Add loop-avoidance guard logic so bot-generated lockfile commits do not create infinite workflow runs.

## Verification Strategy

- After refresh, run `cargo check --workspace --locked`.
- Validate three scenarios:
1. no-op (no lockfile delta),
2. lockfile changed and committed,
3. no trigger loop from bot commit.

## Expected Outcome

Release cycles consistently include an up-to-date `Cargo.lock`, reducing stale dependency metadata during release preparation while keeping release automation centralized.
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# Cargo Lock Refresh in Release Workflow Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Refresh `Cargo.lock` during each `main`-branch release cycle so release automation uses current dependency resolution.

**Architecture:** Extend `.github/workflows/release-please.yml` with a pre-release lockfile refresh stage. The job updates lockfile state, validates it with locked resolution, conditionally commits only `Cargo.lock`, then runs `release-please` with existing app-token permissions. Loop avoidance is handled by commit-message and actor guards.

**Tech Stack:** GitHub Actions YAML, Rust/Cargo (`cargo update`, `cargo check --locked`), git CLI in runner.

---

### Task 1: Add lockfile refresh and safety guards in release workflow

**Files:**
- Modify: `.github/workflows/release-please.yml`

**Step 1: Write the failing test (policy check for missing refresh step)**

Create a temporary local policy assertion by checking that the workflow currently does not contain a `Refresh Cargo.lock` step.

Run: `rg "Refresh Cargo.lock" .github/workflows/release-please.yml`
Expected: no matches (acts as failing precondition).

**Step 2: Run precondition command to verify current gap**

Run: `rg "cargo check --workspace --locked" .github/workflows/release-please.yml`
Expected: no matches.

**Step 3: Write minimal implementation in workflow**

Add these logical workflow updates:
- Checkout step before any git/cargo operations.
- Rust toolchain setup step (`dtolnay/rust-toolchain@stable`).
- Lockfile refresh step that updates dependencies for release workflow.
- Validation step: `cargo check --workspace --locked`.
- Change-detection step setting output (e.g., `lock_changed=true/false`) by checking `git diff --quiet -- Cargo.lock`.
- Conditional commit step (only when lock changed) that:
- configures bot identity,
- stages only `Cargo.lock`,
- commits with a deterministic message such as `chore(lockfile): refresh Cargo.lock for release`,
- pushes to `main`.
- Guard `Run release-please` step so lockfile-only self-commit does not recurse (for example, skip when head commit message matches lockfile refresh commit marker).

**Step 4: Run local verification for workflow content**

Run: `rg "Refresh Cargo.lock|cargo check --workspace --locked|chore\(lockfile\): refresh Cargo.lock for release" .github/workflows/release-please.yml`
Expected: all patterns matched.

**Step 5: Commit**

```bash
git add .github/workflows/release-please.yml
git commit -m "ci: refresh Cargo.lock during release workflow"
```

### Task 2: Validate behavior for no-op and update paths

**Files:**
- Modify: `.github/workflows/release-please.yml` (if fixes needed)

**Step 1: Write the failing test (simulate expected guards not present)**

Check that skip/guard condition is explicitly encoded in workflow for self-commit loop prevention.

Run: `rg "if: .*lockfile.*|if: .*steps\..*lock_changed" .github/workflows/release-please.yml`
Expected: at least one match after implementation; if none, treat as failing test.

**Step 2: Run lint/parse check for YAML validity**

Run: `python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release-please.yml')); print('ok')"`
Expected: `ok`.

**Step 3: Write minimal implementation fixes (if needed)**

If guard or syntax issues appear:
- fix conditional expressions,
- ensure step IDs referenced in conditions exist,
- ensure shell snippets use `set -euo pipefail` where appropriate.

**Step 4: Re-run validation commands**

Run: `python -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release-please.yml')); print('ok')" && rg "cargo check --workspace --locked|lock_changed|refresh Cargo.lock" .github/workflows/release-please.yml`
Expected: YAML parses and all required markers are present.

**Step 5: Commit**

```bash
git add .github/workflows/release-please.yml
git commit -m "ci: guard release workflow lockfile refresh"
```
39 changes: 39 additions & 0 deletions .github/workflows/release-please.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,44 @@ jobs:
name: release-please
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}

- name: Set up Rust toolchain
uses: dtolnay/rust-toolchain@stable

- name: Refresh Cargo.lock
run: |
set -euo pipefail
cargo update --workspace

- name: Validate Cargo.lock
run: |
set -euo pipefail
cargo check --workspace --locked

- name: Detect lockfile changes
id: lockfile
run: |
set -euo pipefail
if git diff --quiet -- Cargo.lock; then
echo "lock_changed=false" >> "$GITHUB_OUTPUT"
else
echo "lock_changed=true" >> "$GITHUB_OUTPUT"
fi

- name: Commit refreshed Cargo.lock
if: ${{ steps.lockfile.outputs.lock_changed == 'true' }}
run: |
set -euo pipefail
git config user.name "crosspack-bot"
git config user.email "crosspack-bot@users.noreply.github.com"
git add Cargo.lock
git commit -m "chore(lockfile): refresh Cargo.lock for release"
git push origin main

- name: Create GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
Expand All @@ -32,6 +70,7 @@ jobs:
permission-issues: write

- name: Run release-please
if: "${{ !contains(github.event.head_commit.message, 'chore(lockfile): refresh Cargo.lock for release') }}"
id: release
uses: googleapis/release-please-action@v4
with:
Expand Down