Skip to content

Promote

Promote #113

Workflow file for this run

# AUTO-GENERATED by cascade - DO NOT EDIT MANUALLY
# Regenerate with: cascade generate-workflow --config .github/manifest.yaml
#
# Environments: staging → prod
#
# Promotion modes:
# default - Sequential single-step promotion (each env → immediate next)
# Supports --force to continue on failure
# cascade - Atomic cascade from source to target (e.g., dev-to-prod)
# All intermediate environments updated with same artifact
# Fails entirely if any step fails (no partial state)
#
# Release states (role, or position when no role is set):
# staging = prerelease
# prod = released
#
# Cascade targets (for cascade mode):
# staging-to-prod - Promotes staging → prod
#
# Breaking changes:
# Breaking changes block at: pre-release → release AND release → prod
# Check 'allow_breaking_changes' to proceed with breaking changes.
name: Promote
on:
workflow_dispatch:
inputs:
mode:
description: 'Promotion mode - default (sequential) or select a cascade target'
type: choice
required: true
options:
- default
- staging-to-prod
default: default
force:
description: 'Continue on failure (default mode only)'
type: boolean
default: false
allow_breaking_changes:
description: 'Required if promoting breaking changes past pre-release → release'
type: boolean
default: false
dry_run:
description: 'Dry run mode'
type: boolean
default: false
deploys:
description: 'Deploys to promote (comma-separated names or "all")'
type: string
default: 'all'
rollback_on_failure:
description: 'Revert successful deploys if any fails (atomic promotion)'
type: boolean
default: true
allow_downgrade:
description: 'Permit promoting an older version (downgrade); prod always requires this'
type: boolean
default: false
# Per-deploy selection (deprecated, use 'deploys' input instead)
deploy_app:
description: '[Deprecated] Include app deployment'
type: boolean
default: true
permissions:
contents: read
concurrency:
group: "${{ github.workflow }}"
cancel-in-progress: false
jobs:
preflight:
name: Pre-flight Check
runs-on: ubuntu-latest
outputs:
source_env: ${{ steps.preflight.outputs.source_env }}
target_env: ${{ steps.preflight.outputs.target_env }}
source_sha: ${{ steps.preflight.outputs.source_sha }}
source_version: ${{ steps.preflight.outputs.source_version }}
source_image_tag: ${{ steps.preflight.outputs.source_image_tag }}
source_image_digest: ${{ steps.preflight.outputs.source_image_digest }}
changelog_base_sha: ${{ steps.preflight.outputs.changelog_base_sha }}
rollback_sha: ${{ steps.preflight.outputs.rollback_sha }}
rollback_on_failure: ${{ steps.preflight.outputs.rollback_on_failure }}
envs_to_update: ${{ steps.preflight.outputs.envs_to_update }}
skipped_envs: ${{ steps.preflight.outputs.skipped_envs }}
deploys_to_run: ${{ steps.preflight.outputs.deploys_to_run }}
external_deploys_to_run: ${{ steps.preflight.outputs.external_deploys_to_run }}
is_prerelease_env: ${{ steps.preflight.outputs.is_prerelease_env }}
is_final_env: ${{ steps.preflight.outputs.is_final_env }}
is_cascade: ${{ steps.preflight.outputs.is_cascade }}
release_action: ${{ steps.preflight.outputs.release_action }}
has_prod_deployment: ${{ steps.preflight.outputs.has_prod_deployment }}
prod_sha: ${{ steps.preflight.outputs.prod_sha }}
prod_version: ${{ steps.preflight.outputs.prod_version }}
has_breaking: ${{ steps.preflight.outputs.has_breaking }}
can_proceed: ${{ steps.preflight.outputs.can_proceed }}
promotion_result: ${{ steps.preflight.outputs.promotion_result }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.6-rc.1
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.16.6-rc.1
- name: Run Preflight
id: preflight
env:
PROMOTION_MODE: ${{ github.event.inputs.mode }}
PROMOTION_FORCE: ${{ github.event.inputs.force }}
ALLOW_BREAKING: ${{ github.event.inputs.allow_breaking_changes }}
DEPLOYS: ${{ github.event.inputs.deploys }}
ROLLBACK_ON_FAILURE: ${{ github.event.inputs.rollback_on_failure }}
ALLOW_DOWNGRADE: ${{ github.event.inputs.allow_downgrade }}
DEPLOY_APP: ${{ github.event.inputs.deploy_app }}
run: |
cascade promote preflight \
--mode "${PROMOTION_MODE:-default}" \
--force="${PROMOTION_FORCE:-false}" \
--config .github/manifest.yaml \
--allow-breaking="${ALLOW_BREAKING:-false}" \
--deploys="${DEPLOYS:-all}" \
--rollback-on-failure="${ROLLBACK_ON_FAILURE:-true}" \
--allow-downgrade="${ALLOW_DOWNGRADE:-false}" \
--gha-output
- name: Fail if Cannot Proceed
if: steps.preflight.outputs.can_proceed == 'false'
run: exit 1
promote:
name: Promote
needs: preflight
if: ${{ github.event.inputs.dry_run != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.6-rc.1
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.16.6-rc.1
- name: Validate Promotion
env:
MODE: ${{ github.event.inputs.mode }}
run: |
echo "Promotion validated by preflight job"
echo "Mode: $MODE"
echo "Source: ${{ needs.preflight.outputs.source_env }}"
echo "Final Env: ${{ needs.preflight.outputs.target_env }}"
echo "::notice::Promotion validation completed successfully"
deploy-app:
name: Deploy app
needs: [preflight, promote]
if: ${{ github.event.inputs.dry_run != 'true' && contains(fromJSON(needs.preflight.outputs.deploys_to_run), 'app') }}
uses: ./.github/workflows/deploy-app.yaml
with:
environment: ${{ needs.preflight.outputs.target_env }}
sha: ${{ needs.preflight.outputs.source_sha }}
deploy-app-prod:
name: Deploy app (prod)
needs: [preflight, promote]
if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.has_prod_deployment == 'true' }}
uses: ./.github/workflows/deploy-app.yaml
with:
environment: prod
sha: ${{ needs.preflight.outputs.prod_sha }}
# Rollback jobs - revert successful deploys if any deploy fails
rollback-app:
name: Rollback app
needs: [preflight, deploy-app, deploy-app-prod]
if: |
always() &&
needs.preflight.outputs.rollback_on_failure == 'true' &&
needs.preflight.outputs.rollback_sha != '' &&
needs.deploy-app.result == 'success' &&
(needs.deploy-app.result == 'failure' || needs.deploy-app-prod.result == 'failure')
uses: ./.github/workflows/deploy-app.yaml
with:
environment: ${{ needs.preflight.outputs.target_env }}
sha: ${{ needs.preflight.outputs.rollback_sha }}
finalize:
name: Finalize
needs: [preflight, promote, deploy-app, deploy-app-prod]
if: always() && needs.preflight.result == 'success'
runs-on: ubuntu-latest
permissions:
contents: write
actions: write
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@v0.16.6-rc.1
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.16.6-rc.1
- name: Generate Changelog
id: changelog
env:
GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
CHANGELOG_BASE_SHA: ${{ needs.preflight.outputs.changelog_base_sha }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
run: |
# Use changelog base SHA from preflight (first target env's current state)
TARGET_SHA="$CHANGELOG_BASE_SHA"
if [[ -z "$TARGET_SHA" ]]; then
# First deployment ever - compare against initial commit
TARGET_SHA=$(git rev-list --max-parents=0 HEAD | tail -n 1)
fi
RESULT=$(cascade generate-changelog --base-sha "$TARGET_SHA" --head-sha "$SOURCE_SHA" --repo "${{ github.repository }}")
echo "$RESULT" | jq -r '.changelog' > "$RUNNER_TEMP/cascade-changelog.md"
- name: Extract Release Data
id: release-data
env:
PROMOTION_RESULT: ${{ needs.preflight.outputs.promotion_result }}
run: |
# Extract release_data from promotion result
# This contains the correct values for the version being released:
# - sha: the commit SHA of the release
# - rc_version: the RC tag (e.g., v1.0.0-rc.0) - used for prerelease
# - sem_version: the semver tag (e.g., v1.0.0) - used for publish
RELEASE_DATA=$(echo "$PROMOTION_RESULT" | jq -r '.release_data // {}')
if [[ "$RELEASE_DATA" != "{}" && "$RELEASE_DATA" != "null" ]]; then
RELEASE_SHA=$(echo "$RELEASE_DATA" | jq -r '.sha // ""')
RC_VERSION=$(echo "$RELEASE_DATA" | jq -r '.rc_version // ""')
SEM_VERSION=$(echo "$RELEASE_DATA" | jq -r '.sem_version // ""')
echo "sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
echo "rc_version=$RC_VERSION" >> "$GITHUB_OUTPUT"
echo "sem_version=$SEM_VERSION" >> "$GITHUB_OUTPUT"
echo "::notice::Release data - SHA: ${RELEASE_SHA:0:7}, RC: $RC_VERSION, Semver: $SEM_VERSION"
else
echo "::notice::No release data (not a prerelease/publish promotion)"
echo "sha=" >> "$GITHUB_OUTPUT"
echo "rc_version=" >> "$GITHUB_OUTPUT"
echo "sem_version=" >> "$GITHUB_OUTPUT"
fi
- name: Update Release
if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.is_prerelease_env != 'true' && needs.preflight.outputs.is_final_env != 'true' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: update
environment: ${{ needs.preflight.outputs.target_env }}
sha: ${{ needs.preflight.outputs.source_sha }}
tag: ${{ needs.preflight.outputs.source_version }}
changelog_file: ${{ runner.temp }}/cascade-changelog.md
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Ensure Release Exists
if: ${{ github.event.inputs.dry_run != 'true' && (needs.preflight.outputs.is_prerelease_env == 'true' || needs.preflight.outputs.is_final_env == 'true') }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: update
environment: ${{ needs.preflight.outputs.source_env }}
sha: ${{ needs.preflight.outputs.source_sha }}
tag: ${{ needs.preflight.outputs.source_version }}
changelog_file: ${{ runner.temp }}/cascade-changelog.md
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Create Prerelease
if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.is_prerelease_env == 'true' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: prerelease
environment: ${{ needs.preflight.outputs.target_env }}
sha: ${{ steps.release-data.outputs.sha }}
tag: ${{ steps.release-data.outputs.rc_version }}
changelog_file: ${{ runner.temp }}/cascade-changelog.md
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Cleanup Orphaned Releases
if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.skipped_envs != '' }}
env:
SKIPPED_ENVS: ${{ needs.preflight.outputs.skipped_envs }}
SOURCE_VERSION: ${{ needs.preflight.outputs.source_version }}
GITHUB_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
run: |
IFS=',' read -ra ENVS <<< "$SKIPPED_ENVS"
for ENV in "${ENVS[@]}"; do
echo "Cleaning up orphaned release for $ENV"
cascade manage-release \
--repo "${{ github.repository }}" \
--action delete \
--environment "$ENV" \
--sha "" \
--tag "$SOURCE_VERSION" || true
done
- name: Publish Release
if: ${{ github.event.inputs.dry_run != 'true' && needs.preflight.outputs.is_final_env == 'true' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: publish
environment: ${{ needs.preflight.outputs.target_env }}
sha: ${{ steps.release-data.outputs.sha }}
tag: ${{ steps.release-data.outputs.sem_version }}
delete_tag: ${{ steps.release-data.outputs.rc_version }}
changelog_file: ${{ runner.temp }}/cascade-changelog.md
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Finalize Promotion
if: ${{ github.event.inputs.dry_run != 'true' }}
env:
GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
GITHUB_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
PROMOTION_RESULT: ${{ needs.preflight.outputs.promotion_result }}
DEPLOYS_TO_RUN: ${{ needs.preflight.outputs.deploys_to_run }}
DEPLOY_RESULT_APP: ${{ needs.deploy-app.result }}
run: |
cascade promote finalize \
--config .github/manifest.yaml \
--promotion-result "$PROMOTION_RESULT" \
--repo "${{ github.repository }}" \
--run-id "${{ github.run_id }}" \
--commit-push
- name: Summary
env:
PROMOTION_MODE: ${{ github.event.inputs.mode }}
SOURCE_ENV: ${{ needs.preflight.outputs.source_env }}
TARGET_ENV: ${{ needs.preflight.outputs.target_env }}
ENVS_UPDATED: ${{ needs.preflight.outputs.envs_to_update }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
{
echo "## Promotion Complete"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| Mode | $PROMOTION_MODE |"
echo "| From | $SOURCE_ENV |"
echo "| To | $TARGET_ENV |"
echo "| Environments Updated | $ENVS_UPDATED |"
echo "| SHA | \`$SOURCE_SHA\` |"
if [[ "$DRY_RUN" == "true" ]]; then
echo "| **DRY RUN** | Yes |"
fi
} >> "$GITHUB_STEP_SUMMARY"