change-user-run 0.1.1

Run commands as other users and create users
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
#!/usr/bin/env -S just --working-directory . --justfile
# Load project-specific properties from the `.env` file

set dotenv-load := true

# Whether coverage should be measured when running tests. Use `create-coverage-report` to create a report from the collected data.

coverage := env("COVERAGE_REPORT", "false")

# Lists all available recipes.
default:
    just --list

# Adds pre-commit and pre-push git hooks
[private]
add-hooks:
    #!/usr/bin/env bash
    set -euo pipefail

    echo just run-pre-commit-hook > .git/hooks/pre-commit
    chmod +x .git/hooks/pre-commit

    echo just run-pre-push-hook > .git/hooks/pre-push
    chmod +x .git/hooks/pre-push

    cat > .git/hooks/prepare-commit-msg <<'EOL'
    #!/bin/sh

    COMMIT_MSG_FILE=$1
    COMMIT_SOURCE=$2

    SOB=$(git var GIT_COMMITTER_IDENT | sed -n 's/^\(.*>\).*$/Signed-off-by: \1/p')
    git interpret-trailers --in-place --trailer "$SOB" "$COMMIT_MSG_FILE"
    if test -z "$COMMIT_SOURCE"; then
        /usr/bin/perl -i.bak -pe 'print "\n" if !$first_line++' "$COMMIT_MSG_FILE"
    fi
    EOL
    chmod +x .git/hooks/prepare-commit-msg

# Updates the local cargo index and displays which crates would be updated
[private]
dry-update:
    cargo update --dry-run --verbose

# Ensures that one or more required commands are installed
[private]
ensure-command +command:
    #!/usr/bin/env bash
    set -euo pipefail

    read -r -a commands <<< "{{ command }}"

    for cmd in "${commands[@]}"; do
        if ! command -v "$cmd" > /dev/null 2>&1 ; then
            printf "Couldn't find required executable '%s'\n" "$cmd" >&2
            exit 1
        fi
    done

# Retrieves the configured target directory for cargo.
[private]
get-cargo-target-directory:
    just ensure-command cargo jq
    cargo metadata --format-version 1 | jq -r  .target_directory

# Gets metadata version of a workspace member.
[private]
get-workspace-member-version package:
    #!/usr/bin/env bash
    set -euo pipefail

    version="$(cargo metadata --format-version=1 | jq -r --arg pkg {{ package }} '.workspace_members[] | capture("/(?<name>[a-z-]+)#(?<version>[0-9.]+)") | select(.name == $pkg).version')"

    if [[ -z "$version" ]]; then
        printf "No version found for package %s\n" {{ package }} >&2
        exit 1
    fi

    printf "%s\n" "$version"

# Prints a section start marker for GitLab collapsible sections. Accepts a `section_name` and its `title`.
[private]
gitlab-section-start section_name title:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly ci="${CI:-}"
    readonly section_name="{{ section_name }}"
    readonly title="{{ title }}"

    if [[ "$ci" == "true" ]]; then
        printf '\e[0Ksection_start:%(%s)T:%s\r\e[0K%s\n' '-1' "$section_name" "$title"
    fi

# Prints a section end marker for GitLab collapsible sections. Accepts a `section_name`.
[private]
gitlab-section-end section_name:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly ci="${CI:-}"
    readonly section_name="{{ section_name }}"

    if [[ "$ci" == "true" ]]; then
        printf '\e[0Ksection_end:%(%s)T:%s\r\e[0K\n' '-1' "$section_name"
    fi

# Installs a `set` of ALPM packages.
[private]
install-alpm-package-set set:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly set="{{ set }}"

    case "$set" in
        all)
            packages="$PACMAN_PACKAGES"
            ;;
        commits)
            packages="$CHECK_COMMITS_PACKAGES"
            ;;
        containerized)
            packages="$CONTAINERIZED_TEST_PACKAGES"
            ;;
        coverage)
            packages="$TEST_COVERAGE_PACKAGES"
            ;;
        docs)
            packages="$DOCS_PACKAGES"
            ;;
        dependencies)
            packages="$CHECK_DEPENDENCIES_PACKAGES"
            ;;
        formatting)
            packages="$FORMAT_PACKAGES"
            ;;
        licenses)
            packages="$CHECK_LICENSES_PACKAGES"
            ;;
        links)
            packages="$CHECK_LINKS_PACKAGES"
            ;;
        publish)
            packages="$PUBLISH_PACKAGES"
            ;;
        rust)
            packages="$CHECK_RUST_PACKAGES"
            ;;
        rust-dev)
            packages="$RUST_DEV_TOOLS_PACKAGES"
            ;;
        shell)
            packages="$CHECK_SHELL_PACKAGES"
            ;;
        spelling)
            packages="$SPELLING_PACKAGES"
            ;;
        test)
            packages="$TEST_PACKAGES"
            ;;
        unused)
            packages="$CHECK_UNUSED_PACKAGES"
            ;;
        *)
            printf 'Invalid package set %s\n' "$set" >&2
            exit 1
    esac

    just gitlab-section-start "install-alpm-packages" "Install ALPM package set: $set"

    just ensure-command pacman run0

    # Read all packages into an array.
    read -r -d '' -a packages < <(printf '%s\0' "$packages")

    # Deduplicate using an associative array
    declare -A unique_packages
    for package in "${packages[@]}"; do
        if [[ ! "${unique_packages[$package]+_}" ]]; then
            unique_packages["$package"]=1
        fi
    done

    # Use run0 when not root
    command=()
    if (( "$(id -u)" > 0 )); then
        command+=(run0)
    fi
    command+=(
        pacman -Su --needed --noconfirm
    )

    "${command[@]}" "${!unique_packages[@]}"

    just gitlab-section-end "install-alpm-packages"

# Checks if a string matches a workspace member exactly
[private]
is-workspace-member package:
    #!/usr/bin/env bash
    set -euo pipefail

    mapfile -t workspace_members < <(just get-workspace-members 2>/dev/null)

    for name in "${workspace_members[@]}"; do
        if [[ "$name" == {{ package }} ]]; then
            exit 0
        fi
    done
    exit 1

# Runs checks and tests before creating a commit.
[private]
run-pre-commit-hook: check docs test test-docs

# Runs checks before pushing commits to remote repository.
[private]
run-pre-push-hook: check-commits

# Build local documentation
[group('build')]
docs:
    just gitlab-section-start "docs" "Build Rust API documentation"

    RUSTDOCFLAGS='-D warnings' cargo doc --document-private-items --no-deps

    just gitlab-section-end "docs"

# Runs all check targets
[group('check')]
check: check-spelling check-formatting check-shell-code check-rust-code check-unused-deps check-dependencies check-licenses check-links

# Checks source code formatting
[group('check')]
check-formatting:
    just gitlab-section-start "check-formatting" "Check source code formatting"

    just ensure-command cargo-sort-derives taplo

    just --unstable --fmt --check
    # We're using nightly to properly group imports, see rustfmt.toml
    cargo +nightly fmt -- --check

    taplo format --check

    # Checks for consistent sorting of rust derives
    cargo sort-derives --check

    just gitlab-section-end "check-formatting"

# Checks commit messages for correctness
[group('check')]
check-commits:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly default_branch="${CI_DEFAULT_BRANCH:-main}"

    just gitlab-section-start "check-commits" "Check commits"

    just ensure-command codespell cog committed rg

    if ! git rev-parse --verify "origin/$default_branch" > /dev/null 2>&1; then
        printf "The default branch '%s' does not exist!\n" "$default_branch" >&2
        exit 1
    fi

    tmpdir="$(mktemp --dry-run --directory)"
    readonly check_tmpdir="$tmpdir"
    mkdir -p "$check_tmpdir"

    # remove temporary dir on exit
    cleanup() (
      if [[ -n "${check_tmpdir:-}" ]]; then
        rm -rf "${check_tmpdir}"
      fi
    )

    trap cleanup EXIT

    for commit in $(git rev-list "origin/${default_branch}.."); do
        printf "Checking commit %s\n" "$commit"

        commit_message="$(git show -s --format=%B "$commit")"
        codespell_config="$(mktemp --tmpdir="$check_tmpdir")"

        # either use the commit's .codespellrc or create one
        if git show "$commit:.codespellrc" > /dev/null 2>&1; then
            git show "$commit:.codespellrc" > "$codespell_config"
        else
            printf "[codespell]\nskip = .cargo,.git,target,.env,Cargo.lock\nignore-words-list = crate\n" > "$codespell_config"
        fi

        if ! rg -q "Signed-off-by: " <<< "$commit_message"; then
            printf "Commit %s ❌️\n" "$commit" >&2
            printf "The commit message lacks a \"Signed-off-by\" line.\n" >&2
            printf "%s\n" \
                "  Please use:" \
                "    git rebase --signoff main && git push --force-with-lease" \
                "  See https://developercertificate.org/ for more details." >&2
            exit 1
        elif ! codespell --config "$codespell_config" - <<< "$commit_message"; then
            printf "Commit %s ❌️\n" "$commit" >&2
            printf "The spelling of the commit message needs improvement.\n" >&2
            exit 1
        elif ! cog verify "$commit_message"; then
            printf "Commit %s ❌️\n" "$commit" >&2
            printf "%s\n" \
                "The commit message is not a conventional commit message:" \
                "$commit_message" \
                "See https://www.conventionalcommits.org/en/v1.0.0/ for more details." >&2
            exit 1
        elif ! committed "$commit"; then
            printf "Commit %s ❌️\n" "$commit" >&2
            printf "%s\n" \
                "The commit message does not meet the required standards:" \
                "$commit_message"
            exit 1
        else
            printf "Commit %s ✅️\n\n" "$commit"
        fi
    done

    just gitlab-section-end "check-commits"

# Checks for issues with dependencies
[group('check')]
check-dependencies: dry-update
    just gitlab-section-start "check-dependencies" "Check Rust dependencies"

    cargo deny --all-features check

    just gitlab-section-end "check-licenses"

# Checks licensing status
[group('check')]
check-licenses:
    just gitlab-section-start "check-licenses" "Check licenses"

    just ensure-command reuse
    reuse lint

    just gitlab-section-end "check-licenses"

# Check for stale links in documentation
[group('check')]
check-links:
    just gitlab-section-start "check-links" "Check links in all files"

    just ensure-command lychee
    lychee .

    just gitlab-section-end "check-links"

# Checks the Rust source code using cargo-clippy.
[group('check')]
check-rust-code:
    just gitlab-section-start "check-rust-code" "Check Rust code"

    just ensure-command cargo cargo-clippy mold
    cargo clippy --all-features --all-targets --workspace -- -D warnings

    just gitlab-section-end "check-rust-code"

# Checks shell code using shellcheck.
[group('check')]
check-shell-code:
    just gitlab-section-start "check-shell-code" "Check shell code"

    just check-shell-recipe check-commits
    just check-shell-recipe check-unused-deps
    just check-shell-recipe ci-publish
    just check-shell-recipe containerized-integration-tests
    just check-shell-recipe create-coverage-report
    just check-shell-recipe 'get-workspace-member-version foo'
    just check-shell-recipe 'ensure-command test'
    just check-shell-recipe 'gitlab-section-end foo'
    just check-shell-recipe 'gitlab-section-start foo bar'
    just check-shell-recipe 'install-alpm-package-set all'
    just check-shell-recipe 'is-workspace-member test'
    just check-shell-recipe 'prepare-release test'
    just check-shell-recipe 'release test'
    just check-shell-recipe test
    just check-shell-recipe test-docs

    just check-shell-script .cargo/runner.sh

    just gitlab-section-end "check-shell-code"

# Checks justfile recipe relying on shell semantics using shellcheck.
[group('check')]
check-shell-recipe recipe:
    just ensure-command rg shellcheck
    just -vv -n {{ recipe }} 2>&1 | rg -v '===> Running recipe' | shellcheck -

# Checks a shell script using shellcheck.
[group('check')]
check-shell-script file:
    just ensure-command shellcheck
    shellcheck --shell bash {{ file }}

# Checks common spelling mistakes
[group('check')]
check-spelling:
    just gitlab-section-start "check-spelling" "Check spelling in all files"

    codespell

    just gitlab-section-end "check-spelling"

# Checks for unused dependencies
[group('check')]
check-unused-deps:
    #!/usr/bin/env bash
    set -euo pipefail

    just gitlab-section-start "check-unused-deps" "Check for unused Rust dependencies"

    just ensure-command cargo-machete

    cargo machete

    just gitlab-section-end "check-unused-deps"

# Adds needed git configuration for the local repository
[group('dev')]
configure-git:
    # Enforce gpg signed keys for this repository
    git config commit.gpgsign true

    just add-hooks

# Installs all tools required for development\
[group('dev')]
dev-install: install-pacman-dev-packages install-rust-dev-tools

# Fixes common issues. Files need to be git add'ed
[group('dev')]
fix:
    #!/usr/bin/env bash
    set -euo pipefail

    if ! git diff-files --quiet ; then
        printf "Working tree has changes. Please stage them: git add .\n"
        exit 1
    fi

    codespell --write-changes
    just --unstable --fmt
    cargo clippy --fix --allow-staged
    # fmt must be last as clippy's changes may break formatting
    cargo +nightly fmt

    taplo format

# Installs development packages using pacman
[group('dev')]
install-pacman-dev-packages:
    just install-alpm-package-set all

# Installs all Rust tools required for development
[group('dev')]
install-rust-dev-tools:
    just gitlab-section-start "install-rust-dev-tools" "Install Rust development tools"

    just ensure-command rustup

    rustup default stable
    rustup component add clippy llvm-tools-preview
    # Install nightly as we use it for formatting rules.
    rustup toolchain install nightly
    rustup component add --toolchain nightly rustfmt llvm-tools-preview

    just gitlab-section-end "install-rust-dev-tools"

# Runs integration tests guarded by the `_containerized-integration-test` feature, located in modules named `containerized` (accepts `cargo nextest run` options via `options`).
[group('test')]
containerized-integration-tests *options:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly coverage="{{ coverage }}"
    commands=(
        bash
        cargo
        cargo-nextest
        jq
        podman
    )
    read -r -a options <<< "{{ options }}"

    just gitlab-section-start "containerized-integration-tests" "Run containerized integration tests"

    if [[ "$coverage" == "true" ]]; then
        commands+=(cargo-llvm-cov)
        just ensure-command "${commands[@]}"
        # Use the environment prepared by `cargo llvm-cov show-env`
        # shellcheck source=/dev/null
        source <(cargo llvm-cov show-env --export-prefix)
    else
        just ensure-command "${commands[@]}"
    fi

    cargo build --examples --bins
    cargo nextest run --features _containerized-integration-test --filterset 'kind(test) and binary_id(/::containerized$/)' "${options[@]}"

    just gitlab-section-end "containerized-integration-tests"

[doc('Creates code coverage report for all projects from all available sources.
When providing `with-docs` to the `mode` parameter, this also includes doc test coverage in the report (requires nightly).
The `metrics_name` parameter can be used to override the metrics name in the `coverage-metrics.txt` file used by GitLab.')]
[group('test')]
create-coverage-report mode="without-docs" metrics_name="Test-coverage":
    #!/usr/bin/env bash
    set -euo pipefail

    readonly metrics_name="{{ metrics_name }}"
    readonly mode="{{ mode }}"

    target_dir="$(just get-cargo-target-directory)"

    just gitlab-section-start "create-coverage-report" "Create coverage report"

    just ensure-command cargo-llvm-cov cargo-nextest jq

    mkdir --parents "$target_dir/llvm-cov/"

    # Options for cargo
    cargo_options=(+stable)

    # The chosen reporting style (defaults to without doctest coverage)
    reporting_style="without doctest coverage"

    # Options for creating cobertura coverage report with cargo-llvm-cov
    cargo_llvm_cov_cobertura_options=(
        --cobertura
        --output-path "$target_dir/llvm-cov/cobertura-coverage.xml"
    )

    # Options for creating coverage report summary with cargo-llvm-cov
    cargo_llvm_cov_summary_options=(
        --json
        --summary-only
    )

    if [[ "$mode" == "with-docs" ]]; then
        reporting_style="with doctest coverage"
        # The support for doctest coverage is a nightly feature
        cargo_options=(+nightly)
        cargo_llvm_cov_cobertura_options+=(--doctests)
        cargo_llvm_cov_summary_options+=(--doctests)
    fi

    printf "Creating report %s\n" "$reporting_style"

    # Use the environment prepared by `cargo llvm-cov show-env`
    # shellcheck source=/dev/null
    source <(cargo llvm-cov show-env --export-prefix)

    # Create cobertura coverage report
    cargo "${cargo_options[@]}" llvm-cov report "${cargo_llvm_cov_cobertura_options[@]}"

    printf "Calculating percentage...\n"

    # Get total coverage percentage from summary
    percentage="$(cargo "${cargo_options[@]}" llvm-cov report "${cargo_llvm_cov_summary_options[@]}" | jq '.data[0].totals.lines.percent')"

    # Trim percentage to 4 decimal places.
    percentage="$(LC_NUMERIC=C printf "%.4f\n" "$percentage")"

    # Writes to target/coverage-metrics.txt for Gitlab CI metric consumption.
    # https://docs.gitlab.com/ci/testing/metrics_reports/
    printf "%s %s\n" "$metrics_name" "$percentage" > "$target_dir/llvm-cov/coverage-metrics.txt"
    printf "Test-coverage: %s%%\n" "$percentage"

    just gitlab-section-end "create-coverage-report"

# Runs all unit tests (accepts `cargo nextest run` options via `options`).
[group('test')]
test *options:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly coverage="{{ coverage }}"
    commands=(
        cargo
        cargo-nextest
        mold
    )
    read -r -a options <<< "{{ options }}"

    just gitlab-section-start "test" "Run unit tests"

    if [[ "$coverage" == "true" ]]; then
        commands+=(cargo-llvm-cov)
        just ensure-command "${commands[@]}"
        # Use the environment prepared by `cargo llvm-cov show-env`
        # shellcheck source=/dev/null
        source <(cargo llvm-cov show-env --export-prefix)
    else
        just ensure-command "${commands[@]}"
    fi

    cargo nextest run --locked --all "${options[@]}"

    just gitlab-section-end "test"

# Runs all doc tests
[group('test')]
test-docs *options:
    #!/usr/bin/env bash
    set -euo pipefail

    readonly coverage="{{ coverage }}"
    toolchain="+stable"
    commands=(cargo)
    read -r -a options <<< "{{ options }}"

    just gitlab-section-start "test-docs" "Run doc tests"

    if [[ "$coverage" == "true" ]]; then
        commands+=(cargo-llvm-cov)
        toolchain="+nightly"
        just ensure-command "${commands[@]}"
        # Use the environment prepared by `cargo llvm-cov show-env`
        # shellcheck source=/dev/null
        source <(cargo llvm-cov show-env --export-prefix)
    else
        just ensure-command "${commands[@]}"
    fi

    cargo "$toolchain" test --locked --doc "${options[@]}"
    just gitlab-section-end "test-docs"

# Publishes a crate in the workspace from GitLab CI in a pipeline for tags
[group('release')]
ci-publish:
    #!/usr/bin/env bash
    set -euo pipefail

    # an auth token with publishing capabilities is expected to be set in GitLab project settings
    readonly token="${CARGO_REGISTRY_TOKEN:-}"
    # rely on predefined variable to retrieve git tag: https://docs.gitlab.com/ee/ci/variables/predefined_variables.html
    readonly tag="${CI_COMMIT_TAG:-}"
    readonly crate="${tag//\/*/}"
    readonly version="${tag#*/}"

    just gitlab-section-start "ci-publish" "Publish release on crates.io"

    just ensure-command cargo mold

    if [[ -z "$tag" ]]; then
        printf "There is no tag!\n" >&2
        exit 1
    fi
    if [[ -z "$token" ]]; then
        printf "There is no token for crates.io!\n" >&2
        exit 1
    fi
    if ! just is-workspace-member "$crate" &>/dev/null; then
        printf "The crate %s is not a workspace member of the project!\n" "$crate" >&2
        exit 1
    fi

    current_member_version="$(just get-workspace-member-version "$crate" 2>/dev/null)"
    readonly current_member_version="$current_member_version"
    if [[ "$version" != "$current_member_version" ]]; then
        printf "Current version in metadata of crate %s (%s) does not match the version from the tag (%s)!\n" "$crate" "$current_member_version" "$version"
        exit 1
    fi

    printf "Found tag %s (crate %s in version %s).\n" "$tag" "$crate" "$version"
    cargo publish -p "$crate"

    just gitlab-section-end "ci-publish"

# Prepares the release of a crate by updating dependencies, incrementing the crate version and creating a changelog entry (optionally, the version can be set explicitly)
[group('release')]
prepare-release package version="":
    #!/usr/bin/env bash
    set -euo pipefail

    readonly package_name="{{ package }}"
    if [[ -z "$package_name" ]]; then
        printf "No package name provided!\n"
        exit 1
    fi
    readonly package_version="{{ version }}"
    branch_name=""

    just ensure-command git release-plz

    release-plz update -u -p "$package_name"

    # NOTE: When setting the version specifically, we are likely in a situation where `release-plz` did not detect a version change (e.g. when only changes to top-level files took place since last release).
    # In this case we are fine to potentially have no changes in the CHANGELOG.md or having to adjust it manually afterwards.
    if [[ -n "$package_version" ]]; then
        release-plz set-version "${package_name}@${package_version}"
    fi

    # make sure that the current version would be publishable, but ignore files not added to git
    cargo publish -p "$package_name" --dry-run --allow-dirty

    updated_package_version="$(just get-workspace-member-version "$package_name")"
    readonly updated_package_version="$updated_package_version"

    if [[ -n "$package_version" ]]; then
        branch_name="release/$package_name/$package_version"
    else
        branch_name="release/$package_name/$updated_package_version"
    fi
    git checkout -b "$branch_name"

    shopt -s globstar
    git add ./**/Cargo.* ./**/CHANGELOG.md
    git commit --gpg-sign --signoff --message "chore: Upgrade $package_name crate to $updated_package_version"
    git push --set-upstream origin "$branch_name"

# Creates a release of a crate in the workspace by creating a tag and pushing it
[group('release')]
release package:
    #!/usr/bin/env bash
    set -euo pipefail

    package_version="$(just get-workspace-member-version {{ package }})"
    readonly package_version="$package_version"
    if [[ -z "$package_version" ]]; then
        exit 1
    fi
    readonly current_version="{{ package }}/$package_version"

    just ensure-command git

    if [[ -n "$(git tag -l "$current_version")" ]]; then
        printf "The tag %s exists already!\n" "$current_version" >&2
        exit 1
    fi

    printf "Creating tag %s...\n" "$current_version"
    git tag -s "$current_version" -m "$current_version"
    printf "Pushing tag %s...\n" "$current_version"
    git push origin refs/tags/"$current_version"