Skip to content

feat: scenario source change #17

feat: scenario source change

feat: scenario source change #17

Workflow file for this run

# AUTO-GENERATED by cascade - DO NOT EDIT MANUALLY
# Regenerate with: cascade generate-workflow --config .github/manifest.yaml
name: Orchestrate CI/CD
on:
push:
branches: [main]
paths:
- 'src/**'
workflow_dispatch:
inputs:
environment:
description: 'Target environment'
type: choice
options:
- staging
- prod
default: 'staging'
dry_run:
description: 'Dry run mode'
type: boolean
default: false
concurrency:
group: orchestrate-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
actions: read
jobs:
setup:
name: Setup
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
run_build_build: ${{ steps.setup.outputs.run_build_build }}
run_deploy_app: ${{ steps.setup.outputs.run_deploy_app }}
run_deploy_notify: ${{ steps.setup.outputs.run_deploy_notify }}
head_sha: ${{ steps.setup.outputs.head_sha }}
version: ${{ steps.setup.outputs.version }}
previous_tag: ${{ steps.setup.outputs.previous_tag }}
changelog_base_sha: ${{ steps.setup.outputs.changelog_base_sha }}
base_build_build: ${{ steps.setup.outputs.base_build_build }}
base_deploy_app: ${{ steps.setup.outputs.base_deploy_app }}
base_deploy_notify: ${{ steps.setup.outputs.base_deploy_notify }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@v1.1.2-rc.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v1.1.2-rc.0
- name: Run Setup
id: setup
env:
ENVIRONMENT: ${{ github.event.inputs.environment || 'staging' }}
run: |
cascade orchestrate setup \
--environment "$ENVIRONMENT" \
--config .github/manifest.yaml \
--gha-output
build-build:
name: Build (build)
needs: [setup]
if: |
needs.setup.outputs.run_build_build == 'true'
uses: ./.github/workflows/build.yaml
with:
environment: ${{ github.event.inputs.environment || 'staging' }}
sha: ${{ needs.setup.outputs.head_sha }}
deploy-app:
name: Deploy (app)
needs: [setup]
if: |
needs.setup.outputs.run_deploy_app == 'true'
uses: ./.github/workflows/deploy-app.yaml
with:
environment: ${{ github.event.inputs.environment || 'staging' }}
sha: ${{ needs.setup.outputs.head_sha }}
deploy-app-retry-1:
name: Deploy (app) - Retry 1
needs: [setup, deploy-app]
if: ${{ !cancelled() && needs.deploy-app.result == 'failure' }}
uses: ./.github/workflows/deploy-app.yaml
with:
environment: ${{ github.event.inputs.environment || 'staging' }}
sha: ${{ needs.setup.outputs.head_sha }}
deploy-notify:
name: Deploy (notify)
needs: [setup, deploy-app, deploy-app-retry-1]
if: |
!cancelled() &&
(needs.deploy-app.result == 'success' || needs.deploy-app-retry-1.result == 'success')
uses: ./.github/workflows/deploy-notify.yaml
with:
environment: ${{ github.event.inputs.environment || 'staging' }}
sha: ${{ needs.setup.outputs.head_sha }}
finalize:
name: Finalize
needs: [setup, build-build, deploy-app, deploy-app-retry-1, deploy-notify]
if: always() && needs.setup.result == 'success'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
outputs:
build_build_artifact_id: ${{ needs.build-build.outputs.artifact_id }}
steps:
- uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Generate Summary
run: |
echo "## Orchestration Complete" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "### Callback Results" >> "$GITHUB_STEP_SUMMARY"
echo "| Callback | Result | On Failure |" >> "$GITHUB_STEP_SUMMARY"
echo "|----------|--------|------------|" >> "$GITHUB_STEP_SUMMARY"
echo "| Build (build) | ${{ needs.build-build.result }} | abort |" >> "$GITHUB_STEP_SUMMARY"
echo "| Deploy (app) | ${{ (needs.deploy-app.result == 'success' || needs.deploy-app-retry-1.result == 'success') && 'success' || 'failure' }} | abort |" >> "$GITHUB_STEP_SUMMARY"
echo "| Deploy (notify) | ${{ needs.deploy-notify.result }} | abort |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "### Outputs" >> "$GITHUB_STEP_SUMMARY"
HAS_OUTPUTS=false
if [[ -n "${{ needs.build-build.outputs.artifact_id }}" ]]; then
if [[ "$HAS_OUTPUTS" == "false" ]]; then
echo "| Output | Value |" >> "$GITHUB_STEP_SUMMARY"
echo "|--------|-------|" >> "$GITHUB_STEP_SUMMARY"
HAS_OUTPUTS=true
fi
echo "| build_build_artifact_id | ${{ needs.build-build.outputs.artifact_id }} |" >> "$GITHUB_STEP_SUMMARY"
fi
if [[ "$HAS_OUTPUTS" == "false" ]]; then
echo "_No outputs produced_" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/setup-cli@v1.1.2-rc.0
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v1.1.2-rc.0
- name: Generate Changelog
id: changelog
env:
GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
run: |
# Use changelog_base_sha which compares this env to next env
# This shows commits in this env NOT yet promoted to next env
RESULT=$(cascade generate-changelog \
--base-sha "${{ needs.setup.outputs.changelog_base_sha }}" \
--head-sha "${{ needs.setup.outputs.head_sha }}" \
--repo "${{ github.repository }}")
echo "$RESULT" | jq -r '.changelog' > "$RUNNER_TEMP/cascade-changelog.md"
- name: Manage Release
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: update
tag: ${{ needs.setup.outputs.version }}
create_tag: 'true'
environment: ${{ github.event.inputs.environment || 'staging' }}
sha: ${{ needs.setup.outputs.head_sha }}
dry_run: ${{ github.event.inputs.dry_run == 'true' }}
changelog_file: ${{ runner.temp }}/cascade-changelog.md
previous_tag: ${{ needs.setup.outputs.previous_tag }}
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Update Manifest
env:
GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
HEAD_SHA: ${{ needs.setup.outputs.head_sha }}
VERSION: ${{ needs.setup.outputs.version }}
DRY_RUN: ${{ github.event.inputs.dry_run == 'true' }}
ENVIRONMENT: ${{ github.event.inputs.environment || 'staging' }}
APP_RESULT: ${{ (needs.deploy-app.result == 'success' || needs.deploy-app-retry-1.result == 'success') && 'success' || 'failure' }}
NOTIFY_RESULT: ${{ needs.deploy-notify.result }}
BUILD_ARTIFACT_BUILD: ${{ needs.build-build.outputs.artifact_id }}
run: |
MANIFEST_FILE=".github/manifest.yaml"
MANIFEST_KEY="ci"
if [[ ! -f "$MANIFEST_FILE" ]]; then
echo "No $MANIFEST_FILE found - skipping state update"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BRANCH="${GITHUB_REF##refs/heads/}"
apply_state_edits() {
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
# Update environment-level state (committed, not deployed)
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.sha = \"$HEAD_SHA\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.version = \"$VERSION\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.committed_at = \"$TIMESTAMP\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.committed_by = \"${{ github.actor }}\"" "$MANIFEST_FILE"
if [[ "$APP_RESULT" == "success" ]]; then
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.app.sha = \"$HEAD_SHA\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.app.deployed_at = \"$TIMESTAMP\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.app.deployed_by = \"${{ github.actor }}\"" "$MANIFEST_FILE"
fi
if [[ "$NOTIFY_RESULT" == "success" ]]; then
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.notify.sha = \"$HEAD_SHA\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.notify.deployed_at = \"$TIMESTAMP\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.deploys.notify.deployed_by = \"${{ github.actor }}\"" "$MANIFEST_FILE"
fi
if [[ -n "$BUILD_ARTIFACT_BUILD" ]]; then
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.builds.build.sha = \"$HEAD_SHA\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.state.$ENVIRONMENT.builds.build.artifact_id = \"$BUILD_ARTIFACT_BUILD\"" "$MANIFEST_FILE"
fi
}
if [[ "$DRY_RUN" == "true" ]]; then
apply_state_edits
echo "cascade-state-write: dry-run preview (no commit)"
git --no-pager diff -- "$MANIFEST_FILE" || true
exit 0
fi
if [[ "$GITHUB_SERVER_URL" != "https://github.com" ]]; then
# act/gitea e2e: no GitHub API, and the trunk is neither protected nor
# signature-checked, so push the state commit directly with retries.
for attempt in 1 2 3 4 5 6 7 8 9 10; do
echo "cascade-state-write: attempt=$attempt/10"
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
apply_state_edits
if git diff --quiet "$MANIFEST_FILE"; then
echo "No state changes"
exit 0
fi
git add "$MANIFEST_FILE"
git commit -m "chore: update state for $ENVIRONMENT [skip ci]"
if git push origin "HEAD:$BRANCH"; then
echo "Pushed state on attempt $attempt"
echo "cascade-state-write: ok attempt=$attempt"
exit 0
fi
echo "Push attempt $attempt rejected (likely concurrent run); retrying..." >&2
backoff=$(( 1 << (attempt - 1) ))
if [ "$backoff" -gt 8 ]; then backoff=8; fi
sleep $(( backoff + RANDOM % 2 ))
done
echo "cascade-state-write: exhausted attempts=10" >&2
echo "::error::Failed to push state after 10 attempts" >&2
exit 1
fi
# Real GitHub: write state through the Contents REST API. API commits are
# signed by GitHub (Verified) and, with a bypass-capable token, update the
# trunk even when a required status check protects it.
for attempt in 1 2 3 4 5 6 7 8 9 10; do
echo "cascade-state-write: attempt=$attempt/10"
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
BASE_SHA=$(git rev-parse "origin/$BRANCH:$MANIFEST_FILE" 2>/dev/null || true)
if [[ -z "$BASE_SHA" ]]; then
echo "cascade-state-write: missing-base" >&2
echo "::error::$MANIFEST_FILE has no blob at the fetched origin/$BRANCH tip; refusing an unguarded state write" >&2
exit 1
fi
apply_state_edits
if git diff --quiet "$MANIFEST_FILE"; then
echo "No state changes"
exit 0
fi
CONTENT_B64=$(base64 -w0 "$MANIFEST_FILE" 2>/dev/null || base64 "$MANIFEST_FILE" | tr -d '\n')
API_ARGS=("repos/${{ github.repository }}/contents/$MANIFEST_FILE" -X PUT
-f "message=chore: update state for $ENVIRONMENT [skip ci]"
-f "content=$CONTENT_B64"
-f "branch=$BRANCH"
-f "sha=$BASE_SHA"
-f "author[name]=github-actions[bot]"
-f "author[email]=github-actions[bot]@users.noreply.github.com"
-f "committer[name]=github-actions[bot]"
-f "committer[email]=github-actions[bot]@users.noreply.github.com")
if API_ERR=$(gh api "${API_ARGS[@]}" 2>&1 >/dev/null); then
echo "Pushed state via API on attempt $attempt"
echo "cascade-state-write: ok attempt=$attempt"
exit 0
fi
echo "$API_ERR" >&2
case "$API_ERR" in
*"HTTP 409"*|*Conflict*)
echo "State write attempt $attempt hit an optimistic-lock conflict (concurrent run); retrying..." >&2
;;
*"HTTP 429"*|*"Too Many Requests"*|*"rate limit"*|*"secondary rate"*|*"abuse"*|*"Retry-After"*|*"retry-after"*)
echo "State write attempt $attempt was rate limited; retrying with backoff..." >&2
;;
*"HTTP 4"*)
echo "cascade-state-write: permanent-error attempt=$attempt" >&2
echo "::error::State write failed with a permanent API error (see log above); not retrying" >&2
exit 1
;;
*)
echo "State write attempt $attempt failed (transient server or network error); retrying..." >&2
;;
esac
backoff=$(( 1 << (attempt - 1) ))
if [ "$backoff" -gt 8 ]; then backoff=8; fi
sleep $(( backoff + RANDOM % 2 ))
done
echo "cascade-state-write: exhausted attempts=10" >&2
echo "::error::Failed to write state via API after 10 attempts" >&2
exit 1
- name: Check for Failures
if: contains(fromJSON('["failure", "cancelled"]'), needs.build-build.result) || (contains(fromJSON('["failure", "cancelled"]'), needs.deploy-app.result) && !(needs.deploy-app-retry-1.result == 'success')) || contains(fromJSON('["failure", "cancelled"]'), needs.deploy-notify.result)
run: |
echo "One or more critical callbacks failed or were cancelled"
exit 1