hotfix(test): cherry-pick 2a74afaf (conflicts) #681
Workflow file for this run
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 | |
| # | |
| # Cascade hotfix workflow. | |
| # | |
| # Cherry-picks a trunk fix onto a diverged intermediate environment. On | |
| # manual dispatch it plans the cherry-pick, replays the commit onto the | |
| # env/<env> integration branch via a hotfix/<env>/<sha> branch, and opens a | |
| # resolution pull request. When that pull request merges it builds, deploys, | |
| # and finalizes the hotfix for the target environment. Clean cherry-picks | |
| # auto-merge; conflicting ones open a labeled pull request for a human to | |
| # resolve locally before the build/deploy stages run. | |
| name: Cascade Hotfix | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| commit: | |
| description: 'Trunk commit SHA(s) to hotfix, comma-delimited (must be on trunk)' | |
| required: true | |
| type: string | |
| target_env: | |
| description: 'Target environment' | |
| required: true | |
| type: choice | |
| options: | |
| - test | |
| - staging | |
| - prod | |
| pr_number: | |
| description: 'Existing hotfix PR number to replay (optional)' | |
| required: false | |
| type: string | |
| dry_run: | |
| description: 'Dry run (validate only, mutate nothing)' | |
| required: false | |
| type: boolean | |
| default: false | |
| pull_request: | |
| types: [closed] | |
| branches: | |
| - 'env/*' | |
| permissions: | |
| contents: read | |
| actions: read | |
| concurrency: | |
| group: ${{ github.event_name == 'pull_request' && format('hotfix-finalize-{0}', github.repository) || format('hotfix-{0}', github.event.inputs.target_env) }} | |
| cancel-in-progress: false | |
| jobs: | |
| plan: | |
| name: Plan Hotfix | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| pull-requests: read | |
| actions: read | |
| outputs: | |
| branch: ${{ steps.plan.outputs.branch }} | |
| fix_sha: ${{ steps.plan.outputs.fix_sha }} | |
| base_sha: ${{ steps.plan.outputs.base_sha }} | |
| hotfix_version_candidate: ${{ steps.plan.outputs.hotfix_version_candidate }} | |
| conflict_expected: ${{ steps.plan.outputs.conflict_expected }} | |
| no_op: ${{ steps.plan.outputs.no_op }} | |
| env_sequence: ${{ steps.plan.outputs.env_sequence }} | |
| commits_test: ${{ steps.plan.outputs.commits_test }} | |
| no_op_test: ${{ steps.plan.outputs.no_op_test }} | |
| base_test: ${{ steps.plan.outputs.base_test }} | |
| commits_staging: ${{ steps.plan.outputs.commits_staging }} | |
| no_op_staging: ${{ steps.plan.outputs.no_op_staging }} | |
| base_staging: ${{ steps.plan.outputs.base_staging }} | |
| commits_prod: ${{ steps.plan.outputs.commits_prod }} | |
| no_op_prod: ${{ steps.plan.outputs.no_op_prod }} | |
| base_prod: ${{ steps.plan.outputs.base_prod }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup CLI | |
| uses: stablekernel/cascade/.github/actions/setup-cli@3742e04377b952a263bb48a0a0bb5905c253d603 # v1.1.2-rc.0 | |
| with: | |
| version: v1.1.2-rc.0 | |
| token: ${{ github.token }} | |
| - name: Fetch env branches and tags | |
| run: | | |
| git fetch origin '+refs/heads/env/*:refs/remotes/origin/env/*' --tags | |
| - name: Plan hotfix | |
| id: plan | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| HOTFIX_COMMIT: ${{ github.event.inputs.commit }} | |
| HOTFIX_TARGET_ENV: ${{ github.event.inputs.target_env }} | |
| HOTFIX_DRY_RUN: ${{ github.event.inputs.dry_run }} | |
| run: | | |
| cascade hotfix plan \ | |
| --config .github/manifest.yaml \ | |
| --commits "$HOTFIX_COMMIT" \ | |
| --target-env "$HOTFIX_TARGET_ENV" \ | |
| --repo "${{ github.repository }}" \ | |
| --dry-run="$HOTFIX_DRY_RUN" \ | |
| --gha-output | |
| - name: Surface protection suggestions | |
| if: steps.plan.outputs.protection_suggestions != '' | |
| env: | |
| SUGGESTIONS: ${{ steps.plan.outputs.protection_suggestions }} | |
| run: | | |
| while IFS= read -r line; do | |
| [ -z "$line" ] && continue | |
| echo "::notice::$line" | |
| done <<< "$SUGGESTIONS" | |
| apply: | |
| name: Apply Hotfix Cherry-Pick | |
| needs: plan | |
| if: github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run != 'true' && needs.plan.outputs.env_sequence != '' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| env: | |
| GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }} | |
| HOTFIX_COMMIT: ${{ github.event.inputs.commit }} | |
| HOTFIX_TARGET_ENV: ${{ github.event.inputs.target_env }} | |
| ENV_SEQUENCE: ${{ needs.plan.outputs.env_sequence }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup CLI | |
| uses: stablekernel/cascade/.github/actions/setup-cli@3742e04377b952a263bb48a0a0bb5905c253d603 # v1.1.2-rc.0 | |
| with: | |
| version: v1.1.2-rc.0 | |
| token: ${{ github.token }} | |
| - name: Fetch env branches and tags | |
| run: | | |
| git fetch origin '+refs/heads/env/*:refs/remotes/origin/env/*' --tags | |
| - name: Configure git identity | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| - name: Check branch protection on env branch | |
| continue-on-error: true | |
| run: | | |
| for env in $(echo "$ENV_SEQUENCE" | tr ',' '\n'); do | |
| PROT_PATH="repos/${{ github.repository }}/branches/env%2F${env}/protection" | |
| PROT=$(gh api "$PROT_PATH" 2>/dev/null || echo '') | |
| CHECKS=$(echo "$PROT" | jq -r '.required_status_checks.contexts[]? // empty' 2>/dev/null || echo '') | |
| if [ -z "$PROT" ] || [ -z "$CHECKS" ]; then | |
| echo "::warning::Branch env/${env} has no required status checks; hotfix auto-merge will NOT be gated by required checks." | |
| echo "::warning::Configure protection: gh api \"$PROT_PATH\" -X PUT -f required_status_checks.strict=true -F required_status_checks.contexts[]=hotfix-check" | |
| fi | |
| done | |
| - name: Ensure hotfix labels exist | |
| run: | | |
| gh label create cascade-hotfix --color B60205 --description "Cascade hotfix resolution PR" || true | |
| gh label create cascade-hotfix-conflict --color D93F0B --description "Cascade hotfix resolution PR with cherry-pick conflicts" || true | |
| - name: Cherry-pick and open resolution PRs | |
| env: | |
| COMMITS_TEST: ${{ needs.plan.outputs.commits_test }} | |
| BASE_TEST: ${{ needs.plan.outputs.base_test }} | |
| COMMITS_STAGING: ${{ needs.plan.outputs.commits_staging }} | |
| BASE_STAGING: ${{ needs.plan.outputs.base_staging }} | |
| COMMITS_PROD: ${{ needs.plan.outputs.commits_prod }} | |
| BASE_PROD: ${{ needs.plan.outputs.base_prod }} | |
| run: | | |
| REMAINING="$ENV_SEQUENCE" | |
| for env in $(echo "$ENV_SEQUENCE" | tr ',' '\n'); do | |
| REMAINING="${REMAINING#"$env"}" | |
| REMAINING="${REMAINING#,}" | |
| case "$env" in | |
| test) COMMITS="$COMMITS_TEST"; BASE="$BASE_TEST" ;; | |
| staging) COMMITS="$COMMITS_STAGING"; BASE="$BASE_STAGING" ;; | |
| prod) COMMITS="$COMMITS_PROD"; BASE="$BASE_PROD" ;; | |
| esac | |
| if [ -z "$COMMITS" ]; then | |
| echo "::notice::env/${env}: all commits already present, skipping" | |
| continue | |
| fi | |
| FIRST_COMMIT=$(echo "$COMMITS" | cut -d',' -f1) | |
| SHORT_SHA=$(echo "$FIRST_COMMIT" | cut -c1-8) | |
| BRANCH="hotfix/${env}/${SHORT_SHA}" | |
| if ! git rev-parse --verify --quiet "refs/remotes/origin/env/${env}" >/dev/null; then | |
| git push origin "${BASE}:refs/heads/env/${env}" | |
| git fetch origin "+refs/heads/env/${env}:refs/remotes/origin/env/${env}" | |
| fi | |
| git switch -c "$BRANCH" "$BASE" | |
| BODY=$(printf 'Cascade-Hotfix-Target: %s\nCascade-Hotfix-Source: %s\nCascade-Hotfix-Base: %s\n' "$env" "$COMMITS" "$BASE") | |
| CLEAN=true | |
| CONFLICT_COMMIT="" | |
| CONFLICTS="" | |
| for commit in $(echo "$COMMITS" | tr ',' '\n'); do | |
| if ! git cherry-pick -x "$commit"; then | |
| CLEAN=false | |
| CONFLICT_COMMIT="$commit" | |
| CONFLICTS=$(git diff --name-only --diff-filter=U) | |
| git add -A | |
| git -c core.editor=true cherry-pick --continue || git commit -m "hotfix: cherry-pick $(echo "$commit" | cut -c1-8) with conflicts" | |
| break | |
| fi | |
| done | |
| if $CLEAN; then | |
| git push origin "$BRANCH" | |
| gh pr create \ | |
| --base "env/${env}" \ | |
| --head "$BRANCH" \ | |
| --label cascade-hotfix \ | |
| --title "hotfix(${env}): cherry-pick ${SHORT_SHA}" \ | |
| --body "$BODY" | |
| ATTEMPTS=20 | |
| SLEEP=15 | |
| MERGED=false | |
| for i in $(seq 1 "$ATTEMPTS"); do | |
| STATE=$(gh pr view "$BRANCH" --json mergeable,mergeStateStatus -q '.mergeable + " " + .mergeStateStatus' 2>/dev/null || echo "UNKNOWN UNKNOWN") | |
| MERGEABLE=$(echo "$STATE" | cut -d' ' -f1) | |
| STATUS=$(echo "$STATE" | cut -d' ' -f2) | |
| echo "::notice::resolution PR mergeable=$MERGEABLE state=$STATUS (attempt $i/$ATTEMPTS)" | |
| if [ "$MERGEABLE" = "MERGEABLE" ] && [ "$STATUS" != "BLOCKED" ]; then | |
| if gh pr merge --squash --delete-branch "$BRANCH"; then | |
| MERGED=true | |
| break | |
| fi | |
| fi | |
| sleep "$SLEEP" | |
| done | |
| if [ "$MERGED" != "true" ]; then | |
| echo "::error::Resolution PR for $BRANCH did not become mergeable within the timeout; merge it manually to run the hotfix finalize chain" | |
| exit 1 | |
| fi | |
| git fetch origin '+refs/heads/env/*:refs/remotes/origin/env/*' --tags | |
| else | |
| echo "::warning::Cherry-pick conflicted on env/${env}; opening resolution PR and halting chain" | |
| git push origin "$BRANCH" | |
| CONFLICT_BODY=$(printf '%s\n\nConflicting files:\n%s\n\nThis resolves %s.\n\nEnvironments still pending: %s.\n\nAfter merge, re-engage the hotfix workflow targeting %s.\n\nResolve locally:\n git fetch && git switch %s\n # resolve conflicts, then\n git push --force-with-lease\n' "$BODY" "$CONFLICTS" "$env" "$REMAINING" "$HOTFIX_TARGET_ENV" "$BRANCH") | |
| gh pr create \ | |
| --base "env/${env}" \ | |
| --head "$BRANCH" \ | |
| --label cascade-hotfix-conflict \ | |
| --title "hotfix(${env}): cherry-pick $(echo "$CONFLICT_COMMIT" | cut -c1-8) (conflicts)" \ | |
| --body "$CONFLICT_BODY" | |
| break | |
| fi | |
| done | |
| check: | |
| name: Validate Hotfix PR | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged != true | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup CLI | |
| uses: stablekernel/cascade/.github/actions/setup-cli@3742e04377b952a263bb48a0a0bb5905c253d603 # v1.1.2-rc.0 | |
| with: | |
| version: v1.1.2-rc.0 | |
| token: ${{ github.token }} | |
| - name: Validate manifest | |
| run: | | |
| MANIFEST_FILE=".github/manifest.yaml" | |
| RESULT=$(cascade lint --json --config "$MANIFEST_FILE") | |
| echo "$RESULT" | |
| VALID=$(echo "$RESULT" | jq -r '.valid // false') | |
| if [[ "$VALID" != "true" ]]; then | |
| echo "$RESULT" | jq -r '.errors[]? | "::error::" + .' | |
| echo "::error::Manifest validation failed" | |
| exit 1 | |
| fi | |
| echo "::notice::Manifest is valid" | |
| context: | |
| name: Hotfix Context | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'cascade-hotfix') | |
| runs-on: ubuntu-latest | |
| outputs: | |
| target_env: ${{ steps.ctx.outputs.target_env }} | |
| fix_sha: ${{ steps.ctx.outputs.fix_sha }} | |
| base_sha: ${{ steps.ctx.outputs.base_sha }} | |
| rollback_sha: ${{ steps.ctx.outputs.rollback_sha }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| with: | |
| fetch-depth: 0 | |
| - name: Derive target environment and hotfix SHAs | |
| id: ctx | |
| env: | |
| BASE_REF: ${{ github.event.pull_request.base.ref }} | |
| PR_BODY: ${{ github.event.pull_request.body }} | |
| run: | | |
| TARGET_ENV="${BASE_REF#env/}" | |
| FIX_SHA=$(printf '%s\n' "$PR_BODY" | grep -m1 '^Cascade-Hotfix-Source:' | sed 's/^Cascade-Hotfix-Source:[[:space:]]*//' || true) | |
| BASE_SHA=$(printf '%s\n' "$PR_BODY" | grep -m1 '^Cascade-Hotfix-Base:' | sed 's/^Cascade-Hotfix-Base:[[:space:]]*//' || true) | |
| MANIFEST_FILE=".github/manifest.yaml" | |
| MANIFEST_KEY="ci" | |
| ROLLBACK_SHA=$(yq eval ".$MANIFEST_KEY.state.${TARGET_ENV}.sha // \"\"" "$MANIFEST_FILE") | |
| if [ "$ROLLBACK_SHA" = "null" ]; then ROLLBACK_SHA=""; fi | |
| { | |
| echo "target_env=${TARGET_ENV}" | |
| echo "fix_sha=${FIX_SHA}" | |
| echo "base_sha=${BASE_SHA}" | |
| echo "rollback_sha=${ROLLBACK_SHA}" | |
| } >> "$GITHUB_OUTPUT" | |
| build-app: | |
| name: Build app | |
| needs: context | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'cascade-hotfix') | |
| uses: ./.github/workflows/build-app.yaml | |
| with: | |
| sha: ${{ github.event.pull_request.merge_commit_sha }} | |
| target_env: ${{ needs.context.outputs.target_env }} | |
| build-docs: | |
| name: Build docs | |
| needs: context | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'cascade-hotfix') | |
| uses: ./.github/workflows/build-docs.yaml | |
| with: | |
| sha: ${{ github.event.pull_request.merge_commit_sha }} | |
| target_env: ${{ needs.context.outputs.target_env }} | |
| deploy-app: | |
| name: Deploy app | |
| needs: [context, build-app, build-docs] | |
| if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'cascade-hotfix') | |
| environment: ${{ needs.context.outputs.target_env }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Run deploy app | |
| env: | |
| DEPLOY_ENV: ${{ needs.context.outputs.target_env }} | |
| DEPLOY_SHA: ${{ github.event.pull_request.merge_commit_sha }} | |
| run: | | |
| echo "deploy app via ./.github/workflows/deploy-app.yaml to $DEPLOY_ENV at $DEPLOY_SHA" | |
| rollback-app: | |
| name: Rollback app | |
| needs: [context, deploy-app] | |
| if: always() && needs.context.outputs.rollback_sha != '' && needs.deploy-app.result == 'failure' | |
| environment: ${{ needs.context.outputs.target_env }} | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Rollback deploy app | |
| env: | |
| ROLLBACK_ENV: ${{ needs.context.outputs.target_env }} | |
| ROLLBACK_SHA: ${{ needs.context.outputs.rollback_sha }} | |
| run: | | |
| echo "rollback app in $ROLLBACK_ENV to $ROLLBACK_SHA" | |
| finalize: | |
| name: Finalize Hotfix | |
| needs: [context, deploy-app] | |
| if: success() && github.event_name == 'pull_request' && github.event.pull_request.merged == true && contains(github.event.pull_request.labels.*.name, 'cascade-hotfix') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| env: | |
| TARGET_ENV: ${{ needs.context.outputs.target_env }} | |
| MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} | |
| FIX_SHA: ${{ needs.context.outputs.fix_sha }} | |
| BASE_SHA: ${{ needs.context.outputs.base_sha }} | |
| steps: | |
| - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup CLI | |
| uses: stablekernel/cascade/.github/actions/setup-cli@3742e04377b952a263bb48a0a0bb5905c253d603 # v1.1.2-rc.0 | |
| with: | |
| version: v1.1.2-rc.0 | |
| token: ${{ github.token }} | |
| - name: Fetch env branches and tags | |
| run: | | |
| git fetch origin '+refs/heads/env/*:refs/remotes/origin/env/*' --tags | |
| - name: Finalize hotfix | |
| env: | |
| GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} | |
| run: | | |
| cascade hotfix finalize \ | |
| --config .github/manifest.yaml \ | |
| --target-env "$TARGET_ENV" \ | |
| --merge-sha "$MERGE_SHA" \ | |
| --fix-sha "$FIX_SHA" \ | |
| --base-sha "$BASE_SHA" |