# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
name: "ReleaseKit"
description: "Release orchestration for uv workspaces — publish, plan, prepare, and release Python packages."
author: "Google"
branding:
icon: "package"
color: "blue"
inputs:
command:
description: |
The releasekit subcommand to run. One of:
- publish Publish all changed packages to PyPI.
- plan Preview the execution plan.
- prepare Bump versions, changelogs, open Release PR.
- release Tag a merged Release PR and create GitHub Release.
- check Run workspace health checks.
- discover List all workspace packages.
- version Show computed version bumps.
- rollback Delete a tag and its GitHub release.
required: true
default: "plan"
group:
description: |
Release group name. Only packages matching the named group's
patterns (from [tool.releasekit] groups) will be included.
Leave empty to include all packages.
required: false
default: ""
dry-run:
description: "Preview mode: log commands without executing."
required: false
default: "false"
force:
description: "Skip confirmation prompts and preflight checks."
required: false
default: "false"
forge-backend:
description: |
Which Forge backend to use for GitHub operations.
- cli Use the gh CLI (requires gh to be installed).
- api Use the GitHub REST API (uses GITHUB_TOKEN, no gh needed).
required: false
default: "api"
check-url:
description: "URL to check for existing files (uv publish --check-url)."
required: false
default: ""
index-url:
description: "Custom index URL (e.g., Test PyPI). Leave empty for production PyPI."
required: false
default: ""
concurrency:
description: "Max packages publishing simultaneously per dependency level."
required: false
default: "5"
max-retries:
description: "Retry failed publishes up to N times with exponential backoff."
required: false
default: "2"
python-version:
description: "Python version to install."
required: false
default: "3.12"
uv-version:
description: "uv version to install."
required: false
default: "latest"
working-directory:
description: |
Working directory for releasekit. Should contain or be within
a uv workspace (a directory with pyproject.toml containing
[tool.uv.workspace]).
required: false
default: "."
extra-args:
description: "Additional arguments passed directly to the releasekit command."
required: false
default: ""
outputs:
exit-code:
description: "The exit code from the releasekit command."
value: ${{ steps.run.outputs.exit-code }}
plan-json:
description: "JSON output from the plan command (only when command=plan)."
value: ${{ steps.plan-json.outputs.json }}
runs:
using: "composite"
steps:
# 1. Install Python.
- name: Set up Python ${{ inputs.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ inputs.python-version }}
# 2. Install uv.
- name: Install uv
uses: astral-sh/setup-uv@v4
with:
version: ${{ inputs.uv-version }}
# 3. Install releasekit in the workspace.
- name: Install workspace
shell: bash
working-directory: ${{ inputs.working-directory }}
run: uv sync --active
# 4. Build the command dynamically.
- name: Run releasekit
id: run
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
set +e
# Build command as an array to prevent injection from free-form inputs.
cmd_array=(uv run --active releasekit ${{ inputs.command }})
# Add --group if specified.
if [[ -n "${{ inputs.group }}" ]]; then
cmd_array+=(--group "${{ inputs.group }}")
fi
# Add --dry-run if enabled.
if [[ "${{ inputs.dry-run }}" == "true" ]]; then
cmd_array+=(--dry-run)
fi
# Add --force if enabled.
if [[ "${{ inputs.force }}" == "true" ]]; then
cmd_array+=(--force)
fi
# Add --forge-backend for commands that support it.
case "${{ inputs.command }}" in
publish|prepare)
cmd_array+=(--forge-backend "${{ inputs.forge-backend }}")
;;
esac
# Add publish-specific flags.
if [[ "${{ inputs.command }}" == "publish" ]]; then
cmd_array+=(--concurrency "${{ inputs.concurrency }}")
cmd_array+=(--max-retries "${{ inputs.max-retries }}")
if [[ -n "${{ inputs.check-url }}" ]]; then
cmd_array+=(--check-url "${{ inputs.check-url }}")
fi
if [[ -n "${{ inputs.index-url }}" ]]; then
cmd_array+=(--index-url "${{ inputs.index-url }}")
fi
fi
# Add extra args (split on whitespace into array).
if [[ -n "${{ inputs.extra-args }}" ]]; then
read -ra extra_args_array <<< "${{ inputs.extra-args }}"
cmd_array+=("${extra_args_array[@]}")
fi
echo "::group::Running: ${cmd_array[*]}"
"${cmd_array[@]}"
EXIT_CODE=$?
echo "::endgroup::"
echo "exit-code=$EXIT_CODE" >> "$GITHUB_OUTPUT"
exit $EXIT_CODE
# 5. Capture plan JSON output (only for plan command).
- name: Capture plan JSON
id: plan-json
if: inputs.command == 'plan' && success()
shell: bash
working-directory: ${{ inputs.working-directory }}
env:
GITHUB_TOKEN: ${{ github.token }}
run: |
cmd_array=(uv run --active releasekit plan --format json)
if [[ -n "${{ inputs.group }}" ]]; then
cmd_array+=(--group "${{ inputs.group }}")
fi
PLAN_JSON=$("${cmd_array[@]}")
echo "json<<EOF" >> "$GITHUB_OUTPUT"
echo "$PLAN_JSON" >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"