Skip to content

hotfix(test): cherry-pick 2a74afaf (conflicts) #681

hotfix(test): cherry-pick 2a74afaf (conflicts)

hotfix(test): cherry-pick 2a74afaf (conflicts) #681

Workflow file for this run

# 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"