From d41bad2ca668e6bd1f9847c1331995c4ff357bd0 Mon Sep 17 00:00:00 2001 From: Leynos Date: Wed, 7 Jan 2026 05:06:52 +0000 Subject: [PATCH 01/10] Update release workflow to shared-actions@df81280 and add dry run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor the release workflow to use the updated shared-actions commit (df81280dcc1d6e66134114dbc924313328b15f05) and add support for dry run mode via a reusable workflow pattern. Changes to release.yml: - Add workflow_call trigger with dry-run and publish inputs - Add metadata job using determine-release-modes and ensure-cargo-version - Replace hardcoded version extraction with metadata job outputs - Add conditional artifact upload based on release mode flags - Replace softprops/action-gh-release with upload-release-assets action - Add concurrency group to prevent parallel release runs - Add permissions block for contents: write New release-dry-run.yml: - Triggers on PR events (opened, synchronize, reopened, ready_for_review) - Calls release.yml with dry-run: true - Enables CI verification of release packaging without publishing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release-dry-run.yml | 12 +++ .github/workflows/release.yml | 129 ++++++++++++++++++++++---- 2 files changed, 121 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/release-dry-run.yml diff --git a/.github/workflows/release-dry-run.yml b/.github/workflows/release-dry-run.yml new file mode 100644 index 0000000..ac6d127 --- /dev/null +++ b/.github/workflows/release-dry-run.yml @@ -0,0 +1,12 @@ +name: Release Dry Run + +'on': + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +jobs: + release: + uses: ./.github/workflows/release.yml + with: + dry-run: true + secrets: inherit diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c26c4eb..e3d1fb5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,13 +1,68 @@ name: Release -on: +'on': push: tags: - 'v*.*.*' + workflow_call: + inputs: + publish: + description: >- + Whether the workflow should publish artefacts to the GitHub release + associated with the tag. Defaults to `false` when invoked as a + reusable workflow. + required: false + type: boolean + default: false + dry-run: + description: >- + When `true`, build artefacts without uploading them to GitHub + releases or workflow artefacts. + required: false + type: boolean + default: false + outputs: + version: + description: Package version + value: ${{ jobs.metadata.outputs.version }} + should_publish: + description: Whether to publish + value: ${{ jobs.metadata.outputs.should_publish }} + dry_run: + description: Dry run mode + value: ${{ jobs.metadata.outputs.dry_run }} + should_upload_workflow_artifacts: + description: Upload workflow artifacts + value: ${{ jobs.metadata.outputs.should_upload_workflow_artifacts }} + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: true jobs: + metadata: + runs-on: ubuntu-latest + outputs: + version: ${{ steps.ensure_version.outputs.crate-version }} + should_publish: ${{ steps.release_modes.outputs.should-publish }} + dry_run: ${{ steps.release_modes.outputs.dry-run }} + should_upload_workflow_artifacts: ${{ steps.release_modes.outputs.should-upload-workflow-artifacts }} + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 0 + - name: Determine release modes + id: release_modes + uses: leynos/shared-actions/.github/actions/determine-release-modes@df81280dcc1d6e66134114dbc924313328b15f05 + - name: Read package version + id: ensure_version + uses: leynos/shared-actions/.github/actions/ensure-cargo-version@df81280dcc1d6e66134114dbc924313328b15f05 + with: + check-tag: ${{ fromJSON(steps.release_modes.outputs.should-publish) }} + build-packages: name: Build ${{ matrix.bin }} for ${{ matrix.target }} + needs: metadata runs-on: ubuntu-latest env: PACKAGE_DEB_DEPENDS: ${{ matrix.bin == 'comenqd' && 'bash, systemd' || '' }} @@ -32,32 +87,26 @@ jobs: uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Prepare release version - run: | - set -euo pipefail - version="${GITHUB_REF_NAME#v}" - echo "RELEASE_VERSION=${version}" >> "$GITHUB_ENV" - name: Clean dist directory run: rm -rf dist - name: Build ${{ matrix.bin }} - uses: leynos/shared-actions/.github/actions/rust-build-release@1479e2ffbbf1053bb0205357dfe965299b7493ed + uses: leynos/shared-actions/.github/actions/rust-build-release@df81280dcc1d6e66134114dbc924313328b15f05 with: target: ${{ matrix.target }} bin-name: ${{ matrix.bin }} - version: ${{ env.RELEASE_VERSION }} - formats: deb,rpm - name: Package ${{ matrix.bin }} - uses: leynos/shared-actions/.github/actions/linux-packages@1479e2ffbbf1053bb0205357dfe965299b7493ed + uses: leynos/shared-actions/.github/actions/linux-packages@df81280dcc1d6e66134114dbc924313328b15f05 with: bin-name: ${{ matrix.bin }} package-name: ${{ matrix.bin }} target: ${{ matrix.target }} - version: ${{ env.RELEASE_VERSION }} + version: ${{ needs.metadata.outputs.version }} formats: deb,rpm man-paths: dist/${{ matrix.bin }}_linux_${{ matrix.arch }}/${{ matrix.bin }}.1 deb-depends: ${{ env.PACKAGE_DEB_DEPENDS }} rpm-depends: ${{ env.PACKAGE_RPM_DEPENDS }} - name: Upload artefacts + if: fromJSON(needs.metadata.outputs.should_upload_workflow_artifacts) || needs.metadata.outputs.should_publish == 'true' uses: actions/upload-artifact@v4 with: name: ${{ matrix.bin }}-${{ matrix.arch }} @@ -67,22 +116,62 @@ jobs: dist/nfpm.yaml dist/.man/** if-no-files-found: error + release: name: Publish GitHub release + if: needs.metadata.outputs.should_publish == 'true' runs-on: ubuntu-latest - needs: build-packages + permissions: + contents: write + needs: [metadata, build-packages] steps: + - name: Checkout + uses: actions/checkout@v5 + - name: Ensure release exists (draft) + shell: bash + run: | + set -euo pipefail + gh release view "${{ github.ref_name }}" >/dev/null 2>&1 || \ + gh release create "${{ github.ref_name }}" \ + --draft \ + --verify-tag \ + --notes "Automated release for ${{ github.ref_name }}" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Download artefacts uses: actions/download-artifact@v4 with: - path: release-artifacts - - name: Create draft release - uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836 + path: dist + - id: upload_comenq + name: Upload comenq artefacts + uses: leynos/shared-actions/.github/actions/upload-release-assets@df81280dcc1d6e66134114dbc924313328b15f05 with: - tag_name: ${{ github.ref_name }} - draft: true - files: | - release-artifacts/**/*.deb - release-artifacts/**/*.rpm + release-tag: ${{ github.ref_name }} + bin-name: comenq + dist-dir: dist env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - id: upload_comenqd + name: Upload comenqd artefacts + uses: leynos/shared-actions/.github/actions/upload-release-assets@df81280dcc1d6e66134114dbc924313328b15f05 + with: + release-tag: ${{ github.ref_name }} + bin-name: comenqd + dist-dir: dist + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Check comenq upload errors + if: steps.upload_comenq.outputs.upload-error == 'true' + run: | + echo "Error uploading comenq release assets:" + printf '%s\n' "${{ steps.upload_comenq.outputs.error-message }}" + exit 1 + - name: Check comenqd upload errors + if: steps.upload_comenqd.outputs.upload-error == 'true' + run: | + echo "Error uploading comenqd release assets:" + printf '%s\n' "${{ steps.upload_comenqd.outputs.error-message }}" + exit 1 From 1b9dcb00e51797f6d8ee1f5f0d16393e0414ef51 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 8 Jan 2026 19:12:50 +0000 Subject: [PATCH 02/10] Add explicit binary targets and update workflow test assertions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add explicit [[bin]] sections to both comenq and comenqd crates to ensure Cargo correctly identifies and builds the binaries when building the workspace. Without explicit declarations, Cargo's auto-detection was failing because both crates have both lib.rs and main.rs files. Changes: - crates/comenq/Cargo.toml: Add [[bin]] section for comenq binary - crates/comenqd/Cargo.toml: Add [lib] and [[bin]] sections for clarity - test-support/src/workflow.rs: Update shared-actions SHA to df81280dcc1d6e66134114dbc924313328b15f05 and replace softprops/ action-gh-release check with upload-release-assets action reference This fixes the release dry-run workflow failure where cargo build completed but the binary was not found at the expected path target//release/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/comenq/Cargo.toml | 4 +++ crates/comenqd/Cargo.toml | 6 ++++ test-support/src/workflow.rs | 54 +++++++++++++++++++++++++----------- 3 files changed, 48 insertions(+), 16 deletions(-) diff --git a/crates/comenq/Cargo.toml b/crates/comenq/Cargo.toml index 4503182..5dd25f5 100644 --- a/crates/comenq/Cargo.toml +++ b/crates/comenq/Cargo.toml @@ -6,6 +6,10 @@ edition = "2024" [lib] path = "src/lib.rs" +[[bin]] +name = "comenq" +path = "src/main.rs" + [dependencies] tokio = { workspace = true } clap = { workspace = true } diff --git a/crates/comenqd/Cargo.toml b/crates/comenqd/Cargo.toml index 4ff565b..5138ed5 100644 --- a/crates/comenqd/Cargo.toml +++ b/crates/comenqd/Cargo.toml @@ -3,6 +3,12 @@ name = "comenqd" version = "0.1.0" edition = "2024" +[lib] +path = "src/lib.rs" + +[[bin]] +name = "comenqd" +path = "src/main.rs" [dependencies] tokio = { workspace = true } diff --git a/test-support/src/workflow.rs b/test-support/src/workflow.rs index b044ece..6f1f077 100644 --- a/test-support/src/workflow.rs +++ b/test-support/src/workflow.rs @@ -5,7 +5,7 @@ use serde_yaml::Value; // Provide the shared-actions commit hash as a literal so concat! can build constants without runtime formatting. macro_rules! shared_actions_commit_literal { () => { - "1479e2ffbbf1053bb0205357dfe965299b7493ed" + "df81280dcc1d6e66134114dbc924313328b15f05" }; } @@ -18,6 +18,10 @@ const EXPECTED_SHARED_ACTIONS_COMMIT: &str = shared_actions_commit_literal!(); /// The prefix for the shared release build composite action identifier. const RUST_BUILD_RELEASE_PREFIX: &str = "leynos/shared-actions/.github/actions/rust-build-release@"; +/// The prefix for the shared release asset upload composite action identifier. +const UPLOAD_RELEASE_ASSETS_PREFIX: &str = + "leynos/shared-actions/.github/actions/upload-release-assets@"; + /// The release builder action reference expected by tests, built at compile time to avoid allocations. #[cfg(test)] const EXPECTED_RUST_BUILDER: &str = concat!( @@ -25,6 +29,13 @@ const EXPECTED_RUST_BUILDER: &str = concat!( shared_actions_commit_literal!(), ); +/// The release publisher action reference expected by tests, built at compile time to avoid allocations. +#[cfg(test)] +const EXPECTED_UPLOAD_RELEASE_ASSETS: &str = concat!( + "leynos/shared-actions/.github/actions/upload-release-assets@", + shared_actions_commit_literal!(), +); + /// Return `true` when the release workflow uses the shared composite actions to /// build binaries and publish packages. /// @@ -57,7 +68,10 @@ pub fn uses_shared_release_actions(yaml: &str) -> Result Result Date: Sun, 11 Jan 2026 18:19:55 +0000 Subject: [PATCH 03/10] Specify project-dir for workspace crate builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The rust-build-release shared action builds the manifest at project-dir/Cargo.toml. Without project-dir, it defaults to the workspace root which builds comenq-lib (a library with no binary). By setting project-dir to crates/${{ matrix.bin }}, the action builds each binary crate directly, ensuring the expected binary is produced at target//release/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e3d1fb5..e387467 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -94,6 +94,7 @@ jobs: with: target: ${{ matrix.target }} bin-name: ${{ matrix.bin }} + project-dir: crates/${{ matrix.bin }} - name: Package ${{ matrix.bin }} uses: leynos/shared-actions/.github/actions/linux-packages@df81280dcc1d6e66134114dbc924313328b15f05 with: From 1dc0c1717bbd5357a9ed5e09d25ca05019e980b6 Mon Sep 17 00:00:00 2001 From: Leynos Date: Sun, 11 Jan 2026 20:25:11 +0000 Subject: [PATCH 04/10] Use manifest-path for workspace crate builds MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update shared-actions to cb06757 which supports the manifest-path parameter for rust-build-release. This allows building workspace member crates by specifying their Cargo.toml path directly. The previous project-dir approach did not work because the action still used --manifest-path Cargo.toml relative to the checkout root, building only the root comenq-lib package (a library with no binary). With manifest-path: crates/${{ matrix.bin }}/Cargo.toml, the action builds each binary crate directly, producing the expected binaries at target//release/. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 14 +++++++------- test-support/src/workflow.rs | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e387467..c5c35b4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -53,10 +53,10 @@ jobs: fetch-depth: 0 - name: Determine release modes id: release_modes - uses: leynos/shared-actions/.github/actions/determine-release-modes@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/determine-release-modes@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 - name: Read package version id: ensure_version - uses: leynos/shared-actions/.github/actions/ensure-cargo-version@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/ensure-cargo-version@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 with: check-tag: ${{ fromJSON(steps.release_modes.outputs.should-publish) }} @@ -90,13 +90,13 @@ jobs: - name: Clean dist directory run: rm -rf dist - name: Build ${{ matrix.bin }} - uses: leynos/shared-actions/.github/actions/rust-build-release@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/rust-build-release@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 with: target: ${{ matrix.target }} bin-name: ${{ matrix.bin }} - project-dir: crates/${{ matrix.bin }} + manifest-path: crates/${{ matrix.bin }}/Cargo.toml - name: Package ${{ matrix.bin }} - uses: leynos/shared-actions/.github/actions/linux-packages@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/linux-packages@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 with: bin-name: ${{ matrix.bin }} package-name: ${{ matrix.bin }} @@ -146,7 +146,7 @@ jobs: path: dist - id: upload_comenq name: Upload comenq artefacts - uses: leynos/shared-actions/.github/actions/upload-release-assets@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/upload-release-assets@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 with: release-tag: ${{ github.ref_name }} bin-name: comenq @@ -156,7 +156,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - id: upload_comenqd name: Upload comenqd artefacts - uses: leynos/shared-actions/.github/actions/upload-release-assets@df81280dcc1d6e66134114dbc924313328b15f05 + uses: leynos/shared-actions/.github/actions/upload-release-assets@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 with: release-tag: ${{ github.ref_name }} bin-name: comenqd diff --git a/test-support/src/workflow.rs b/test-support/src/workflow.rs index 6f1f077..b2a3d2b 100644 --- a/test-support/src/workflow.rs +++ b/test-support/src/workflow.rs @@ -5,7 +5,7 @@ use serde_yaml::Value; // Provide the shared-actions commit hash as a literal so concat! can build constants without runtime formatting. macro_rules! shared_actions_commit_literal { () => { - "df81280dcc1d6e66134114dbc924313328b15f05" + "cb06757ebba47bb018ac0ade84fa5dc9ffb95020" }; } From 102a7249485e89d48ff320051736a9c579ed3020 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 12 Jan 2026 12:38:32 +0000 Subject: [PATCH 05/10] Gate test-only code to fix release build warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In release builds (without test targets), code only used by tests appears as dead code. The -D warnings flag treats these as errors. Changes: - Gate METADATA_FILE_NAMES and is_metadata_file() with #[cfg(any(test, feature = "test-support"))] since they are only used in tests - Remove incorrect #[expect(dead_code)] from WorkerHooks::drained field, which is actually used in production code via notify_drained_if_empty() 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- crates/comenqd/src/util.rs | 3 +++ crates/comenqd/src/worker.rs | 4 ---- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/crates/comenqd/src/util.rs b/crates/comenqd/src/util.rs index 9b2c039..26d29f4 100644 --- a/crates/comenqd/src/util.rs +++ b/crates/comenqd/src/util.rs @@ -2,11 +2,13 @@ //! //! Provides helpers used across production code and tests. +#[cfg(any(test, feature = "test-support"))] use std::ffi::OsStr; /// Names of files storing queue metadata. /// /// Extend this list when new metadata files are introduced. +#[cfg(any(test, feature = "test-support"))] pub(crate) const METADATA_FILE_NAMES: [&str; 3] = ["version", "recv.lock", "send.lock"]; /// Returns whether a file name represents queue metadata. @@ -19,6 +21,7 @@ pub(crate) const METADATA_FILE_NAMES: [&str; 3] = ["version", "recv.lock", "send /// assert!(is_metadata_file(OsStr::new("version"))); /// assert!(!is_metadata_file(OsStr::new("0001"))); /// ``` +#[cfg(any(test, feature = "test-support"))] pub fn is_metadata_file(name: impl AsRef) -> bool { let name = name.as_ref(); METADATA_FILE_NAMES.iter().any(|m| OsStr::new(m) == name) diff --git a/crates/comenqd/src/worker.rs b/crates/comenqd/src/worker.rs index 801d6ed..259de41 100644 --- a/crates/comenqd/src/worker.rs +++ b/crates/comenqd/src/worker.rs @@ -70,10 +70,6 @@ pub struct WorkerHooks { /// Signalled when the queue is empty and the worker is idle. /// /// Only one waiter is supported; additional waiters will not be notified. - #[cfg_attr( - not(any(test, feature = "test-support")), - expect(dead_code, reason = "test hook only used in test/test-support builds") - )] pub drained: Option>, } From a66566ca94ae28cc354227508b470711799a70b7 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 12 Jan 2026 22:56:55 +0000 Subject: [PATCH 06/10] Forward workflow inputs to determine-release-modes action MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pass inputs.dry-run and inputs.publish to the determine-release-modes action so that when release-dry-run.yml invokes the workflow with dry-run: true, the action respects that override instead of relying solely on event context. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5c35b4..63e81d8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,9 @@ jobs: - name: Determine release modes id: release_modes uses: leynos/shared-actions/.github/actions/determine-release-modes@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 + with: + dry-run: ${{ inputs.dry-run }} + publish: ${{ inputs.publish }} - name: Read package version id: ensure_version uses: leynos/shared-actions/.github/actions/ensure-cargo-version@cb06757ebba47bb018ac0ade84fa5dc9ffb95020 From 129d5c3cc05f4fd62dea5e52de7b090b76399579 Mon Sep 17 00:00:00 2001 From: Leynos Date: Mon, 12 Jan 2026 22:58:32 +0000 Subject: [PATCH 07/10] Use consistent boolean handling in release workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replace string comparison `== 'true'` with `fromJSON()` for consistent boolean parsing across all workflow conditions - Combine sequential upload error checks into a single step that reports all errors before failing, rather than terminating on the first failure 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 31 ++++++++++++++++++------------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 63e81d8..2371cd8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -110,7 +110,7 @@ jobs: deb-depends: ${{ env.PACKAGE_DEB_DEPENDS }} rpm-depends: ${{ env.PACKAGE_RPM_DEPENDS }} - name: Upload artefacts - if: fromJSON(needs.metadata.outputs.should_upload_workflow_artifacts) || needs.metadata.outputs.should_publish == 'true' + if: fromJSON(needs.metadata.outputs.should_upload_workflow_artifacts) || fromJSON(needs.metadata.outputs.should_publish) uses: actions/upload-artifact@v4 with: name: ${{ matrix.bin }}-${{ matrix.arch }} @@ -123,7 +123,7 @@ jobs: release: name: Publish GitHub release - if: needs.metadata.outputs.should_publish == 'true' + if: fromJSON(needs.metadata.outputs.should_publish) runs-on: ubuntu-latest permissions: contents: write @@ -167,15 +167,20 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: Check comenq upload errors - if: steps.upload_comenq.outputs.upload-error == 'true' + - name: Check upload errors + if: steps.upload_comenq.outputs.upload-error == 'true' || steps.upload_comenqd.outputs.upload-error == 'true' run: | - echo "Error uploading comenq release assets:" - printf '%s\n' "${{ steps.upload_comenq.outputs.error-message }}" - exit 1 - - name: Check comenqd upload errors - if: steps.upload_comenqd.outputs.upload-error == 'true' - run: | - echo "Error uploading comenqd release assets:" - printf '%s\n' "${{ steps.upload_comenqd.outputs.error-message }}" - exit 1 + has_error=false + if [ "${{ steps.upload_comenq.outputs.upload-error }}" = "true" ]; then + echo "Error uploading comenq release assets:" + printf '%s\n' "${{ steps.upload_comenq.outputs.error-message }}" + has_error=true + fi + if [ "${{ steps.upload_comenqd.outputs.upload-error }}" = "true" ]; then + echo "Error uploading comenqd release assets:" + printf '%s\n' "${{ steps.upload_comenqd.outputs.error-message }}" + has_error=true + fi + if [ "$has_error" = "true" ]; then + exit 1 + fi From 73d60122ceafec1d4ef5b8d1e83842dbdd88f4b1 Mon Sep 17 00:00:00 2001 From: Leynos Date: Thu, 15 Jan 2026 00:40:13 +0000 Subject: [PATCH 08/10] Harden release workflow concurrency and error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Set cancel-in-progress to false so release workflows complete fully rather than being cancelled mid-upload when a new workflow starts - Fix shell injection risk in upload error checking by exporting step outputs as environment variables and referencing them safely within the script 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .github/workflows/release.yml | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2371cd8..968720d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -37,7 +37,7 @@ name: Release concurrency: group: release-${{ github.ref }} - cancel-in-progress: true + cancel-in-progress: false jobs: metadata: @@ -169,16 +169,21 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Check upload errors if: steps.upload_comenq.outputs.upload-error == 'true' || steps.upload_comenqd.outputs.upload-error == 'true' + env: + UPLOAD_ERROR_COMENQ: ${{ steps.upload_comenq.outputs.upload-error }} + UPLOAD_ERROR_COMENQD: ${{ steps.upload_comenqd.outputs.upload-error }} + ERROR_MSG_COMENQ: ${{ steps.upload_comenq.outputs.error-message }} + ERROR_MSG_COMENQD: ${{ steps.upload_comenqd.outputs.error-message }} run: | has_error=false - if [ "${{ steps.upload_comenq.outputs.upload-error }}" = "true" ]; then + if [ "$UPLOAD_ERROR_COMENQ" = "true" ]; then echo "Error uploading comenq release assets:" - printf '%s\n' "${{ steps.upload_comenq.outputs.error-message }}" + printf '%s\n' "$ERROR_MSG_COMENQ" has_error=true fi - if [ "${{ steps.upload_comenqd.outputs.upload-error }}" = "true" ]; then + if [ "$UPLOAD_ERROR_COMENQD" = "true" ]; then echo "Error uploading comenqd release assets:" - printf '%s\n' "${{ steps.upload_comenqd.outputs.error-message }}" + printf '%s\n' "$ERROR_MSG_COMENQD" has_error=true fi if [ "$has_error" = "true" ]; then From 010de31d14a963160398126f5cb4caea5131664e Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 16 Jan 2026 20:48:51 +0000 Subject: [PATCH 09/10] Add negative tests for upload-release-assets action pinning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests to verify uses_shared_release_actions returns false when the upload-release-assets action is pinned to a different commit SHA or uses a tag reference instead of a commit SHA. - mismatched_publisher_commit_fails: verifies detection of wrong SHA - unpinned_publisher_fails: verifies detection of tag reference (@v1) These tests are analogous to the existing mismatched_builder_commit_fails and unpinned_builder_fails tests for the rust-build-release action. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- test-support/src/workflow.rs | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test-support/src/workflow.rs b/test-support/src/workflow.rs index b2a3d2b..45f7e8d 100644 --- a/test-support/src/workflow.rs +++ b/test-support/src/workflow.rs @@ -164,6 +164,36 @@ mod tests { assert!(!uses_shared_release_actions(&yaml).expect("parse")); } + #[test] + #[expect(clippy::expect_used, reason = "simplify test output")] + fn mismatched_publisher_commit_fails() { + let yaml = format!( + r#" + jobs: + release: + steps: + - uses: {EXPECTED_RUST_BUILDER} + - uses: leynos/shared-actions/.github/actions/upload-release-assets@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef + "# + ); + assert!(!uses_shared_release_actions(&yaml).expect("parse")); + } + + #[test] + #[expect(clippy::expect_used, reason = "simplify test output")] + fn unpinned_publisher_fails() { + let yaml = format!( + r#" + jobs: + release: + steps: + - uses: {EXPECTED_RUST_BUILDER} + - uses: leynos/shared-actions/.github/actions/upload-release-assets@v1 + "# + ); + assert!(!uses_shared_release_actions(&yaml).expect("parse")); + } + #[test] #[expect(clippy::expect_used, reason = "simplify test output")] fn malformed_yaml_errors() { From 347b74f0803039346c6f2de37b205de8df9f2c8c Mon Sep 17 00:00:00 2001 From: Leynos Date: Fri, 16 Jan 2026 23:37:56 +0000 Subject: [PATCH 10/10] Consolidate action pinning tests with rstest parameterisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace four near-identical test functions (mismatched_builder_commit_fails, unpinned_builder_fails, mismatched_publisher_commit_fails, unpinned_publisher_fails) with a single parameterised rstest function that covers all four cases. - Add rstest as dev-dependency to test-support crate - Use #[case] attributes to define each invalid pinning scenario - Reduce test code duplication while maintaining full coverage 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 1 + test-support/Cargo.toml | 1 + test-support/src/workflow.rs | 71 +++++++++++------------------------- 3 files changed, 23 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3109041..3a8ded3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2526,6 +2526,7 @@ name = "test-support" version = "0.1.0" dependencies = [ "octocrab", + "rstest", "serde", "serde_yaml", "serial_test", diff --git a/test-support/Cargo.toml b/test-support/Cargo.toml index 26f34a9..d5e2b7f 100644 --- a/test-support/Cargo.toml +++ b/test-support/Cargo.toml @@ -13,4 +13,5 @@ serde = { workspace = true } tracing-subscriber = { workspace = true } [dev-dependencies] +rstest = { workspace = true } serial_test = "^2" diff --git a/test-support/src/workflow.rs b/test-support/src/workflow.rs index 45f7e8d..a363107 100644 --- a/test-support/src/workflow.rs +++ b/test-support/src/workflow.rs @@ -87,6 +87,7 @@ mod tests { EXPECTED_RUST_BUILDER, EXPECTED_SHARED_ACTIONS_COMMIT, EXPECTED_UPLOAD_RELEASE_ASSETS, uses_shared_release_actions, }; + use rstest::rstest; #[test] #[expect(clippy::expect_used, reason = "simplify test output")] @@ -134,61 +135,31 @@ mod tests { assert!(!uses_shared_release_actions(&yaml).expect("parse")); } - #[test] - #[expect(clippy::expect_used, reason = "simplify test output")] - fn mismatched_builder_commit_fails() { - let yaml = format!( - r#" - jobs: - release: - steps: - - uses: leynos/shared-actions/.github/actions/rust-build-release@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef - - uses: {EXPECTED_UPLOAD_RELEASE_ASSETS} - "# - ); - assert!(!uses_shared_release_actions(&yaml).expect("parse")); - } - - #[test] - #[expect(clippy::expect_used, reason = "simplify test output")] - fn unpinned_builder_fails() { - let yaml = format!( - r#" - jobs: - release: - steps: - - uses: leynos/shared-actions/.github/actions/rust-build-release@v1 - - uses: {EXPECTED_UPLOAD_RELEASE_ASSETS} - "# - ); - assert!(!uses_shared_release_actions(&yaml).expect("parse")); - } - - #[test] - #[expect(clippy::expect_used, reason = "simplify test output")] - fn mismatched_publisher_commit_fails() { + #[rstest] + #[case::mismatched_builder_commit( + "leynos/shared-actions/.github/actions/rust-build-release@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef", + EXPECTED_UPLOAD_RELEASE_ASSETS + )] + #[case::unpinned_builder( + "leynos/shared-actions/.github/actions/rust-build-release@v1", + EXPECTED_UPLOAD_RELEASE_ASSETS + )] + #[case::mismatched_publisher_commit( + EXPECTED_RUST_BUILDER, + "leynos/shared-actions/.github/actions/upload-release-assets@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + )] + #[case::unpinned_publisher( + EXPECTED_RUST_BUILDER, + "leynos/shared-actions/.github/actions/upload-release-assets@v1" + )] + fn invalid_action_pinning_fails(#[case] builder: &str, #[case] publisher: &str) { let yaml = format!( r#" jobs: release: steps: - - uses: {EXPECTED_RUST_BUILDER} - - uses: leynos/shared-actions/.github/actions/upload-release-assets@deadbeefdeadbeefdeadbeefdeadbeefdeadbeef - "# - ); - assert!(!uses_shared_release_actions(&yaml).expect("parse")); - } - - #[test] - #[expect(clippy::expect_used, reason = "simplify test output")] - fn unpinned_publisher_fails() { - let yaml = format!( - r#" - jobs: - release: - steps: - - uses: {EXPECTED_RUST_BUILDER} - - uses: leynos/shared-actions/.github/actions/upload-release-assets@v1 + - uses: {builder} + - uses: {publisher} "# ); assert!(!uses_shared_release_actions(&yaml).expect("parse"));