From d1cb3c961ed4d7644f1beeee16094095ed15da2d Mon Sep 17 00:00:00 2001 From: Tim Cogan Date: Thu, 16 Apr 2026 08:09:18 -0500 Subject: [PATCH 1/3] ci(release): automate GitHub releases from Cargo.toml version bumps --- .github/workflows/release.yml | 84 ++++++++++++++++++++++++++++++----- RELEASE.md | 16 ++++--- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0927f69..55f2483 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,5 +1,6 @@ # This file is based on the cargo-dist GitHub Actions template. -# It publishes artifacts to GitHub Releases on version tags. +# It publishes artifacts to GitHub Releases when a version bump lands on the +# default branch, creating the tag as part of the release. name: Release permissions: @@ -8,8 +9,11 @@ permissions: on: pull_request: push: - tags: - - 'v[0-9]+.[0-9]+.[0-9]+' + branches: + - main + - master + paths: + - Cargo.toml concurrency: group: dist-${{ github.ref }} @@ -20,16 +24,72 @@ jobs: runs-on: ubuntu-22.04 outputs: val: ${{ steps.plan.outputs.manifest }} - tag: ${{ !github.event.pull_request && github.ref_name || '' }} - tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }} - publishing: ${{ !github.event.pull_request }} + tag: ${{ steps.release_meta.outputs.tag }} + tag_flag: ${{ steps.release_meta.outputs.tag_flag }} + publishing: ${{ steps.release_meta.outputs.publishing }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v6 with: + fetch-depth: 0 persist-credentials: false submodules: recursive + - id: release_meta + shell: bash + env: + EVENT_NAME: ${{ github.event_name }} + BEFORE_SHA: ${{ github.event.before }} + run: | + set -euo pipefail + + current_version="$( + awk ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $3; exit } + ' Cargo.toml | tr -d '"' + )" + + [[ -n "$current_version" ]] || { + echo "failed to determine package version from Cargo.toml" >&2 + exit 1 + } + + tag="v$current_version" + publishing=false + previous_version="" + + if [[ "$EVENT_NAME" == "push" ]]; then + if [[ -n "${BEFORE_SHA:-}" && "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]]; then + if previous_manifest="$(git show "$BEFORE_SHA:Cargo.toml" 2>/dev/null)"; then + previous_version="$( + printf '%s\n' "$previous_manifest" | awk ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $3; exit } + ' | tr -d '"' + )" + fi + fi + + if [[ "$previous_version" != "$current_version" ]]; then + [[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "release automation only supports Cargo.toml versions in X.Y.Z format, found: $current_version" >&2 + exit 1 + } + + if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then + echo "tag $tag already exists; skipping publish" + else + publishing=true + fi + fi + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "tag_flag=--tag=$tag" >> "$GITHUB_OUTPUT" + echo "publishing=$publishing" >> "$GITHUB_OUTPUT" - name: Install dist shell: bash run: | @@ -41,13 +101,13 @@ jobs: path: ~/.cargo/bin/dist - id: plan env: - TAG: ${{ github.ref_name }} + TAG: ${{ steps.release_meta.outputs.tag }} + PUBLISHING: ${{ steps.release_meta.outputs.publishing }} run: | set -euo pipefail - if [[ "${{ github.event_name }}" == "pull_request" ]]; then + if [[ "${{ github.event_name }}" == "pull_request" || "$PUBLISHING" != "true" ]]; then dist plan --output-format=json --target=x86_64-unknown-linux-gnu > plan-dist-manifest.json else - [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1 dist host --steps=create --tag="$TAG" --output-format=json > plan-dist-manifest.json fi echo "dist ran successfully" @@ -104,7 +164,7 @@ jobs: - name: Build artifacts shell: bash env: - TAG_FLAG: ${{ needs.plan.outputs.tag-flag }} + TAG_FLAG: ${{ needs.plan.outputs.tag_flag }} run: | set -euo pipefail dist build $TAG_FLAG --target=${{ join(matrix.targets, ',') }} --print=linkage --output-format=json ${{ matrix.dist_args }} > dist-manifest.json @@ -156,7 +216,7 @@ jobs: - id: cargo-dist shell: bash env: - TAG_FLAG: ${{ needs.plan.outputs.tag-flag }} + TAG_FLAG: ${{ needs.plan.outputs.tag_flag }} run: | set -euo pipefail dist build $TAG_FLAG --target=x86_64-unknown-linux-gnu --output-format=json --artifacts=global > dist-manifest.json @@ -201,7 +261,7 @@ jobs: - id: host shell: bash env: - TAG_FLAG: ${{ needs.plan.outputs.tag-flag }} + TAG_FLAG: ${{ needs.plan.outputs.tag_flag }} run: | set -euo pipefail [[ "$TAG_FLAG" =~ ^--tag=v[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1 diff --git a/RELEASE.md b/RELEASE.md index f45c166..7bcec82 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,7 +1,8 @@ # Release Process This project uses Semantic Versioning and publishes binaries via GitHub Actions. -The release workflow triggers only on tags that match `vX.Y.Z` (no suffixes). +The release workflow watches for a version bump in `Cargo.toml` on `main` or +`master`, then creates the `vX.Y.Z` tag and GitHub Release automatically. ## Versioning Rules (SemVer) @@ -15,12 +16,15 @@ The release workflow triggers only on tags that match `vX.Y.Z` (no suffixes). - `version = "X.Y.Z"` 2. Commit the release change: - Example message: `chore(release): vX.Y.Z` -3. Create an annotated tag: - - `git tag -a vX.Y.Z -m "vX.Y.Z"` -4. Push the commit and tag: - - `git push origin master --tags` +3. Push the commit to `main` or `master`. +4. GitHub Actions will: + - detect that the package version changed + - reserve `vX.Y.Z` for cargo-dist + - create the GitHub Release and corresponding tag automatically ## Notes -- The CI release workflow only triggers on tags that match `vX.Y.Z` exactly. +- Automatic publishing only happens when the version changes and the + corresponding `vX.Y.Z` tag does not already exist. +- Release automation only supports plain `X.Y.Z` versions (no suffixes). - `dist-workspace.toml` does not need a version bump for application releases; it only changes when the dist tool version or release targets change. From 6dd39ab70f9a32f1d347c261b0e2bb47f1cd1746 Mon Sep 17 00:00:00 2001 From: Tim Cogan Date: Thu, 16 Apr 2026 08:41:00 -0500 Subject: [PATCH 2/3] Address comments and pin rust-toolchain version --- .github/dependabot.yml | 14 ++++++++ .github/workflows/ci.yml | 33 +++++++++++++++++- .github/workflows/release.yml | 64 ++++++++++++++++++++++++----------- RELEASE.md | 6 +++- rust-toolchain.toml | 4 +++ src/launch.rs | 46 ++++++++++++------------- 6 files changed, 121 insertions(+), 46 deletions(-) create mode 100644 rust-toolchain.toml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9beb348..296dd23 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,3 +44,17 @@ updates: github-actions: patterns: - "*" + + - package-ecosystem: rust-toolchain + directory: / + schedule: + interval: weekly + day: tuesday + time: "09:00" + timezone: America/Chicago + open-pull-requests-limit: 2 + assignees: + - timcogan + commit-message: + prefix: chore + include: scope diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf608ed..df3dfd1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,6 +16,37 @@ concurrency: cancel-in-progress: true jobs: + version: + name: Version Checks + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Validate release version format + shell: bash + run: | + set -euo pipefail + + current_version="$( + awk ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $3; exit } + ' Cargo.toml | tr -d '"' + )" + + [[ -n "$current_version" ]] || { + echo "failed to determine package version from Cargo.toml" >&2 + exit 1 + } + + [[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "release automation only supports Cargo.toml versions in X.Y.Z format, found: $current_version" >&2 + exit 1 + } + rust: name: Rust Checks runs-on: ubuntu-latest @@ -27,7 +58,7 @@ jobs: fetch-depth: 0 - name: Install Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: dtolnay/rust-toolchain@1.95.0 with: components: rustfmt, clippy diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 55f2483..4a73239 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,12 +8,15 @@ permissions: on: pull_request: + workflow_dispatch: push: branches: - main - master paths: - Cargo.toml + - dist-workspace.toml + - .github/workflows/release.yml concurrency: group: dist-${{ github.ref }} @@ -40,6 +43,7 @@ jobs: env: EVENT_NAME: ${{ github.event_name }} BEFORE_SHA: ${{ github.event.before }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -euo pipefail @@ -56,36 +60,56 @@ jobs: exit 1 } + [[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "release automation only supports Cargo.toml versions in X.Y.Z format, found: $current_version" >&2 + exit 1 + } + tag="v$current_version" publishing=false previous_version="" + release_state="missing" - if [[ "$EVENT_NAME" == "push" ]]; then - if [[ -n "${BEFORE_SHA:-}" && "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]]; then - if previous_manifest="$(git show "$BEFORE_SHA:Cargo.toml" 2>/dev/null)"; then - previous_version="$( - printf '%s\n' "$previous_manifest" | awk ' - /^\[package\]$/ { in_package = 1; next } - /^\[/ { in_package = 0 } - in_package && /^version = / { print $3; exit } - ' | tr -d '"' - )" - fi + if gh release view "$tag" --json isDraft > release-state.json 2>/dev/null; then + if jq -e '.isDraft' release-state.json > /dev/null; then + release_state="draft" + else + release_state="published" fi + elif git rev-parse -q --verify "refs/tags/$tag" > /dev/null; then + release_state="tag-only" + fi - if [[ "$previous_version" != "$current_version" ]]; then - [[ "$current_version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { - echo "release automation only supports Cargo.toml versions in X.Y.Z format, found: $current_version" >&2 - exit 1 - } + case "$EVENT_NAME" in + push) + if [[ -n "${BEFORE_SHA:-}" && "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]]; then + if previous_manifest="$(git show "$BEFORE_SHA:Cargo.toml" 2>/dev/null)"; then + previous_version="$( + printf '%s\n' "$previous_manifest" | awk ' + /^\[package\]$/ { in_package = 1; next } + /^\[/ { in_package = 0 } + in_package && /^version = / { print $3; exit } + ' | tr -d '"' + )" + fi + fi - if git rev-parse -q --verify "refs/tags/$tag" >/dev/null; then - echo "tag $tag already exists; skipping publish" + if [[ "$previous_version" != "$current_version" ]]; then + if [[ "$release_state" == "published" ]]; then + echo "release $tag already exists; skipping publish" + else + publishing=true + fi + fi + ;; + workflow_dispatch) + if [[ "$release_state" == "published" ]]; then + echo "release $tag already exists; skipping publish" else publishing=true fi - fi - fi + ;; + esac echo "tag=$tag" >> "$GITHUB_OUTPUT" echo "tag_flag=--tag=$tag" >> "$GITHUB_OUTPUT" diff --git a/RELEASE.md b/RELEASE.md index 7bcec82..2f148b8 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -3,6 +3,8 @@ This project uses Semantic Versioning and publishes binaries via GitHub Actions. The release workflow watches for a version bump in `Cargo.toml` on `main` or `master`, then creates the `vX.Y.Z` tag and GitHub Release automatically. +If a release needs to be retried, the workflow can also be started manually with +GitHub Actions `workflow_dispatch`. ## Versioning Rules (SemVer) @@ -21,10 +23,12 @@ The release workflow watches for a version bump in `Cargo.toml` on `main` or - detect that the package version changed - reserve `vX.Y.Z` for cargo-dist - create the GitHub Release and corresponding tag automatically +5. If a release fails after the version bump landed, rerun the existing workflow + or start the `Release` workflow manually from the Actions tab. ## Notes - Automatic publishing only happens when the version changes and the - corresponding `vX.Y.Z` tag does not already exist. + corresponding `vX.Y.Z` release has not already been published. - Release automation only supports plain `X.Y.Z` versions (no suffixes). - `dist-workspace.toml` does not need a version bump for application releases; it only changes when the dist tool version or release targets change. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..8770b1d --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,4 @@ +[toolchain] +channel = "1.95.0" +profile = "minimal" +components = ["clippy", "rustfmt"] diff --git a/src/launch.rs b/src/launch.rs index 7bf359b..e7cf842 100644 --- a/src/launch.rs +++ b/src/launch.rs @@ -83,10 +83,8 @@ pub fn parse_perspecta_uri(uri: &str) -> Result { let key = key.trim().to_ascii_lowercase(); let decoded_value = percent_decode(value)?; match key.as_str() { - "path" | "file" => { - if !decoded_value.trim().is_empty() { - raw_paths.push(decoded_value); - } + "path" | "file" if !decoded_value.trim().is_empty() => { + raw_paths.push(decoded_value); } "paths" | "files" => { let split_paths = split_path_list(&decoded_value); @@ -157,30 +155,30 @@ pub fn parse_perspecta_uri(uri: &str) -> Result { } } } - "study" | "studyuid" | "studyinstanceuid" | "study_instance_uid" => { - if !decoded_value.trim().is_empty() { - study_uid = Some(decoded_value.trim().to_string()); - } + "study" | "studyuid" | "studyinstanceuid" | "study_instance_uid" + if !decoded_value.trim().is_empty() => + { + study_uid = Some(decoded_value.trim().to_string()); } - "series" | "seriesuid" | "seriesinstanceuid" | "series_instance_uid" => { - if !decoded_value.trim().is_empty() { - series_uid = Some(decoded_value.trim().to_string()); - } + "series" | "seriesuid" | "seriesinstanceuid" | "series_instance_uid" + if !decoded_value.trim().is_empty() => + { + series_uid = Some(decoded_value.trim().to_string()); } - "instance" | "instanceuid" | "sopinstanceuid" | "sop_instance_uid" => { - if !decoded_value.trim().is_empty() { - instance_uid = Some(decoded_value.trim().to_string()); - } + "instance" | "instanceuid" | "sopinstanceuid" | "sop_instance_uid" + if !decoded_value.trim().is_empty() => + { + instance_uid = Some(decoded_value.trim().to_string()); } - "user" | "username" | "dicomweb_user" | "dicomweb_username" => { - if !decoded_value.trim().is_empty() { - dicomweb_username = Some(decoded_value.trim().to_string()); - } + "user" | "username" | "dicomweb_user" | "dicomweb_username" + if !decoded_value.trim().is_empty() => + { + dicomweb_username = Some(decoded_value.trim().to_string()); } - "pass" | "password" | "dicomweb_pass" | "dicomweb_password" => { - if !decoded_value.trim().is_empty() { - dicomweb_password = Some(decoded_value.trim().to_string()); - } + "pass" | "password" | "dicomweb_pass" | "dicomweb_password" + if !decoded_value.trim().is_empty() => + { + dicomweb_password = Some(decoded_value.trim().to_string()); } "auth" | "dicomweb_auth" => { let trimmed = decoded_value.trim(); From 2b09ad4027319546717768991898042d004ec67e Mon Sep 17 00:00:00 2001 From: Tim Cogan Date: Thu, 16 Apr 2026 10:42:19 -0500 Subject: [PATCH 3/3] Add minor improvements to `ci.yml` --- .github/workflows/ci.yml | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df3dfd1..8282cf3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,9 +31,9 @@ jobs: current_version="$( awk ' - /^\[package\]$/ { in_package = 1; next } - /^\[/ { in_package = 0 } - in_package && /^version = / { print $3; exit } + /^\s*\[package\]\s*$/ { in_package = 1; next } + /^\s*\[/ { in_package = 0 } + in_package && /^\s*version\s*=\s*/ { print $3; exit } ' Cargo.toml | tr -d '"' )" @@ -49,6 +49,7 @@ jobs: rust: name: Rust Checks + needs: [version] runs-on: ubuntu-latest steps: @@ -76,6 +77,7 @@ jobs: gitleaks: name: Secret Scan + needs: [version] runs-on: ubuntu-latest steps: