feat: add dry-run flag via CommandExecutor interface (#26422)

## Summary

Adds a `--dry-run` capability to the `release-action` Go tool and
exposes it through a **new** manual workflow, `tag-and-release.yaml`,
without disturbing the existing `release.yaml` pipeline.

PR #25162 had rewritten `release.yaml` in place to be driven by
`scripts/release-action`, which changed its `workflow_dispatch` inputs
from `release_channel`/`release_notes`/`dry_run` to
`release_type`/`commit_sha`. That broke `scripts/releaser`, which
dispatches `release.yaml` with the original inputs. This PR restores
`release.yaml` and moves the Go-driven pipeline to its own workflow.

## Workflow layout after this PR

| Workflow | Trigger | Driven by | Purpose |
|---|---|---|---|
| `release.yaml` | `scripts/releaser` (`gh workflow run`) | legacy
inline shell | Existing pipeline, restored to pre-#25162 state |
| `tag-and-release.yaml` | Manual (Actions UI) |
`scripts/release-action` Go tool | New pipeline with `prepare-release` +
`dry_run` |

`release.yaml` is restored byte-for-byte to its pre-#25162 version, so
its inputs match what `scripts/releaser` sends again.

## `release-action` design

### CommandExecutor interface

Abstracts CLI command execution behind read-only and mutating methods:

| Method | Purpose | Dry-run behavior |
|---|---|---|
| `RunOutput` | Read-only, capture stdout | Executes normally |
| `Run` | Read-only, exit code only | Executes normally |
| `RunMutation` | Changes remote state, no output | **Prints command,
skips execution** |
| `RunMutationStdout` | Changes remote state, streaming I/O | **Prints
command, skips execution** |

Two implementations: `realExecutor` (executes via `os/exec`) and
`dryRunExecutor` (delegates read-only calls, prints mutating calls).

### `prepare-release` subcommand

Composes `calculateNextVersion` with idempotent tag and branch
creation+push, emitting the same JSON as `calculate-version`. Matching
existing refs are skipped; mismatched refs error.

### `tag-and-release.yaml` `dry_run` input

When enabled: `prepare-release` runs with `--dry-run` (version
calculated, plan printed, nothing pushed), notes are generated for
inspection, and the build+publish job is skipped via an `if` guard
(cascading to homebrew/winget/docs).

## Mutating commands covered by `--dry-run`

| Command | Call site |
|---|---|
| `git tag -a <version> ...` | `createAndPushTag` |
| `git push origin refs/tags/...` | `createAndPushTag` |
| `git push origin <sha>:refs/heads/...` | `createAndPushBranch` |
| `gh release create ...` | `publishRelease` |

`git fetch --tags --force origin` is intentionally not a mutation; it
only updates local remote-tracking refs and must run for accurate
version calculation.

## Changes

- **New**: `scripts/release-action/cmdexec.go`, `prepare.go` (+ tests)
- **Refactored**: `git.go`, `github.go`, `calculate.go`, `notes.go`,
`commit.go`, `publish.go` to thread `CommandExecutor`; added `gitMutate`
- **Updated**: `main.go` adds `--dry-run` flag and `prepare-release`
subcommand
- **New**: `.github/workflows/tag-and-release.yaml` (manual, Go-driven,
with `dry_run`)
- **Reverted**: `.github/workflows/release.yaml` to its pre-#25162 state

> [!NOTE]
> Generated by Coder Agents on behalf of @f0ssel
This commit is contained in:
Garrett Delfosse
2026-07-01 16:20:00 -04:00
committed by GitHub
parent 4936ff9808
commit ff7e0bc193
14 changed files with 1763 additions and 313 deletions
+144 -230
View File
@@ -3,24 +3,35 @@ name: Release
on:
workflow_dispatch:
inputs:
release_type:
release_channel:
type: choice
description: "Type of release (use 'Use workflow from' to pick the branch)"
required: true
description: Release channel
options:
- mainline
- stable
- rc
- release
- create-release-branch
commit_sha:
description: "Optional: commit SHA to tag (defaults to HEAD of selected branch)"
type: string
default: ""
release_notes:
description: Release notes for the publishing the release. This is required to create a release.
dry_run:
description: Perform a dry-run release (devel). Note that ref must be an annotated tag when run without dry-run.
type: boolean
required: true
default: false
permissions:
contents: read
concurrency: ${{ github.workflow }}-${{ github.ref }}
env:
# Use `inputs` (vs `github.event.inputs`) to ensure that booleans are actual
# booleans, not strings.
# https://github.blog/changelog/2022-06-10-github-actions-inputs-unified-across-manual-and-reusable-workflows/
CODER_RELEASE: ${{ !inputs.dry_run }}
CODER_DRY_RUN: ${{ inputs.dry_run }}
CODER_RELEASE_CHANNEL: ${{ inputs.release_channel }}
CODER_RELEASE_NOTES: ${{ inputs.release_notes }}
jobs:
# Only allow maintainers/admins to release.
check-perms:
@@ -48,141 +59,9 @@ jobs:
if (!allowed) core.setFailed('Denied: requires maintain or admin');
prepare-release:
name: Prepare release
needs: [check-perms]
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
contents: write
outputs:
version: ${{ steps.prepare.outputs.version }}
previous_version: ${{ steps.prepare.outputs.previous_version }}
stable: ${{ steps.prepare.outputs.stable }}
target_ref: ${{ steps.prepare.outputs.target_ref }}
create_branch: ${{ steps.prepare.outputs.create_branch }}
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: true
- name: Fetch git tags
run: git fetch --tags --force
- name: Setup Go
uses: ./.github/actions/setup-go
with:
use-cache: false
- name: Calculate version and create tag
id: prepare
env:
RELEASE_TYPE: ${{ inputs.release_type }}
REF_NAME: ${{ github.ref_name }}
COMMIT_SHA: ${{ inputs.commit_sha }}
run: |
set -euo pipefail
args=(--type "$RELEASE_TYPE" --ref "$REF_NAME")
if [[ -n "$COMMIT_SHA" ]]; then
args+=(--commit "$COMMIT_SHA")
fi
output=$(go run ./scripts/release-action calculate-version "${args[@]}")
echo "Raw output: $output"
version=$(echo "$output" | jq -r '.version')
previous_version=$(echo "$output" | jq -r '.previous_version')
stable=$(echo "$output" | jq -r '.stable')
target_ref=$(echo "$output" | jq -r '.target_ref')
create_branch=$(echo "$output" | jq -r '.create_branch // empty')
# Validate required outputs are non-empty.
for var in version previous_version target_ref; do
eval "val=\$$var"
if [[ -z "$val" || "$val" == "null" ]]; then
echo "::error::calculate-version returned empty or null '$var'"
exit 1
fi
done
{
echo "version=$version"
echo "previous_version=$previous_version"
echo "stable=$stable"
echo "target_ref=$target_ref"
echo "create_branch=$create_branch"
} >> "$GITHUB_OUTPUT"
{
echo "### Release preparation"
echo "| Field | Value |"
echo "|-------|-------|"
echo "| Version | \`$version\` |"
echo "| Previous | \`$previous_version\` |"
echo "| Stable | \`$stable\` |"
echo "| Target ref | \`$target_ref\` |"
if [[ -n "$create_branch" ]]; then
echo "| Create branch | \`$create_branch\` |"
fi
} >> "$GITHUB_STEP_SUMMARY"
- name: Create and push tag
env:
VERSION: ${{ steps.prepare.outputs.version }}
TARGET_REF: ${{ steps.prepare.outputs.target_ref }}
run: |
set -euo pipefail
# Skip if tag already exists (idempotent)
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists, skipping."
exit 0
fi
git tag -a "$VERSION" -m "Release $VERSION" "$TARGET_REF"
git push origin "$VERSION"
- name: Create release branch
if: ${{ steps.prepare.outputs.create_branch != '' }}
env:
CREATE_BRANCH: ${{ steps.prepare.outputs.create_branch }}
TARGET_REF: ${{ steps.prepare.outputs.target_ref }}
run: |
set -euo pipefail
# Skip if branch already exists
if git ls-remote --exit-code origin "refs/heads/$CREATE_BRANCH" >/dev/null 2>&1; then
echo "Branch $CREATE_BRANCH already exists, skipping."
exit 0
fi
git branch "$CREATE_BRANCH" "$TARGET_REF"
git push origin "$CREATE_BRANCH"
- name: Generate release notes
env:
VERSION: ${{ steps.prepare.outputs.version }}
PREV_VERSION: ${{ steps.prepare.outputs.previous_version }}
run: |
set -euo pipefail
go run ./scripts/release-action generate-notes \
--version "$VERSION" \
--previous-version "$PREV_VERSION" > /tmp/release_notes.md
- name: Upload release notes
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-notes
path: /tmp/release_notes.md
retention-days: 30
release:
name: Build and publish
needs: [check-perms, prepare-release]
needs: [check-perms]
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
# Required to publish a release
@@ -196,8 +75,6 @@ jobs:
# Required for GitHub Actions attestation
attestations: write
env:
CODER_RELEASE: "true"
CODER_RELEASE_STABLE: ${{ needs.prepare-release.outputs.stable }}
# Necessary for Docker manifest
DOCKER_CLI_EXPERIMENTAL: "enabled"
outputs:
@@ -209,7 +86,7 @@ jobs:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
@@ -222,36 +99,66 @@ jobs:
- name: Fetch git tags
run: git fetch --tags --force
- name: Checkout release commit
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
run: |
set -euo pipefail
git checkout "refs/tags/$VERSION"
- name: Print version
id: version
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
run: |
set -euo pipefail
# VERSION comes from the env block, not a misspelling of the local 'version'.
# shellcheck disable=SC2153
# Strip the "v" prefix for use in build steps.
version="${VERSION#v}"
version="$(./scripts/version.sh)"
echo "version=$version" >> "$GITHUB_OUTPUT"
# Speed up future version.sh calls.
echo "CODER_FORCE_VERSION=$version" >> "$GITHUB_ENV"
echo "$version"
- name: Download release notes
uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1
with:
name: release-notes
path: /tmp
# Verify that all expectations for a release are met.
- name: Verify release input
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
- name: Set release notes env
run: echo CODER_RELEASE_NOTES_FILE=/tmp/release_notes.md >> "$GITHUB_ENV"
if [[ "${GITHUB_REF}" != "refs/tags/v"* ]]; then
echo "Ref must be a semver tag when creating a release, did you use scripts/release.sh?"
exit 1
fi
# Derive the release branch from the version tag.
# Non-RC releases must be on a release/X.Y branch.
# RC tags are allowed on any branch (typically main).
version="$(./scripts/version.sh)"
# Strip any pre-release suffix first (e.g. 2.32.0-rc.0 -> 2.32.0)
base_version="${version%%-*}"
# Then strip patch to get major.minor (e.g. 2.32.0 -> 2.32)
release_branch="release/${base_version%.*}"
if [[ "$version" == *-rc.* ]]; then
echo "RC release detected, skipping release branch check (RC tags are cut from main)."
else
branch_contains_tag=$(git branch --remotes --contains "${GITHUB_REF}" --list "*/${release_branch}" --format='%(refname)')
if [[ -z "${branch_contains_tag}" ]]; then
echo "Ref tag must exist in a branch named ${release_branch} when creating a non-RC release, did you use scripts/release.sh?"
exit 1
fi
fi
if [[ -z "${CODER_RELEASE_NOTES}" ]]; then
echo "Release notes are required to create a release, did you use scripts/release.sh?"
exit 1
fi
echo "Release inputs verified:"
echo
echo "- Ref: ${GITHUB_REF}"
echo "- Version: ${version}"
echo "- Release channel: ${CODER_RELEASE_CHANNEL}"
echo "- Release branch: ${release_branch}"
echo "- Release notes: true"
- name: Create release notes file
run: |
set -euo pipefail
release_notes_file="$(mktemp -t release_notes.XXXXXX)"
echo "$CODER_RELEASE_NOTES" > "$release_notes_file"
echo CODER_RELEASE_NOTES_FILE="$release_notes_file" >> "$GITHUB_ENV"
- name: Show release notes
run: |
@@ -376,8 +283,12 @@ jobs:
id: image-base-tag
run: |
set -euo pipefail
# Empty value means use the default and avoid building a fresh one.
echo "tag=$(CODER_IMAGE_BASE=ghcr.io/coder/coder-base ./scripts/image_tag.sh)" >> "$GITHUB_OUTPUT"
if [[ "${CODER_RELEASE:-}" != *t* ]] || [[ "${CODER_DRY_RUN:-}" == *t* ]]; then
# Empty value means use the default and avoid building a fresh one.
echo "tag=" >> "$GITHUB_OUTPUT"
else
echo "tag=$(CODER_IMAGE_BASE=ghcr.io/coder/coder-base ./scripts/image_tag.sh)" >> "$GITHUB_OUTPUT"
fi
- name: Create empty base-build-context directory
if: steps.image-base-tag.outputs.tag != ''
@@ -439,7 +350,7 @@ jobs:
- name: GitHub Attestation for Base Docker image
id: attest_base
if: ${{ steps.build_base_image.outputs.digest != '' }}
if: ${{ !inputs.dry_run && steps.build_base_image.outputs.digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
@@ -452,6 +363,13 @@ jobs:
run: |
set -euxo pipefail
# we can't build multi-arch if the images aren't pushed, so quit now
# if dry-running
if [[ "$CODER_RELEASE" != *t* ]]; then
echo Skipping multi-arch docker builds due to dry-run.
exit 0
fi
# build Docker images for each architecture
version="$(./scripts/version.sh)"
make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag
@@ -485,6 +403,7 @@ jobs:
CODER_BASE_IMAGE_TAG: ${{ steps.image-base-tag.outputs.tag }}
- name: SBOM Generation and Attestation
if: ${{ !inputs.dry_run }}
env:
COSIGN_EXPERIMENTAL: '1'
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
@@ -520,6 +439,7 @@ jobs:
- name: Resolve Docker image digests for attestation
id: docker_digests
if: ${{ !inputs.dry_run }}
continue-on-error: true
env:
MULTIARCH_IMAGE: ${{ steps.build_docker.outputs.multiarch_image }}
@@ -537,7 +457,7 @@ jobs:
- name: GitHub Attestation for Docker image
id: attest_main
if: ${{ steps.docker_digests.outputs.multiarch_digest != '' }}
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.multiarch_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
@@ -547,7 +467,7 @@ jobs:
- name: GitHub Attestation for "latest" Docker image
id: attest_latest
if: ${{ steps.docker_digests.outputs.latest_digest != '' }}
if: ${{ !inputs.dry_run && steps.docker_digests.outputs.latest_digest != '' }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
@@ -557,6 +477,7 @@ jobs:
- name: GitHub Attestation for release binaries
id: attest_binaries
if: ${{ !inputs.dry_run }}
continue-on-error: true
uses: actions/attest@59d89421af93a897026c735860bf21b6eb4f7b26 # v4.1.0
with:
@@ -572,6 +493,7 @@ jobs:
# Report attestation failures but don't fail the workflow
- name: Check attestation status
if: ${{ !inputs.dry_run }}
run: | # zizmor: ignore[template-injection] We're just reading steps.attest_x.outcome here, no risk of injection
if [[ "${{ steps.attest_base.outcome }}" == "failure" && "${{ steps.attest_base.conclusion }}" != "skipped" ]]; then
echo "::warning::GitHub attestation for base image failed"
@@ -595,6 +517,7 @@ jobs:
run: ls -lh build
- name: Publish Coder CLI binaries and detached signatures to GCS
if: ${{ !inputs.dry_run }}
run: |
set -euxo pipefail
@@ -621,7 +544,19 @@ jobs:
run: |
set -euo pipefail
# Build the list of files to publish.
publish_args=()
if [[ $CODER_RELEASE_CHANNEL == "stable" ]]; then
publish_args+=(--stable)
fi
if [[ $CODER_RELEASE_CHANNEL == "rc" ]]; then
publish_args+=(--rc)
fi
if [[ $CODER_DRY_RUN == *t* ]]; then
publish_args+=(--dry-run)
fi
declare -p publish_args
# Build the list of files to publish
files=(
./build/*_installer.exe
./build/*.zip
@@ -633,28 +568,24 @@ jobs:
"./coder_${VERSION}_sbom.spdx.json"
)
# Only include the latest SBOM file if it was created.
# Only include the latest SBOM file if it was created
if [[ "${CREATED_LATEST_TAG}" == "true" ]]; then
files+=(./coder_latest_sbom.spdx.json)
fi
stable_flag=()
if [[ "$CODER_RELEASE_STABLE" == "true" ]]; then
stable_flag=(--stable)
fi
go run ./scripts/release-action publish \
--version "v${VERSION}" \
"${stable_flag[@]}" \
./scripts/release/publish.sh \
"${publish_args[@]}" \
--release-notes-file "$CODER_RELEASE_NOTES_FILE" \
"${files[@]}"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CODER_GPG_RELEASE_KEY_BASE64: ${{ secrets.GPG_RELEASE_KEY_BASE64 }}
VERSION: ${{ steps.version.outputs.version }}
CREATED_LATEST_TAG: ${{ steps.build_docker.outputs.created_latest_tag }}
# Mark the Linear release as shipped.
- name: Extract Linear release version
if: ${{ !inputs.dry_run }}
id: linear_version
run: |
# Skip RC releases — they must not complete the Linear release.
@@ -672,7 +603,7 @@ jobs:
VERSION: ${{ steps.version.outputs.version }}
- name: Complete Linear release
if: ${{ steps.linear_version.outputs.skip != 'true' }}
if: ${{ !inputs.dry_run && steps.linear_version.outputs.skip != 'true' }}
continue-on-error: true
uses: linear/linear-release-action@0353b5fa8c00326913966f00557d68f8f30b8b6b # v0.7.0
with:
@@ -691,6 +622,7 @@ jobs:
uses: google-github-actions/setup-gcloud@aa5489c8933f4cc7a4f7d45035b3b1440c9c10db # 3.0.1
- name: Publish Helm Chart
if: ${{ !inputs.dry_run }}
run: |
set -euo pipefail
version="$(./scripts/version.sh)"
@@ -706,20 +638,44 @@ jobs:
helm push "build/coder_helm_${version}.tgz" oci://ghcr.io/coder/chart
helm push "build/provisioner_helm_${version}.tgz" oci://ghcr.io/coder/chart
- name: Upload artifacts to actions (if dry-run)
if: ${{ inputs.dry_run }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-artifacts
path: |
./build/*_installer.exe
./build/*.zip
./build/*.tar.gz
./build/*.tgz
./build/*.apk
./build/*.deb
./build/*.rpm
./coder_${{ steps.version.outputs.version }}_sbom.spdx.json
retention-days: 7
- name: Upload latest sbom artifact to actions (if dry-run)
if: inputs.dry_run && steps.build_docker.outputs.created_latest_tag == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: latest-sbom-artifact
path: ./coder_latest_sbom.spdx.json
retention-days: 7
- name: Send repository-dispatch event
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.CDRCI_GITHUB_TOKEN }}
repository: coder/packages
event-type: coder-release
client-payload: '{"coder_version": "${{ steps.version.outputs.version }}"}'
client-payload: '{"coder_version": "${{ steps.version.outputs.version }}", "release_channel": "${{ inputs.release_channel }}"}'
publish-homebrew:
name: Publish to Homebrew tap
runs-on: ubuntu-latest
needs: [release, prepare-release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' && needs.prepare-release.outputs.stable == 'true' }}
needs: release
if: ${{ !inputs.dry_run && inputs.release_channel == 'mainline' }}
steps:
- name: Harden Runner
@@ -791,12 +747,11 @@ jobs:
-a "${GITHUB_ACTOR}" \
-b "This automatic PR was triggered by the release of Coder v$coder_version"
publish-winget:
name: Publish to winget-pkgs
runs-on: windows-latest
needs: [release, prepare-release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
needs: release
if: ${{ !inputs.dry_run && inputs.release_channel != 'rc' }}
steps:
- name: Harden Runner
@@ -810,7 +765,7 @@ jobs:
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
fetch-depth: 0
persist-credentials: false
@@ -884,44 +839,3 @@ jobs:
# different repo.
GH_TOKEN: ${{ secrets.CDRCI_GITHUB_TOKEN }}
VERSION: ${{ needs.release.outputs.version }}
update-docs:
name: Update release docs
needs: [prepare-release, release]
if: ${{ inputs.release_type != 'rc' && inputs.release_type != 'create-release-branch' }}
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
permissions:
contents: write
pull-requests: write
steps:
- name: Harden Runner
uses: step-security/harden-runner@f808768d1510423e83855289c910610ca9b43176 # v2.17.0
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
ref: main
fetch-depth: 0
persist-credentials: true
- name: Fetch git tags
run: git fetch --tags --force
- name: Setup Node
uses: ./.github/actions/setup-node
- name: Update release calendar
run: ./scripts/update-release-calendar.sh
- name: Create docs update PR
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e # v7.0.8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "docs: update release docs for ${{ needs.prepare-release.outputs.version }}"
title: "docs: update release docs for ${{ needs.prepare-release.outputs.version }}"
body: "Automated docs update for release ${{ needs.prepare-release.outputs.version }}."
branch: docs/release-${{ needs.prepare-release.outputs.version }}
base: main