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
14 changes: 14 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
35 changes: 34 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,40 @@ 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 '
/^\s*\[package\]\s*$/ { in_package = 1; next }
/^\s*\[/ { in_package = 0 }
in_package && /^\s*version\s*=\s*/ { 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
needs: [version]
runs-on: ubuntu-latest

steps:
Expand All @@ -27,7 +59,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

Expand All @@ -45,6 +77,7 @@ jobs:

gitleaks:
name: Secret Scan
needs: [version]
runs-on: ubuntu-latest

steps:
Expand Down
108 changes: 96 additions & 12 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
# 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:
contents: read

on:
pull_request:
workflow_dispatch:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+'
branches:
- main
- master
paths:
- Cargo.toml
Comment thread
coderabbitai[bot] marked this conversation as resolved.
- dist-workspace.toml
- .github/workflows/release.yml

concurrency:
group: dist-${{ github.ref }}
Expand All @@ -20,16 +27,93 @@ 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 }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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
}

tag="v$current_version"
publishing=false
previous_version=""
release_state="missing"

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

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 [[ "$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
;;
esac

echo "tag=$tag" >> "$GITHUB_OUTPUT"
echo "tag_flag=--tag=$tag" >> "$GITHUB_OUTPUT"
echo "publishing=$publishing" >> "$GITHUB_OUTPUT"
- name: Install dist
shell: bash
run: |
Expand All @@ -41,13 +125,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"
Expand Down Expand Up @@ -104,7 +188,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
Expand Down Expand Up @@ -156,7 +240,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
Expand Down Expand Up @@ -201,7 +285,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
Expand Down
20 changes: 14 additions & 6 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# 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.
If a release needs to be retried, the workflow can also be started manually with
GitHub Actions `workflow_dispatch`.

## Versioning Rules (SemVer)

Expand All @@ -15,12 +18,17 @@ 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
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

- 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` 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.
4 changes: 4 additions & 0 deletions rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
[toolchain]
channel = "1.95.0"
profile = "minimal"
components = ["clippy", "rustfmt"]
46 changes: 22 additions & 24 deletions src/launch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -83,10 +83,8 @@ pub fn parse_perspecta_uri(uri: &str) -> Result<LaunchRequest, String> {
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);
Expand Down Expand Up @@ -157,30 +155,30 @@ pub fn parse_perspecta_uri(uri: &str) -> Result<LaunchRequest, String> {
}
}
}
"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();
Expand Down