Skip to content

Release

Release #126

Workflow file for this run

# AUTO-GENERATED by cascade - DO NOT EDIT MANUALLY
# Regenerate with: cascade generate-workflow --config .github/manifest.yaml
#
# Environment: prod
#
# Release actions:
# create-draft - Creates/updates a draft release with changelog
# prerelease - Publishes as a pre-release (for testing)
# release - Publishes as a full release
#
# Breaking changes:
# If changelog contains breaking changes, release
# will fail unless 'allow_breaking_changes' is checked.
name: Release
on:
workflow_dispatch:
inputs:
release_action:
description: 'Release action to perform'
type: choice
required: true
options:
- create-draft
- prerelease
- release
default: create-draft
allow_breaking_changes:
description: 'Required if releasing breaking changes'
type: boolean
default: false
dry_run:
description: 'Dry run mode'
type: boolean
default: false
concurrency:
group: "${{ github.workflow }}"
cancel-in-progress: false
jobs:
preflight:
name: Pre-flight Check
runs-on: ubuntu-latest
outputs:
has_breaking: ${{ steps.check.outputs.has_breaking }}
can_proceed: ${{ steps.check.outputs.can_proceed }}
source_sha: ${{ steps.validate.outputs.source_sha }}
source_version: ${{ steps.validate.outputs.source_version }}
semver_tag: ${{ steps.semver.outputs.semver_tag }}
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Validate Environment State
id: validate
run: |
# Colorized logging helpers
log_info() { echo -e "\033[36m[INFO]\033[0m $1"; }
log_success() { echo -e "\033[32m[OK]\033[0m $1"; }
log_warn() { echo -e "\033[33m[WARN]\033[0m $1"; }
log_error() { echo -e "\033[31m[ERROR]\033[0m $1"; }
log_info "Validating prod environment state"
MANIFEST_FILE=".github/manifest.yaml"
MANIFEST_KEY="ci"
if [[ ! -f "$MANIFEST_FILE" ]]; then
log_error "$MANIFEST_FILE not found"
exit 1
fi
log_success "Found $MANIFEST_FILE"
SOURCE_SHA=$(yq eval ".$MANIFEST_KEY.state.prod.sha // \"\"" "$MANIFEST_FILE")
SOURCE_VERSION=$(yq eval ".$MANIFEST_KEY.state.prod.version // \"\"" "$MANIFEST_FILE")
if [[ -z "$SOURCE_SHA" || "$SOURCE_SHA" == "null" ]]; then
log_error "No SHA found in state for prod environment"
log_info "Run the build workflow first to create a deployment"
exit 1
fi
log_success "Source SHA: ${SOURCE_SHA:0:7}"
if [[ -z "$SOURCE_VERSION" || "$SOURCE_VERSION" == "null" ]]; then
log_warn "No version found - using SHA as version"
SOURCE_VERSION="$SOURCE_SHA"
else
log_success "Source version: $SOURCE_VERSION"
fi
echo "source_sha=$SOURCE_SHA" >> "$GITHUB_OUTPUT"
echo "source_version=$SOURCE_VERSION" >> "$GITHUB_OUTPUT"
- name: Calculate Semver Tag
id: semver
env:
SOURCE_VERSION: ${{ steps.validate.outputs.source_version }}
run: |
# Strip RC suffix (e.g., v1.2.0-rc.3 -> v1.2.0)
SEMVER_TAG=$(echo "$SOURCE_VERSION" | sed 's/-rc\.[0-9]*$//')
echo "semver_tag=$SEMVER_TAG" >> "$GITHUB_OUTPUT"
echo "::notice::Semver tag: $SEMVER_TAG (from $SOURCE_VERSION)"
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/[email protected]
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.6.0-dryrun.4
- name: Check Breaking Changes
id: check
env:
SOURCE_SHA: ${{ steps.validate.outputs.source_sha }}
ALLOW_BREAKING: ${{ github.event.inputs.allow_breaking_changes }}
run: |
# Colorized logging helpers
log_info() { echo -e "\033[36m[INFO]\033[0m $1"; }
log_success() { echo -e "\033[32m[OK]\033[0m $1"; }
log_warn() { echo -e "\033[33m[WARN]\033[0m $1"; }
log_error() { echo -e "\033[31m[ERROR]\033[0m $1"; }
log_decision() { echo -e "\033[35m[DECISION]\033[0m $1"; }
log_info "Checking for breaking changes"
# Get latest release SHA for comparison
MANIFEST_FILE=".github/manifest.yaml"
MANIFEST_KEY="ci"
LATEST_SHA=$(yq eval ".$MANIFEST_KEY.latest_release.sha // \"\"" "$MANIFEST_FILE" 2>/dev/null || echo "")
if [[ -z "$LATEST_SHA" || "$LATEST_SHA" == "null" ]]; then
log_info "No previous release found - using initial commit"
LATEST_SHA=$(git rev-list --max-parents=0 HEAD | tail -n 1)
else
log_info "Comparing against previous release: ${LATEST_SHA:0:7}"
fi
# Generate changelog and check for breaking changes
log_info "Generating changelog to detect breaking changes..."
RESULT=$(cascade generate-changelog --base-sha "$LATEST_SHA" --head-sha "$SOURCE_SHA" --repo "${{ github.repository }}")
HAS_BREAKING=$(echo "$RESULT" | jq -r '.has_breaking // false')
echo "has_breaking=$HAS_BREAKING" >> "$GITHUB_OUTPUT"
# Check if we can proceed
if [[ "$HAS_BREAKING" == "true" ]]; then
log_warn "Breaking changes detected!"
if [[ "$ALLOW_BREAKING" == "true" ]]; then
log_decision "'allow_breaking_changes' is checked - proceeding"
echo "can_proceed=true" >> "$GITHUB_OUTPUT"
else
log_error "Breaking changes not allowed - check 'allow_breaking_changes' to proceed"
echo "can_proceed=false" >> "$GITHUB_OUTPUT"
fi
else
log_success "No breaking changes detected"
echo "can_proceed=true" >> "$GITHUB_OUTPUT"
fi
- name: Fail if Cannot Proceed
if: steps.check.outputs.can_proceed == 'false'
run: exit 1
release:
name: Release
needs: preflight
if: ${{ github.event.inputs.dry_run != 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Setup CLI
uses: stablekernel/cascade/.github/actions/[email protected]
with:
token: ${{ secrets.CASCADE_STATE_TOKEN }}
version: v0.6.0-dryrun.4
- name: Generate Changelog
id: changelog
env:
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
run: |
MANIFEST_FILE=".github/manifest.yaml"
MANIFEST_KEY="ci"
# Get latest release SHA for changelog
LATEST_SHA=$(yq eval ".$MANIFEST_KEY.latest_release.sha // \"\"" "$MANIFEST_FILE" 2>/dev/null || echo "")
if [[ -z "$LATEST_SHA" || "$LATEST_SHA" == "null" ]]; then
LATEST_SHA=$(git rev-list --max-parents=0 HEAD | tail -n 1)
fi
RESULT=$(cascade generate-changelog --base-sha "$LATEST_SHA" --head-sha "$SOURCE_SHA" --repo "${{ github.repository }}")
echo "changelog<<EOF" >> "$GITHUB_OUTPUT"
echo "$RESULT" | jq -r '.changelog' >> "$GITHUB_OUTPUT"
echo "EOF" >> "$GITHUB_OUTPUT"
- name: Create Draft Release
if: ${{ github.event.inputs.release_action == 'create-draft' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: update
environment: draft
sha: ${{ needs.preflight.outputs.source_sha }}
tag: ${{ needs.preflight.outputs.semver_tag }}
changelog: ${{ steps.changelog.outputs.changelog }}
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Create Prerelease
if: ${{ github.event.inputs.release_action == 'prerelease' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: prerelease
environment: prerelease
sha: ${{ needs.preflight.outputs.source_sha }}
tag: ${{ needs.preflight.outputs.source_version }}
new_tag: ${{ needs.preflight.outputs.semver_tag }}
changelog: ${{ steps.changelog.outputs.changelog }}
token: ${{ secrets.CASCADE_STATE_TOKEN }}
- name: Publish Release
if: ${{ github.event.inputs.release_action == 'release' }}
uses: ./.github/actions/manage-release
with:
repo: ${{ github.repository }}
action: publish
environment: released
sha: ${{ needs.preflight.outputs.source_sha }}
tag: ${{ needs.preflight.outputs.semver_tag }}
delete_tag: ${{ needs.preflight.outputs.source_version }}
changelog: ${{ steps.changelog.outputs.changelog }}
token: ${{ secrets.CASCADE_STATE_TOKEN }}
finalize:
name: Finalize
needs: [preflight, release]
if: always() && needs.preflight.result == 'success' && github.event.inputs.release_action == 'release'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Update Latest Release State
if: ${{ github.event.inputs.dry_run != 'true' && needs.release.result == 'success' }}
env:
GH_TOKEN: ${{ secrets.CASCADE_STATE_TOKEN }}
SEMVER_TAG: ${{ needs.preflight.outputs.semver_tag }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
run: |
MANIFEST_FILE=".github/manifest.yaml"
MANIFEST_KEY="ci"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
BRANCH="${GITHUB_REF##refs/heads/}"
BRANCH="${BRANCH:-main}"
apply_release_state_edits() {
TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ)
yq eval -i ".$MANIFEST_KEY.latest_release.version = \"$SEMVER_TAG\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.latest_release.sha = \"$SOURCE_SHA\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.latest_release.released_on = \"$TIMESTAMP\"" "$MANIFEST_FILE"
yq eval -i ".$MANIFEST_KEY.latest_release.released_by = \"${{ github.triggering_actor }}\"" "$MANIFEST_FILE"
}
echo "Updating latest_release state"
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; do
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
apply_release_state_edits
if git diff --quiet "$MANIFEST_FILE"; then
echo "No latest_release state changes"
exit 0
fi
git add "$MANIFEST_FILE"
git commit -m "chore: update latest_release state
Version: $SEMVER_TAG"
if git push origin "HEAD:$BRANCH"; then
echo "Pushed latest_release state on attempt $attempt"
exit 0
fi
echo "Push attempt $attempt rejected (likely concurrent run); retrying..." >&2
sleep $((RANDOM % 5 + 2))
done
echo "::error::Failed to push state after 5 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; do
git fetch origin "$BRANCH"
git reset --hard "origin/$BRANCH"
apply_release_state_edits
if git diff --quiet "$MANIFEST_FILE"; then
echo "No latest_release state changes"
exit 0
fi
CONTENT_B64=$(base64 -w0 "$MANIFEST_FILE" 2>/dev/null || base64 "$MANIFEST_FILE" | tr -d '\n')
CURRENT_SHA=$(gh api "repos/${{ github.repository }}/contents/$MANIFEST_FILE?ref=$BRANCH" --jq '.sha' 2>/dev/null || true)
API_ARGS=("repos/${{ github.repository }}/contents/$MANIFEST_FILE" -X PUT
-f "message=chore: update latest_release state Version: $SEMVER_TAG"
-f "content=$CONTENT_B64"
-f "branch=$BRANCH"
-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 [[ -n "$CURRENT_SHA" ]]; then
API_ARGS+=(-f "sha=$CURRENT_SHA")
fi
if gh api "${API_ARGS[@]}" >/dev/null; then
echo "Pushed latest_release state via API on attempt $attempt"
exit 0
fi
echo "State write attempt $attempt failed (likely concurrent run); retrying..." >&2
sleep $((RANDOM % 5 + 2))
done
echo "::error::Failed to write state via API after 5 attempts" >&2
exit 1
- name: Summary
env:
RELEASE_ACTION: ${{ github.event.inputs.release_action }}
SEMVER_TAG: ${{ needs.preflight.outputs.semver_tag }}
SOURCE_SHA: ${{ needs.preflight.outputs.source_sha }}
DRY_RUN: ${{ github.event.inputs.dry_run }}
run: |
{
echo "## Release Complete"
echo ""
echo "| Property | Value |"
echo "|----------|-------|"
echo "| Action | $RELEASE_ACTION |"
echo "| Version | $SEMVER_TAG |"
echo "| SHA | \`$SOURCE_SHA\` |"
if [[ "$DRY_RUN" == "true" ]]; then
echo "| Mode | **DRY RUN** |"
fi
} >> "$GITHUB_STEP_SUMMARY"