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