diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index ee71e2b..2df13fd 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -113,18 +113,23 @@ jobs: # === LINT AND FORMAT CHECK === # Lint runs independently of changelog check - it's a fast check that should always run + # Note: always() is required because detect-changes is skipped on workflow_dispatch, + # and without always(), this job would also be skipped even though its condition includes workflow_dispatch. + # See: https://github.com/actions/runner/issues/491 lint: name: Lint and Format Check runs-on: ubuntu-latest needs: [detect-changes] if: | - github.event_name == 'push' || - github.event_name == 'workflow_dispatch' || - needs.detect-changes.outputs.rs-changed == 'true' || - needs.detect-changes.outputs.toml-changed == 'true' || - needs.detect-changes.outputs.mjs-changed == 'true' || - needs.detect-changes.outputs.docs-changed == 'true' || - needs.detect-changes.outputs.workflow-changed == 'true' + always() && !cancelled() && ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + needs.detect-changes.outputs.toml-changed == 'true' || + needs.detect-changes.outputs.mjs-changed == 'true' || + needs.detect-changes.outputs.docs-changed == 'true' || + needs.detect-changes.outputs.workflow-changed == 'true' + ) steps: - uses: actions/checkout@v4 @@ -166,7 +171,8 @@ jobs: runs-on: ${{ matrix.os }} needs: [detect-changes, changelog] # Run if: push event, OR changelog succeeded, OR changelog was skipped (docs-only PR) - if: always() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') + # Note: always() is required to evaluate the condition when dependencies are skipped. + if: always() && !cancelled() && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || needs.changelog.result == 'success' || needs.changelog.result == 'skipped') strategy: fail-fast: false matrix: @@ -200,7 +206,8 @@ jobs: name: Build Package runs-on: ubuntu-latest needs: [lint, test] - if: always() && needs.lint.result == 'success' && needs.test.result == 'success' + # Note: always() ensures this job runs even when lint/test jobs use always(). + if: always() && !cancelled() && needs.lint.result == 'success' && needs.test.result == 'success' steps: - uses: actions/checkout@v4 @@ -230,7 +237,13 @@ jobs: auto-release: name: Auto Release needs: [lint, test, build] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + # Note: always() is required to evaluate the condition when dependencies use always(). + # The build job ensures lint and test passed before this job runs. + if: | + always() && !cancelled() && + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.build.result == 'success' runs-on: ubuntu-latest permissions: contents: write @@ -310,7 +323,12 @@ jobs: manual-release: name: Manual Release needs: [lint, test, build] - if: github.event_name == 'workflow_dispatch' + # Note: always() is required to evaluate the condition when dependencies use always(). + # The build job ensures lint and test passed before this job runs. + if: | + always() && !cancelled() && + github.event_name == 'workflow_dispatch' && + needs.build.result == 'success' runs-on: ubuntu-latest permissions: contents: write diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4bb20d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,5 @@ +Issue to solve: https://github.com/link-foundation/browser-commander/issues/27 +Your prepared branch: issue-27-4fd8f05681d7 +Your prepared working directory: /tmp/gh-issue-solver-1768607156411 + +Proceed. diff --git a/docs/case-studies/issue-27/README.md b/docs/case-studies/issue-27/README.md new file mode 100644 index 0000000..29c1e3a --- /dev/null +++ b/docs/case-studies/issue-27/README.md @@ -0,0 +1,116 @@ +# Case Study: Rust Release Jobs Skipped (Issue #27) + +## Timeline of Events + +### 2026-01-16 17:09:55 UTC - Run ID 21074589083 +- **Event**: `workflow_dispatch` (manual trigger) +- **Result**: Pipeline completed but release jobs were skipped +- **Jobs Status**: + - Detect Changes: SKIPPED (expected - has `if: github.event_name != 'workflow_dispatch'`) + - Test (all platforms): SUCCESS + - Changelog Fragment Check: SKIPPED + - **Lint and Format Check: SKIPPED** (unexpected) + - **Build Package: SKIPPED** (unexpected - depends on lint) + - **Manual Release: SKIPPED** (unexpected - depends on build) + - Auto Release: SKIPPED (expected - only on push to main) + +### 2026-01-10 13:38:44 UTC - Run ID 20879147120 +- **Event**: `push` to main branch +- **Result**: Build succeeded but Auto Release was skipped +- **Jobs Status**: + - Detect Changes: SUCCESS + - Lint and Format Check: SUCCESS + - Test (all platforms): SUCCESS + - Build Package: SUCCESS + - **Auto Release: SKIPPED** (unexpected) + +## Root Cause Analysis + +### Primary Root Cause: Missing `always()` in Job Conditions + +The GitHub Actions workflow has a fundamental issue with job dependency evaluation. When a job is skipped, all jobs that depend on it are also skipped by default unless they use `always()` in their condition. + +**The Problematic Pattern (current):** +```yaml +lint: + needs: [detect-changes] + if: | + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + ... +``` + +**The Correct Pattern (from template):** +```yaml +lint: + needs: [detect-changes] + if: | + always() && !cancelled() && ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.rs-changed == 'true' || + ... + ) +``` + +### Chain Reaction + +1. On `workflow_dispatch`, `detect-changes` is skipped (by design) +2. Without `always()`, `lint` job is automatically skipped when its dependency is skipped +3. `build` depends on `lint`, so it's also skipped +4. `manual-release` depends on `build`, so it's also skipped + +### Secondary Root Cause: Inconsistent Condition for Auto Release + +The `auto-release` job has the condition: +```yaml +if: github.event_name == 'push' && github.ref == 'refs/heads/main' +``` + +But it lacks `always() && !cancelled()` prefix and `needs.build.result == 'success'` verification, which can cause issues when upstream jobs use `always()`. + +## Solution + +### 1. Add `always() && !cancelled()` to All Dependent Jobs + +Jobs that depend on `detect-changes` need the pattern: +```yaml +if: | + always() && !cancelled() && ( + github.event_name == 'push' || + github.event_name == 'workflow_dispatch' || + ... + ) +``` + +This ensures: +- `always()` - Job runs even when dependencies are skipped +- `!cancelled()` - Job doesn't run if workflow was cancelled +- The actual condition determines if job should run + +### 2. Add Result Verification for Release Jobs + +Release jobs should verify upstream jobs succeeded: +```yaml +auto-release: + if: | + always() && !cancelled() && + github.event_name == 'push' && + github.ref == 'refs/heads/main' && + needs.build.result == 'success' +``` + +## Reference + +- [GitHub Actions: Jobs that use `always()` need dependencies to also use it](https://github.com/actions/runner/issues/491) +- [Template Repository Best Practices](https://github.com/link-foundation/rust-ai-driven-development-pipeline-template) + +## Affected Files + +1. `.github/workflows/rust.yml` - Main workflow file needing fixes +2. `rust/scripts/get-bump-type.mjs` - Currently works with `rust/changelog.d`, needs update for monorepo structure + +## Fix Implementation + +See the PR for the complete fix that aligns with the template repository best practices. diff --git a/docs/case-studies/issue-27/job-summary.md b/docs/case-studies/issue-27/job-summary.md new file mode 100644 index 0000000..c5430e8 --- /dev/null +++ b/docs/case-studies/issue-27/job-summary.md @@ -0,0 +1,47 @@ +# CI Job Summary for Issue #27 + +## Run 21074589083 (workflow_dispatch, 2026-01-16) + +| Job | Conclusion | +|-----|------------| +| Detect Changes | skipped | +| Test (macos-latest) | success | +| Test (windows-latest) | success | +| Test (ubuntu-latest) | success | +| Changelog Fragment Check | skipped | +| Lint and Format Check | **skipped** | +| Build Package | **skipped** | +| Manual Release | **skipped** | +| Auto Release | skipped | + +**Problem**: Lint, Build, and Manual Release were unexpectedly skipped due to missing `always()` in conditions. + +## Run 20879147120 (push to main, 2026-01-10) + +| Job | Conclusion | +|-----|------------| +| Detect Changes | success | +| Lint and Format Check | success | +| Test (ubuntu-latest) | success | +| Test (windows-latest) | success | +| Test (macos-latest) | success | +| Changelog Fragment Check | skipped | +| Build Package | success | +| Auto Release | **skipped** | +| Manual Release | skipped | + +**Problem**: Auto Release was unexpectedly skipped even though all upstream jobs passed because: +1. The `auto-release` condition lacked `always()` prefix +2. The `auto-release` condition didn't verify `needs.build.result == 'success'` + +## Fix Applied + +Updated `.github/workflows/rust.yml` to add `always() && !cancelled()` and result verification to: +- `lint` job +- `test` job +- `build` job +- `auto-release` job +- `manual-release` job + +This matches the pattern used in the reference template: +https://github.com/link-foundation/rust-ai-driven-development-pipeline-template