Skip to content

ci: fix heredoc delimiter bugs in release workflow #3

ci: fix heredoc delimiter bugs in release workflow

ci: fix heredoc delimiter bugs in release workflow #3

Workflow file for this run

# .github/workflows/release.yml
# Automated release workflow.
# How to trigger: git tag v1.2.3 && git push origin v1.2.3
#
# This workflow:
# 1. Validates the tag format
# 2. Auto-generates a changelog from commits since the last tag
# 3. Creates a GitHub Release with release notes
# 4. Attaches a deployment-ready docker-compose.yml as a release asset
name: 🚀 Release
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+" # v1.0.0
- "v[0-9]+.[0-9]+.[0-9]+-*" # v1.0.0-beta.1, v1.0.0-rc.1
permissions:
contents: write # needed to create releases and upload assets
packages: read
jobs:
# ──────────────────────────────────────────────────────────────────────────
# JOB 1: Validate the tag and extract version info
# ──────────────────────────────────────────────────────────────────────────
validate:
name: ✅ Validate Tag
runs-on: ubuntu-latest
outputs:
version: ${{ steps.parse.outputs.version }}
is_prerelease: ${{ steps.parse.outputs.is_prerelease }}
release_name: ${{ steps.parse.outputs.release_name }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Parse version tag
id: parse
run: |
TAG="${{ github.ref_name }}"
VERSION="${TAG#v}" # strip leading 'v'
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "release_name=PRISM $TAG" >> $GITHUB_OUTPUT
# Detect pre-release (anything with a hyphen: -beta, -rc, -alpha)
if [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-.*$ ]]; then
echo "is_prerelease=true" >> $GITHUB_OUTPUT
echo "::notice::Pre-release detected: $TAG"
else
echo "is_prerelease=false" >> $GITHUB_OUTPUT
echo "::notice::Stable release: $TAG"
fi
- name: Validate semver format
run: |
VERSION="${{ steps.parse.outputs.version }}"
if ! echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.]+)?$'; then
echo "::error::Invalid semantic version: $VERSION"
exit 1
fi
echo "✅ Valid semantic version: $VERSION"
# ──────────────────────────────────────────────────────────────────────────
# JOB 2: Generate changelog from git commits
# ──────────────────────────────────────────────────────────────────────────
changelog:
name: 📝 Generate Changelog
runs-on: ubuntu-latest
needs: validate
outputs:
changelog: ${{ steps.generate.outputs.changelog }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
id: generate
run: |
# Find the previous tag
CURRENT_TAG="${{ github.ref_name }}"
PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -v "$CURRENT_TAG" | head -n 1)
echo "Current tag: $CURRENT_TAG"
echo "Previous tag: $PREVIOUS_TAG"
# Build the range for git log
if [ -z "$PREVIOUS_TAG" ]; then
RANGE="HEAD"
SINCE_MSG="(initial release)"
else
RANGE="${PREVIOUS_TAG}..HEAD"
SINCE_MSG="since ${PREVIOUS_TAG}"
fi
# Generate categorised changelog
CHANGELOG="## 📦 What's Changed ($SINCE_MSG)\n\n"
# Features (feat:, feature:)
FEATURES=$(git log $RANGE --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" \
--grep="^feat\|^feature\|^add\|^new" -i 2>/dev/null || echo "")
if [ -n "$FEATURES" ]; then
CHANGELOG+="### ✨ New Features\n${FEATURES}\n\n"
fi
# Bug fixes (fix:, bug:)
FIXES=$(git log $RANGE --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" \
--grep="^fix\|^bug\|^hotfix\|^patch" -i 2>/dev/null || echo "")
if [ -n "$FIXES" ]; then
CHANGELOG+="### 🐛 Bug Fixes\n${FIXES}\n\n"
fi
# Performance (perf:, optim:)
PERF=$(git log $RANGE --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" \
--grep="^perf\|^optim\|^improve\|^speed" -i 2>/dev/null || echo "")
if [ -n "$PERF" ]; then
CHANGELOG+="### ⚡ Performance\n${PERF}\n\n"
fi
# Infrastructure / CI (ci:, docker:, build:)
CI=$(git log $RANGE --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" \
--grep="^ci\|^docker\|^build\|^deploy\|^infra" -i 2>/dev/null || echo "")
if [ -n "$CI" ]; then
CHANGELOG+="### 🔧 Infrastructure\n${CI}\n\n"
fi
# Everything else
OTHER=$(git log $RANGE --pretty=format:"- %s ([%h](https://github.com/${{ github.repository }}/commit/%H))" \
--invert-grep \
--grep="^feat\|^feature\|^add\|^new\|^fix\|^bug\|^hotfix\|^patch\|^perf\|^optim\|^improve\|^speed\|^ci\|^docker\|^build\|^deploy\|^infra" \
-i 2>/dev/null || echo "")
if [ -n "$OTHER" ]; then
CHANGELOG+="### 📌 Other Changes\n${OTHER}\n\n"
fi
# Docker image reference
CHANGELOG+="---\n\n"
CHANGELOG+="### 🐳 Docker Images\n"
CHANGELOG+="\`\`\`bash\n"
CHANGELOG+="# Pull this release\n"
CHANGELOG+="docker pull ghcr.io/${{ github.repository_owner }}/manufacturing-intelligence/mfg-backend:${{ github.ref_name }}\n"
CHANGELOG+="docker pull ghcr.io/${{ github.repository_owner }}/manufacturing-intelligence/mfg-frontend:${{ github.ref_name }}\n"
CHANGELOG+="\`\`\`\n\n"
CHANGELOG+="**Full Diff:** https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${CURRENT_TAG}"
# Write to file (multiline safe)
printf '%b' "$CHANGELOG" > CHANGELOG_RELEASE.md
# Output using a unique delimiter that cannot appear in changelog content
{
echo "changelog<<CHANGELOG_DELIM"
cat CHANGELOG_RELEASE.md
echo "CHANGELOG_DELIM"
} >> $GITHUB_OUTPUT
- name: Upload changelog artifact
uses: actions/upload-artifact@v4
with:
name: changelog
path: CHANGELOG_RELEASE.md
# ──────────────────────────────────────────────────────────────────────────
# JOB 3: Build release assets (versioned docker-compose)
# ──────────────────────────────────────────────────────────────────────────
build-assets:
name: 📦 Build Release Assets
runs-on: ubuntu-latest
needs: [validate, changelog]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate versioned docker-compose
run: |
VERSION="${{ needs.validate.outputs.version }}"
TAG="${{ github.ref_name }}"
OWNER="${{ github.repository_owner }}"
{
echo "# PRISM ${TAG} — Production-ready docker-compose"
echo "# Generated automatically for release ${TAG}"
echo "# Usage: docker-compose -f docker-compose.release.yml up -d"
echo ""
echo "version: '3.8'"
echo ""
echo "services:"
echo " backend:"
echo " image: ghcr.io/${OWNER}/manufacturing-intelligence/mfg-backend:${TAG}"
echo " container_name: prism-backend-${VERSION}"
echo " ports:"
echo ' - "8000:8000"'
echo " volumes:"
echo " - ./data:/app/data"
echo " - ./models:/app/models"
echo " environment:"
echo " - APP_VERSION=${VERSION}"
echo " restart: unless-stopped"
echo " healthcheck:"
echo ' test: ["CMD", "curl", "-f", "http://localhost:8000/api/health"]'
echo " interval: 30s"
echo " timeout: 10s"
echo " retries: 3"
echo " start_period: 60s"
echo ""
echo " frontend:"
echo " image: ghcr.io/${OWNER}/manufacturing-intelligence/mfg-frontend:${TAG}"
echo " container_name: prism-frontend-${VERSION}"
echo " ports:"
echo ' - "3000:3000"'
echo " environment:"
echo " - NEXT_PUBLIC_API_URL=http://backend:8000"
echo " - APP_VERSION=${VERSION}"
echo " depends_on:"
echo " backend:"
echo " condition: service_healthy"
echo " restart: unless-stopped"
echo " healthcheck:"
echo ' test: ["CMD", "curl", "-f", "http://localhost:3000"]'
echo " interval: 30s"
echo " timeout: 10s"
echo " retries: 3"
echo " start_period: 30s"
echo ""
echo "networks:"
echo " default:"
echo " name: prism-network-${VERSION}"
} > docker-compose.release.yml
- name: Upload release assets
uses: actions/upload-artifact@v4
with:
name: release-assets
path: docker-compose.release.yml
# ──────────────────────────────────────────────────────────────────────────
# JOB 4: Create the GitHub Release
# ──────────────────────────────────────────────────────────────────────────
release:
name: 🎉 Create GitHub Release
runs-on: ubuntu-latest
needs: [validate, changelog, build-assets]
steps:
- name: Download release assets
uses: actions/download-artifact@v4
with:
name: release-assets
- name: Download changelog
uses: actions/download-artifact@v4
with:
name: changelog
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
name: ${{ needs.validate.outputs.release_name }}
tag_name: ${{ github.ref_name }}
body_path: CHANGELOG_RELEASE.md
prerelease: ${{ needs.validate.outputs.is_prerelease == 'true' }}
draft: false
files: |
docker-compose.release.yml
generate_release_notes: false # we write our own above
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Post release summary
run: |
echo "## 🚀 Release Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Version:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Pre-release:** ${{ needs.validate.outputs.is_prerelease }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Images" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
echo "mfg-backend:${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "mfg-frontend:${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY