Build and Release (Matrix) #19
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
| name: Build and Release (Matrix) | |
| on: | |
| push: | |
| tags: | |
| - "**" | |
| workflow_dispatch: | |
| inputs: | |
| release_version: | |
| description: "Release version (e.g., v1.0.0 - leave empty for auto-increment)" | |
| required: false | |
| type: string | |
| wildduck_version: | |
| description: "Wildduck version (leave empty for latest)" | |
| required: false | |
| type: string | |
| zonemta_version: | |
| description: "Zone-MTA version (leave empty for latest)" | |
| required: false | |
| type: string | |
| zonemta_template_version: | |
| description: "Zone-MTA Template version (leave empty for latest)" | |
| required: false | |
| type: string | |
| haraka_version: | |
| description: "Haraka version (leave empty for latest)" | |
| required: false | |
| type: string | |
| jobs: | |
| determine-version: | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.set_version.outputs.version }} | |
| steps: | |
| - name: Determine release version | |
| id: set_version | |
| run: | | |
| if [ -n "${{ inputs.release_version }}" ]; then | |
| # Use provided version | |
| VERSION="${{ inputs.release_version }}" | |
| STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ | |
| -H "Authorization: Bearer $GITHUB_TOKEN" \ | |
| -H "Accept: application/vnd.github+json" \ | |
| https://api.github.com/repos/${{ github.repository }}/git/refs/tags/${VERSION}) | |
| if [ "$STATUS" = "200" ]; then | |
| echo "❌ Error: Tag $VERSION already exists" | |
| exit 1 | |
| fi | |
| echo "Using manual version: $VERSION" | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| else | |
| # Get latest release and increment patch version | |
| LATEST_RELEASE=$(curl -s https://api.github.com/repos/${{ github.repository }}/releases/latest \ | |
| | jq -r '.tag_name') | |
| if [ -z "$LATEST_RELEASE" ] || [ "$LATEST_RELEASE" = "null" ]; then | |
| # No releases yet, start with v1.0.0-dispatch | |
| NEW_VERSION="v1.0.0-dispatch" | |
| else | |
| # Remove 'v' prefix and any existing suffix | |
| BASE_VERSION=$(echo "$LATEST_RELEASE" | sed 's/^v//' | sed 's/-dispatch$//') | |
| # Split version into components | |
| IFS='.' read -r MAJOR MINOR PATCH <<< "$BASE_VERSION" | |
| # Increment patch version | |
| PATCH=$((PATCH + 1)) | |
| # Create new version with -dispatch suffix | |
| NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}-dispatch" | |
| fi | |
| echo "Auto-generated version: $NEW_VERSION" | |
| echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT | |
| fi | |
| build: | |
| needs: [determine-version] | |
| if: always() && (github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && needs.determine-version.result == 'success')) | |
| runs-on: ubuntu-latest | |
| strategy: | |
| matrix: | |
| project: | |
| - name: wildduck | |
| repo: zone-eu/wildduck | |
| ref: master | |
| - name: zone-mta | |
| repo: zone-eu/zone-mta | |
| ref: master | |
| - name: zone-mta-template | |
| repo: zone-eu/zone-mta-template | |
| ref: master | |
| - name: haraka | |
| repo: haraka/Haraka | |
| ref: master | |
| steps: | |
| - name: Checkout workflow repo | |
| uses: actions/checkout@v6 | |
| - name: Get version for ${{ matrix.project.name }} | |
| id: get_version | |
| run: | | |
| # Check if explicit version is provided via workflow input | |
| case "${{ matrix.project.name }}" in | |
| wildduck) | |
| EXPLICIT_VERSION="${{ inputs.wildduck_version }}" | |
| ;; | |
| zone-mta) | |
| EXPLICIT_VERSION="${{ inputs.zonemta_version }}" | |
| ;; | |
| zone-mta-template) | |
| EXPLICIT_VERSION="${{ inputs.zonemta_template_version }}" | |
| ;; | |
| haraka) | |
| EXPLICIT_VERSION="${{ inputs.haraka_version }}" | |
| ;; | |
| esac | |
| if [ -n "$EXPLICIT_VERSION" ]; then | |
| echo "Using explicit version: $EXPLICIT_VERSION" | |
| echo "tag=$EXPLICIT_VERSION" >> $GITHUB_OUTPUT | |
| else | |
| # Fetch latest tag | |
| LATEST_TAG=$( | |
| curl -s "https://api.github.com/repos/${{ matrix.project.repo }}/tags?per_page=100" | | |
| jq -r '.[].name' | | |
| grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | | |
| sort -V | | |
| tail -n 1 | |
| ) | |
| if [ -z "$LATEST_TAG" ] || [ "$LATEST_TAG" = "null" ]; then | |
| echo "No tags found, falling back to master" | |
| LATEST_TAG="${{ matrix.project.ref }}" | |
| fi | |
| echo "tag=$LATEST_TAG" >> $GITHUB_OUTPUT | |
| echo "Using tag/ref: $LATEST_TAG for ${{ matrix.project.name }}" | |
| fi | |
| - name: Checkout ${{ matrix.project.name }} | |
| uses: actions/checkout@v6 | |
| with: | |
| repository: ${{ matrix.project.repo }} | |
| ref: ${{ steps.get_version.outputs.tag }} | |
| path: ${{ matrix.project.name }} | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: "24" | |
| - name: Install dependencies | |
| run: npm i --omit=dev | |
| working-directory: ${{ matrix.project.name }} | |
| - name: Install plugins for ${{ matrix.project.name }} | |
| run: | | |
| PLUGIN_FILE="${GITHUB_WORKSPACE}/plugins/${{ matrix.project.name }}.txt" | |
| if [ -f "$PLUGIN_FILE" ]; then | |
| echo "📦 Installing plugins for ${{ matrix.project.name }}..." | |
| plugin_count=0 | |
| while IFS= read -r plugin || [ -n "$plugin" ]; do | |
| # Skip empty lines and comments | |
| if [ -z "$plugin" ] || [[ "$plugin" =~ ^#.* ]]; then | |
| continue | |
| fi | |
| # Trim whitespace | |
| plugin=$(echo "$plugin" | xargs) | |
| echo "Installing: $plugin" | |
| if npm install --save-exact "$plugin"; then | |
| plugin_count=$((plugin_count + 1)) | |
| echo "✓ Successfully installed $plugin" | |
| else | |
| echo "✗ Failed to install $plugin" | |
| exit 1 | |
| fi | |
| done < "$PLUGIN_FILE" | |
| echo "✅ Installed $plugin_count plugins successfully" | |
| else | |
| echo "❌ No plugin file found, skipping plugin installation" | |
| fi | |
| working-directory: ${{ matrix.project.name }} | |
| - name: Generate plugin summary for release notes | |
| run: | | |
| PLUGIN_FILE="${GITHUB_WORKSPACE}/plugins/${{ matrix.project.name }}.txt" | |
| SUMMARY_FILE="${GITHUB_WORKSPACE}/${{ matrix.project.name }}-plugins-summary.txt" | |
| : > "$SUMMARY_FILE" | |
| if [ -f "$PLUGIN_FILE" ]; then | |
| while IFS= read -r plugin || [ -n "$plugin" ]; do | |
| if [ -z "$plugin" ] || [[ "$plugin" =~ ^#.* ]]; then | |
| continue | |
| fi | |
| plugin=$(echo "$plugin" | xargs) | |
| version=$( | |
| PLUGIN_NAME="$plugin" \ | |
| node -p "const pkg=require('./package.json'); const deps={...(pkg.dependencies||{}), ...(pkg.optionalDependencies||{})}; deps[process.env.PLUGIN_NAME] || ''" | |
| ) | |
| if [ -z "$version" ]; then | |
| version="not-installed" | |
| fi | |
| printf "%s|%s\n" "$plugin" "$version" >> "$SUMMARY_FILE" | |
| done < "$PLUGIN_FILE" | |
| else | |
| echo "__NO_PLUGIN_FILE__|n/a" > "$SUMMARY_FILE" | |
| fi | |
| if [ ! -s "$SUMMARY_FILE" ]; then | |
| echo "__NO_PLUGINS__|n/a" > "$SUMMARY_FILE" | |
| fi | |
| working-directory: ${{ matrix.project.name }} | |
| - name: Create archives | |
| run: | | |
| cd ${{ matrix.project.name }} | |
| zip -q -r ../${{ matrix.project.name }}-${{ steps.get_version.outputs.tag }}.zip . -x "*.git" "*.git/*" | |
| tar --exclude='.git' --exclude='.git/*' -czf ../${{ matrix.project.name }}-${{ steps.get_version.outputs.tag }}.tar.gz . | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@v5 | |
| with: | |
| name: ${{ matrix.project.name }}-build | |
| path: | | |
| ${{ matrix.project.name }}-${{ steps.get_version.outputs.tag }}.zip | |
| ${{ matrix.project.name }}-${{ steps.get_version.outputs.tag }}.tar.gz | |
| ${{ matrix.project.name }}-plugins-summary.txt | |
| create-release: | |
| needs: [determine-version, build] | |
| if: always() && needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v6 | |
| if: github.event_name == 'workflow_dispatch' | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v6 | |
| with: | |
| path: artifacts | |
| - name: Get release version | |
| id: get_release_version | |
| run: | | |
| if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | |
| echo "version=${{ needs.determine-version.outputs.version }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "version=${{ github.ref_name }}" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Create tag for workflow_dispatch | |
| if: github.event_name == 'workflow_dispatch' | |
| run: | | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git tag ${{ steps.get_release_version.outputs.version }} | |
| git push origin ${{ steps.get_release_version.outputs.version }} | |
| - name: Generate release body | |
| env: | |
| RELEASE_TRIGGER: ${{ github.event_name == 'workflow_dispatch' && 'Manual Dispatch' || 'Tag Push' }} | |
| run: | | |
| cat > release-body.md <<EOF | |
| ## Mail Server Stack Build | |
| **Trigger:** ${RELEASE_TRIGGER} | |
| EOF | |
| write_plugin_section() { | |
| local app_name="$1" | |
| local project_slug="$2" | |
| local summary_file="artifacts/${project_slug}-build/${project_slug}-plugins-summary.txt" | |
| { | |
| echo "" | |
| echo "### ${app_name}" | |
| if [ ! -f "$summary_file" ]; then | |
| echo "- Plugin summary not available." | |
| return | |
| fi | |
| while IFS='|' read -r plugin version || [ -n "$plugin" ]; do | |
| case "$plugin" in | |
| "__NO_PLUGIN_FILE__") | |
| echo "- No plugin file configured." | |
| return | |
| ;; | |
| "__NO_PLUGINS__") | |
| echo "- No plugins installed." | |
| return | |
| ;; | |
| esac | |
| echo "- \`$plugin\`: \`$version\`" | |
| done < "$summary_file" | |
| } >> release-body.md | |
| } | |
| write_plugin_section "Wildduck" "wildduck" | |
| write_plugin_section "Zone-MTA" "zone-mta" | |
| write_plugin_section "Zone-MTA Template" "zone-mta-template" | |
| write_plugin_section "Haraka" "haraka" | |
| echo "" >> release-body.md | |
| echo "All projects built with production-ready plugins added." >> release-body.md | |
| - name: Create Release | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| tag_name: ${{ steps.get_release_version.outputs.version }} | |
| files: | | |
| artifacts/*/*.zip | |
| artifacts/*/*.tar.gz | |
| draft: false | |
| prerelease: false | |
| generate_release_notes: false | |
| body_path: release-body.md |