Promote #113
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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" |