ci: fix heredoc delimiter bugs in release workflow #3
Workflow file for this run
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
| # .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 |