# 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
# ══════════════════════════════════════════════════════════════════════
# Reusable composite action: Run a releasekit command
#
# Builds the CLI invocation from structured inputs, runs it with
# proper error handling, and parses outputs into GITHUB_OUTPUT.
#
# Supports all three pipeline stages: prepare, release, publish.
# All user-controlled inputs are passed via env vars to prevent
# GitHub Actions script injection.
#
# Outputs:
# has_bumps — "true" if prepare found version bumps
# pr_url — URL of the created/updated Release PR
# release_url — URL of the created GitHub Release
#
# Usage:
#
# - uses: ./.github/actions/run-releasekit
# id: prepare
# with:
# command: prepare
# workspace: py
# releasekit-dir: py/tools/releasekit
# group: ${{ inputs.group }}
# force: ${{ inputs.force_prepare }}
#
# ══════════════════════════════════════════════════════════════════════
name: Run ReleaseKit
description: >-
Run a releasekit command (prepare, release, or publish) with
structured inputs and parsed outputs.
inputs:
command:
description: >-
The releasekit command to run: prepare, release, or publish.
required: true
workspace:
description: >-
Workspace label (e.g. "py", "js", "go").
required: true
releasekit-dir:
description: >-
Path to the releasekit tool directory (relative to repo root).
required: false
default: py/tools/releasekit
dry-run:
description: >-
Set to "true" to pass --dry-run.
required: false
default: "false"
force:
description: >-
Set to "true" to pass --force (prepare and publish).
required: false
default: "false"
group:
description: >-
Release group to target (empty for all).
required: false
default: ""
bump-type:
description: >-
Override bump type: auto, patch, minor, major (prepare only).
required: false
default: "auto"
prerelease:
description: >-
Prerelease suffix, e.g. "rc.1" (prepare only).
required: false
default: ""
concurrency:
description: >-
Max parallel publish jobs, 0 = auto (publish only).
required: false
default: "0"
max-retries:
description: >-
Max retries for failed publishes (publish only).
required: false
default: "0"
check-url:
description: >-
Registry URL for version existence checks (publish only).
required: false
default: ""
index-url:
description: >-
Registry URL for uploading packages (publish only).
required: false
default: ""
no-ai:
description: >-
Disable all AI features (summarization, codenames).
required: false
default: "false"
model:
description: >-
Override AI model (e.g. "ollama/gemma3:12b", "gemini-pro").
required: false
default: ""
codename-theme:
description: >-
Override codename theme (e.g. "galaxies", "animals").
required: false
default: ""
show-plan:
description: >-
Show the execution plan before running the command.
required: false
default: "false"
tag:
description: >-
Git tag to roll back (rollback only).
required: false
default: ""
all-tags:
description: >-
Delete ALL tags pointing to the same commit (rollback only).
required: false
default: "false"
yank:
description: >-
Also yank/deprecate versions from the package registry (rollback only).
required: false
default: "false"
yank-reason:
description: >-
Reason for yanking (rollback only).
required: false
default: ""
outputs:
has_bumps:
description: "'true' if prepare found version bumps."
value: ${{ steps.run.outputs.has_bumps }}
pr_url:
description: URL of the Release PR (prepare only).
value: ${{ steps.run.outputs.pr_url }}
release_url:
description: URL of the GitHub Release (release only).
value: ${{ steps.run.outputs.release_url }}
runs:
using: composite
steps:
# ── Optional: show execution plan ───────────────────────────────
- name: Preview execution plan
if: inputs.show-plan == 'true'
shell: bash
env:
RELEASEKIT_DIR: ${{ inputs.releasekit-dir }}
RELEASEKIT_WORKSPACE: ${{ inputs.workspace }}
RELEASEKIT_DRY_RUN: ${{ inputs.dry-run }}
run: |
echo "::group::Execution Plan"
uv run --directory "$RELEASEKIT_DIR" \
releasekit --workspace "$RELEASEKIT_WORKSPACE" plan --format full 2>&1 || true
echo "::endgroup::"
if [ "$RELEASEKIT_DRY_RUN" = "true" ]; then
echo "::notice::DRY RUN — no side effects"
else
echo "::notice::LIVE RUN"
fi
# ── Build and run the command ───────────────────────────────────
- name: Run releasekit ${{ inputs.command }}
id: run
shell: bash
env:
RELEASEKIT_COMMAND: ${{ inputs.command }}
RELEASEKIT_WORKSPACE: ${{ inputs.workspace }}
RELEASEKIT_DIR: ${{ inputs.releasekit-dir }}
RELEASEKIT_DRY_RUN: ${{ inputs.dry-run }}
RELEASEKIT_FORCE: ${{ inputs.force }}
RELEASEKIT_GROUP: ${{ inputs.group }}
RELEASEKIT_BUMP_TYPE: ${{ inputs.bump-type }}
RELEASEKIT_PRERELEASE: ${{ inputs.prerelease }}
RELEASEKIT_CONCURRENCY: ${{ inputs.concurrency }}
RELEASEKIT_MAX_RETRIES: ${{ inputs.max-retries }}
RELEASEKIT_CHECK_URL: ${{ inputs.check-url }}
RELEASEKIT_INDEX_URL: ${{ inputs.index-url }}
RELEASEKIT_NO_AI: ${{ inputs.no-ai }}
RELEASEKIT_MODEL: ${{ inputs.model }}
RELEASEKIT_CODENAME_THEME: ${{ inputs.codename-theme }}
RELEASEKIT_TAG: ${{ inputs.tag }}
RELEASEKIT_ALL_TAGS: ${{ inputs.all-tags }}
RELEASEKIT_YANK: ${{ inputs.yank }}
RELEASEKIT_YANK_REASON: ${{ inputs.yank-reason }}
run: |
set -euo pipefail
cmd=(uv run --directory "$RELEASEKIT_DIR" releasekit --workspace "$RELEASEKIT_WORKSPACE" "$RELEASEKIT_COMMAND")
# ── Common flags ──────────────────────────────────────────
if [ "$RELEASEKIT_DRY_RUN" = "true" ]; then
cmd+=(--dry-run)
fi
if [ "$RELEASEKIT_FORCE" = "true" ]; then
cmd+=(--force)
fi
if [ -n "$RELEASEKIT_GROUP" ]; then
cmd+=(--group "$RELEASEKIT_GROUP")
fi
# ── prepare-specific flags ────────────────────────────────
if [ "$RELEASEKIT_COMMAND" = "prepare" ]; then
if [ "$RELEASEKIT_BUMP_TYPE" != "auto" ] && [ -n "$RELEASEKIT_BUMP_TYPE" ]; then
cmd+=(--bump "$RELEASEKIT_BUMP_TYPE")
fi
if [ -n "$RELEASEKIT_PRERELEASE" ]; then
cmd+=(--prerelease "$RELEASEKIT_PRERELEASE")
fi
fi
# ── publish-specific flags ────────────────────────────────
if [ "$RELEASEKIT_COMMAND" = "publish" ]; then
if [ -n "$RELEASEKIT_CHECK_URL" ]; then
cmd+=(--check-url "$RELEASEKIT_CHECK_URL")
fi
if [ -n "$RELEASEKIT_INDEX_URL" ]; then
cmd+=(--index-url "$RELEASEKIT_INDEX_URL")
fi
if [ -n "$RELEASEKIT_CONCURRENCY" ] && [ "$RELEASEKIT_CONCURRENCY" != "0" ]; then
cmd+=(--concurrency "$RELEASEKIT_CONCURRENCY")
fi
if [ -n "$RELEASEKIT_MAX_RETRIES" ] && [ "$RELEASEKIT_MAX_RETRIES" != "0" ]; then
cmd+=(--max-retries "$RELEASEKIT_MAX_RETRIES")
fi
fi
# ── AI flags ─────────────────────────────────────────────
if [ "$RELEASEKIT_NO_AI" = "true" ]; then
cmd+=(--no-ai)
fi
if [ -n "$RELEASEKIT_MODEL" ]; then
cmd+=(--model "$RELEASEKIT_MODEL")
fi
if [ -n "$RELEASEKIT_CODENAME_THEME" ]; then
cmd+=(--codename-theme "$RELEASEKIT_CODENAME_THEME")
fi
# ── rollback-specific flags ─────────────────────────────
if [ "$RELEASEKIT_COMMAND" = "rollback" ]; then
if [ -n "$RELEASEKIT_TAG" ]; then
cmd+=(--tag "$RELEASEKIT_TAG")
fi
if [ "$RELEASEKIT_ALL_TAGS" = "true" ]; then
cmd+=(--all-tags)
fi
if [ "$RELEASEKIT_YANK" = "true" ]; then
cmd+=(--yank)
fi
if [ -n "$RELEASEKIT_YANK_REASON" ]; then
cmd+=(--yank-reason "$RELEASEKIT_YANK_REASON")
fi
fi
# ── Execute ───────────────────────────────────────────────
echo "::group::Running: ${cmd[*]}"
OUTPUT=$("${cmd[@]}" 2>&1) || EXIT_CODE=$?
echo "$OUTPUT"
echo "::endgroup::"
if [ "${EXIT_CODE:-0}" -ne 0 ]; then
echo "::error::releasekit $RELEASEKIT_COMMAND failed with exit code $EXIT_CODE"
exit "$EXIT_CODE"
fi
# ── Parse outputs ─────────────────────────────────────────
if [ "$RELEASEKIT_COMMAND" = "prepare" ]; then
PR_URL=$(echo "$OUTPUT" | sed -n 's/.*Release PR: //p' | tail -1)
if [ -n "$PR_URL" ]; then
echo "has_bumps=true" >> "$GITHUB_OUTPUT"
echo "pr_url=$PR_URL" >> "$GITHUB_OUTPUT"
else
echo "has_bumps=false" >> "$GITHUB_OUTPUT"
fi
fi
if [ "$RELEASEKIT_COMMAND" = "release" ]; then
RELEASE_URL=$(echo "$OUTPUT" | sed -n 's/.*release_url=//p' | tail -1)
if [ -n "$RELEASE_URL" ]; then
echo "release_url=$RELEASE_URL" >> "$GITHUB_OUTPUT"
fi
fi