Release-Automation-25190087487 #201
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: Release Automation | |
| run-name: Release-Automation-${{ github.run_id }} | |
| env: | |
| SCHEDULE_FILE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.schedule_file) || 'release-schedule.md' }} | |
| LOCAL_SCHEDULE_FILE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.local_schedule_file) || '' }} | |
| SCHEDULE_REPO: microsoft/Microsoft-365-Agents-Toolkit-Release-Schedule | |
| RELEASE_VERSION: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.release_version) || '' }} | |
| DRY_RUN: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run) || 'false' }} | |
| CREATE_BRANCH: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.create_branch) || 'true' }} | |
| RERUN_CD_ONLY: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.rerun_cd_only) || 'false' }} | |
| GO_PRODUCT: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.go_product) || 'false' }} | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| schedule_file: | |
| description: "Path to release schedule file (relative to schedule repo root)" | |
| required: false | |
| default: "release-schedule.md" | |
| local_schedule_file: | |
| description: "Optional: Path to a local schedule file in this repo (for testing)." | |
| required: false | |
| default: "" | |
| release_version: | |
| description: "Optional: target release Version to process (e.g., 6.5.6). If empty, selects by Cut Bits Date (UTC+8 today)." | |
| required: false | |
| default: "" | |
| rerun_cd_only: | |
| description: "Rerun CD only: select latest pending release (not canceled), skip PR creation/merge checks, and trigger CD directly (useful for regenerating artifacts after fixes on release branch)." | |
| type: boolean | |
| default: false | |
| go_product: | |
| description: "When triggering CD, set goproduct=true (publish TTK as product)." | |
| type: boolean | |
| default: false | |
| dry_run: | |
| description: "Dry run mode (will not create branches or trigger CD)" | |
| type: boolean | |
| default: false | |
| create_branch: | |
| description: "Create release branch if it doesn't exist" | |
| type: boolean | |
| default: true | |
| schedule: | |
| - cron: "0 21 * * *" # Run daily at 05:00 (UTC+8) => 21:00 UTC | |
| pull_request: | |
| types: [closed] | |
| branches: | |
| - "release/**" | |
| permissions: | |
| contents: read | |
| jobs: | |
| parse-schedule: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name != 'pull_request' }} | |
| outputs: | |
| releases: ${{ steps.parse.outputs.releases }} | |
| releases_count: ${{ steps.parse.outputs.releases_count }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| - name: Checkout release schedule repo | |
| if: ${{ env.LOCAL_SCHEDULE_FILE == '' }} | |
| uses: actions/checkout@v3 | |
| with: | |
| repository: ${{ env.SCHEDULE_REPO }} | |
| token: ${{ secrets.RELEASE_AUTOMATION_TOKEN }} | |
| path: schedule-repo | |
| sparse-checkout: | | |
| ${{ env.SCHEDULE_FILE }} | |
| sparse-checkout-cone-mode: false | |
| - name: Set up Python | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.x' | |
| - name: Parse release schedule | |
| id: parse | |
| run: | | |
| # Determine which schedule file to use | |
| if [ -n "${{ env.LOCAL_SCHEDULE_FILE }}" ]; then | |
| SCHEDULE_PATH="${{ env.LOCAL_SCHEDULE_FILE }}" | |
| echo "📁 Using local schedule file: $SCHEDULE_PATH" | |
| else | |
| SCHEDULE_PATH="schedule-repo/${{ env.SCHEDULE_FILE }}" | |
| echo "📁 Using schedule file from private repo: $SCHEDULE_PATH" | |
| fi | |
| # Parse schedule-driven release parameters | |
| python .github/scripts/parse_release_schedule.py "$SCHEDULE_PATH" > releases.json | |
| # Process all VS Code entries (prerelease, stable, hotfix) | |
| cat releases.json | jq '[.[] | select(((.product | ascii_downcase) == "vsc") or (.product | ascii_downcase | contains("vs code")))]' > releases.filtered.json | |
| mv releases.filtered.json releases.json | |
| echo "📋 Parsed Release Schedule:" | |
| cat releases.json | python -m json.tool | |
| # Set outputs | |
| RELEASES=$(cat releases.json | jq -c '.') | |
| RELEASES_COUNT=$(cat releases.json | jq 'length') | |
| echo "releases=$RELEASES" >> $GITHUB_OUTPUT | |
| echo "releases_count=$RELEASES_COUNT" >> $GITHUB_OUTPUT | |
| echo "" | |
| echo "Found $RELEASES_COUNT pending release(s)" | |
| - name: Upload releases configuration | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: releases-config | |
| path: releases.json | |
| select-release: | |
| needs: parse-schedule | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name != 'pull_request' }} | |
| outputs: | |
| found: ${{ steps.select.outputs.found }} | |
| reason: ${{ steps.select.outputs.reason }} | |
| product: ${{ steps.select.outputs.product }} | |
| release_type: ${{ steps.select.outputs.release_type }} | |
| version: ${{ steps.select.outputs.version }} | |
| cut_date: ${{ steps.select.outputs.cut_date }} | |
| branch: ${{ steps.select.outputs.branch }} | |
| preid: ${{ steps.select.outputs.preid }} | |
| series: ${{ steps.select.outputs.series }} | |
| vsrelease: ${{ steps.select.outputs.vsrelease }} | |
| steps: | |
| - name: Download releases configuration | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: releases-config | |
| - name: Select release (by version or Cut Bits Date) | |
| id: select | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| VERSION_INPUT="${{ env.RELEASE_VERSION }}" | |
| RERUN_CD_ONLY="${{ env.RERUN_CD_ONLY }}" | |
| TODAY_UTC8=$(date -u -d '+8 hours' +%Y-%m-%d) | |
| if [ -n "$VERSION_INPUT" ]; then | |
| REASON="version=$VERSION_INPUT" | |
| RELEASE=$(jq -c --arg v "$VERSION_INPUT" '[.[] | select(.version == $v)] | .[0]' releases.json) | |
| elif [ "$RERUN_CD_ONLY" = "true" ]; then | |
| REASON="cut_date_window=$TODAY_UTC8 (UTC+8)" | |
| RELEASE=$(jq -c --arg d "$TODAY_UTC8" ' | |
| [ .[] | select((.cut_date // "") != "") ] | |
| | sort_by(.cut_date) as $s | |
| | if ($s|length) == 0 then null | |
| else | |
| ( | |
| [ range(0; ($s|length)) as $i | |
| | select( | |
| ($s[$i].cut_date <= $d) | |
| and ( | |
| ($i + 1) >= ($s|length) | |
| or ($s[$i+1].cut_date > $d) | |
| ) | |
| ) | |
| | $s[$i] | |
| ] | |
| | .[0] | |
| ) | |
| // ( | |
| [ $s[] | select(.cut_date > $d) ] | |
| | .[0] | |
| ) | |
| // $s[-1] | |
| end | |
| ' releases.json) | |
| else | |
| REASON="cut_date=$TODAY_UTC8 (UTC+8)" | |
| RELEASE=$(jq -c --arg d "$TODAY_UTC8" '[.[] | select(.cut_date == $d)] | .[0]' releases.json) | |
| fi | |
| if [ "$RELEASE" = "null" ] || [ -z "$RELEASE" ]; then | |
| { | |
| echo "## ℹ️ No release selected" | |
| echo "" | |
| echo "No matching release was found for ${REASON}." | |
| echo "" | |
| echo "Notes:" | |
| echo "- Cut Bits Date must be in ISO format (YYYY-MM-DD)." | |
| echo "- Status values like Cancel/Cancelled/Canceled are ignored by the parser." | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "reason=$REASON" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| PRODUCT=$(echo "$RELEASE" | jq -r '.product') | |
| RELEASE_TYPE=$(echo "$RELEASE" | jq -r '.release_type') | |
| VERSION=$(echo "$RELEASE" | jq -r '.version') | |
| CUT_DATE=$(echo "$RELEASE" | jq -r '.cut_date') | |
| BRANCH=$(echo "$RELEASE" | jq -r '.branch') | |
| PREID=$(echo "$RELEASE" | jq -r '.cd_params.preid') | |
| SERIES=$(echo "$RELEASE" | jq -r '.cd_params.series') | |
| VSRELEASE=$(echo "$RELEASE" | jq -r '.cd_params.vsrelease') | |
| { | |
| echo "## ✅ Selected release" | |
| echo "" | |
| echo "- Reason: ${REASON}" | |
| echo "- Rerun CD only: ${RERUN_CD_ONLY}" | |
| echo "- Product: ${PRODUCT}" | |
| echo "- Release Type: ${RELEASE_TYPE}" | |
| echo "- Version: ${VERSION}" | |
| echo "- Cut Bits Date: ${CUT_DATE}" | |
| echo "- Branch: ${BRANCH}" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| echo "reason=$REASON" >> "$GITHUB_OUTPUT" | |
| echo "product=$PRODUCT" >> "$GITHUB_OUTPUT" | |
| echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "cut_date=$CUT_DATE" >> "$GITHUB_OUTPUT" | |
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | |
| echo "preid=$PREID" >> "$GITHUB_OUTPUT" | |
| echo "series=$SERIES" >> "$GITHUB_OUTPUT" | |
| echo "vsrelease=$VSRELEASE" >> "$GITHUB_OUTPUT" | |
| review-releases: | |
| needs: parse-schedule | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name != 'pull_request' && needs.parse-schedule.outputs.releases_count > 0 }} | |
| steps: | |
| - name: Download releases configuration | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: releases-config | |
| - name: Display releases for review | |
| run: | | |
| { | |
| echo "# 📦 Release Automation Plan" | |
| echo "" | |
| echo "The following releases are ready for processing:" | |
| echo "" | |
| jq -r 'to_entries[] | "## Release \(.key + 1): \(.value.product) \(.value.version)\n\n**Release Type:** \(.value.release_type)\n**Branch:** `\(.value.branch)`\n**Cut Date:** \(.value.cut_date)\n\n### CD Pipeline Parameters:\n- **preid:** `\(.value.cd_params.preid)`\n- **series:** `\(.value.cd_params.series)`\n- **vsrelease:** `\(.value.cd_params.vsrelease)`\n\n---\n"' releases.json | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| prepare-release: | |
| needs: | |
| - parse-schedule | |
| - select-release | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name != 'pull_request' && needs.select-release.outputs.found == 'true' }} | |
| env: | |
| APP_GITHUB_APP_ID: ${{ vars.APP_GITHUB_APP_ID }} | |
| APP_GITHUB_APP_PRIVATE_KEY: ${{ secrets.APP_GITHUB_APP_PRIVATE_KEY }} | |
| MAIL_CLIENT_ID: ${{ secrets.MAIL_CLIENT_ID }} | |
| MAIL_CLIENT_SECRET: ${{ secrets.MAIL_CLIENT_SECRET }} | |
| MAIL_TENANT_ID: ${{ secrets.MAIL_TENANT_ID }} | |
| outputs: | |
| branch: ${{ steps.config.outputs.branch }} | |
| product: ${{ steps.config.outputs.product }} | |
| release_type: ${{ steps.config.outputs.release_type }} | |
| version: ${{ steps.config.outputs.version }} | |
| preid: ${{ steps.config.outputs.preid }} | |
| series: ${{ steps.config.outputs.series }} | |
| vsrelease: ${{ steps.config.outputs.vsrelease }} | |
| is_prerelease: ${{ steps.config.outputs.is_prerelease }} | |
| is_stable: ${{ steps.config.outputs.is_stable }} | |
| is_hotfix: ${{ steps.config.outputs.is_hotfix }} | |
| pr_needed: ${{ steps.sync_pr.outputs.pr_needed }} | |
| pr_url: ${{ steps.sync_pr.outputs.pr_url }} | |
| pr_number: ${{ steps.sync_pr.outputs.pr_number }} | |
| branch_created: ${{ steps.create_branch.outputs.created }} | |
| auto_cd: ${{ steps.decide_auto_cd.outputs.auto_cd }} | |
| steps: | |
| - id: generate-token | |
| if: ${{ env.APP_GITHUB_APP_ID != '' && env.APP_GITHUB_APP_PRIVATE_KEY != '' }} | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ env.APP_GITHUB_APP_ID }} | |
| private-key: ${{ env.APP_GITHUB_APP_PRIVATE_KEY }} | |
| - name: Checkout repository | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-depth: 0 | |
| token: ${{ steps.generate-token.outputs.token || github.token }} | |
| - name: Download releases configuration | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: releases-config | |
| - name: Set selected release outputs | |
| id: config | |
| shell: bash | |
| run: | | |
| PRODUCT='${{ needs.select-release.outputs.product }}' | |
| RELEASE_TYPE='${{ needs.select-release.outputs.release_type }}' | |
| VERSION='${{ needs.select-release.outputs.version }}' | |
| BRANCH='${{ needs.select-release.outputs.branch }}' | |
| PREID='${{ needs.select-release.outputs.preid }}' | |
| SERIES='${{ needs.select-release.outputs.series }}' | |
| VSRELEASE='${{ needs.select-release.outputs.vsrelease }}' | |
| echo "Selected release:" | |
| echo " Product: $PRODUCT" | |
| echo " Release Type: $RELEASE_TYPE" | |
| echo " Version: $VERSION" | |
| echo " Branch: $BRANCH" | |
| # Derive release type flags | |
| RELEASE_TYPE_LOWER=$(echo "$RELEASE_TYPE" | tr '[:upper:]' '[:lower:]') | |
| IS_PRERELEASE=false; IS_STABLE=false; IS_HOTFIX=false | |
| case "$RELEASE_TYPE_LOWER" in | |
| *prerelease*) IS_PRERELEASE=true ;; | |
| *hotfix*) IS_HOTFIX=true ;; | |
| *stable*) IS_STABLE=true ;; | |
| *) echo "❌ Unknown release type: $RELEASE_TYPE" >&2; exit 1 ;; | |
| esac | |
| # Hotfix releases only support rerun_cd_only mode | |
| RERUN_CD_ONLY="${{ env.RERUN_CD_ONLY }}" | |
| if [ "$IS_HOTFIX" = "true" ] && [ "$RERUN_CD_ONLY" != "true" ]; then | |
| echo "❌ Hotfix releases only support rerun_cd_only mode. Set rerun_cd_only=true and retry." >&2 | |
| exit 1 | |
| fi | |
| # For stable releases, derive source_branch from the last prerelease entry in the schedule | |
| SOURCE_BRANCH="" | |
| if [ "$IS_STABLE" = "true" ] && [ "$RERUN_CD_ONLY" != "true" ]; then | |
| SOURCE_BRANCH=$(jq -r --arg v "$VERSION" ' | |
| [ .[] | select(.release_type | ascii_downcase | contains("prerelease")) ] | |
| | sort_by(.cut_date) | |
| | last | |
| | .branch // empty | |
| ' releases.json) | |
| if [ -z "$SOURCE_BRANCH" ]; then | |
| echo "⚠️ No previous prerelease found in schedule. Falling back to dev as source branch." | |
| SOURCE_BRANCH="dev" | |
| fi | |
| echo " Source Branch (derived): $SOURCE_BRANCH" | |
| fi | |
| echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" | |
| echo "product=$PRODUCT" >> "$GITHUB_OUTPUT" | |
| echo "release_type=$RELEASE_TYPE" >> "$GITHUB_OUTPUT" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "preid=$PREID" >> "$GITHUB_OUTPUT" | |
| echo "series=$SERIES" >> "$GITHUB_OUTPUT" | |
| echo "vsrelease=$VSRELEASE" >> "$GITHUB_OUTPUT" | |
| echo "source_branch=$SOURCE_BRANCH" >> "$GITHUB_OUTPUT" | |
| echo "is_prerelease=$IS_PRERELEASE" >> "$GITHUB_OUTPUT" | |
| echo "is_stable=$IS_STABLE" >> "$GITHUB_OUTPUT" | |
| echo "is_hotfix=$IS_HOTFIX" >> "$GITHUB_OUTPUT" | |
| - name: Setup git | |
| run: | | |
| git config --global user.name 'github-actions[bot]' | |
| git config --global user.email 'github-actions[bot]@users.noreply.github.com' | |
| - name: Check if branch exists | |
| id: check_branch | |
| run: | | |
| BRANCH="${{ steps.config.outputs.branch }}" | |
| if git show-ref --verify --quiet "refs/heads/$BRANCH"; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "✅ Branch $BRANCH already exists locally" | |
| elif git ls-remote --heads origin "$BRANCH" | grep -q "$BRANCH"; then | |
| echo "exists=true" >> $GITHUB_OUTPUT | |
| echo "✅ Branch $BRANCH exists on remote" | |
| git fetch origin "$BRANCH:$BRANCH" | |
| else | |
| echo "exists=false" >> $GITHUB_OUTPUT | |
| echo "ℹ️ Branch $BRANCH does not exist" | |
| fi | |
| - name: Create PR to merge dev into existing release branch | |
| id: sync_pr | |
| if: ${{ steps.check_branch.outputs.exists == 'true' && env.DRY_RUN == 'false' && env.RERUN_CD_ONLY == 'false' && steps.config.outputs.is_prerelease == 'true' }} | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }} | |
| script: | | |
| const product = '${{ steps.config.outputs.product }}'; | |
| const version = '${{ steps.config.outputs.version }}'; | |
| const releaseType = '${{ steps.config.outputs.release_type }}'; | |
| const base = '${{ steps.config.outputs.branch }}'; | |
| const head = 'dev'; | |
| const preid = '${{ steps.config.outputs.preid }}'; | |
| const series = '${{ steps.config.outputs.series }}'; | |
| const vsrelease = '${{ steps.config.outputs.vsrelease }}' === 'true'; | |
| const goproduct = '${{ env.GO_PRODUCT }}' === 'true'; | |
| const workflowRunUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const markerPayload = { product, version, releaseType, branch: base, preid, series, vsrelease, goproduct }; | |
| const marker = `<!-- release-automation-cd-params: ${JSON.stringify(markerPayload)} -->`; | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const label = 'release-automation-sync'; | |
| // Check if dev has new commits not in the release branch | |
| const compare = await github.rest.repos.compareCommits({ owner, repo, base, head }); | |
| const aheadBy = compare.data.ahead_by ?? 0; | |
| if (aheadBy === 0) { | |
| core.notice(`No sync PR needed: '${head}' is not ahead of '${base}'.`); | |
| core.setOutput('pr_needed', 'false'); | |
| core.setOutput('pr_url', ''); | |
| core.setOutput('pr_number', ''); | |
| return; | |
| } | |
| // Avoid creating duplicate PRs | |
| const existing = await github.rest.pulls.list({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| base, | |
| head: `${owner}:${head}`, | |
| per_page: 10, | |
| }); | |
| if (existing.data.length > 0) { | |
| const pr = existing.data[0]; | |
| core.notice(`Sync PR already exists: ${pr.html_url}`); | |
| // Ensure marker + label exist for merge-trigger workflow | |
| const currentBody = pr.body || ''; | |
| const markerRegex = /<!--\s*release-automation-cd-params:[\s\S]*?-->/; | |
| let updatedBody = currentBody; | |
| if (markerRegex.test(currentBody)) { | |
| updatedBody = currentBody.replace(markerRegex, marker); | |
| } else { | |
| updatedBody = [ | |
| currentBody, | |
| '', | |
| `Release Automation Run: ${workflowRunUrl}`, | |
| marker, | |
| ].join('\n'); | |
| } | |
| if (updatedBody !== currentBody) { | |
| await github.rest.pulls.update({ owner, repo, pull_number: pr.number, body: updatedBody }); | |
| } | |
| try { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number: pr.number, labels: [label] }); | |
| } catch (e) { | |
| core.notice(`Unable to add label '${label}' (may already exist): ${e.message}`); | |
| } | |
| core.setOutput('pr_needed', 'true'); | |
| core.setOutput('pr_url', pr.html_url); | |
| core.setOutput('pr_number', String(pr.number)); | |
| return; | |
| } | |
| const title = `chore: merge dev into ${base} (${product} ${version})`; | |
| const body = [ | |
| 'This PR is created by Release Automation to sync latest changes from `dev` into the release branch.', | |
| '', | |
| `- Product: ${product}`, | |
| `- Release Type: ${releaseType}`, | |
| `- Version: ${version}`, | |
| `- Base (target): \`${base}\``, | |
| `- Head (source): \`${head}\``, | |
| `- Commits ahead: ${aheadBy}`, | |
| '', | |
| `Release Automation Run: ${workflowRunUrl}`, | |
| marker, | |
| '', | |
| 'After merging, approve the Release Automation run to trigger CD.' | |
| ].join('\n'); | |
| const created = await github.rest.pulls.create({ owner, repo, title, head, base, body }); | |
| core.notice(`Created sync PR: ${created.data.html_url}`); | |
| try { | |
| await github.rest.issues.addLabels({ owner, repo, issue_number: created.data.number, labels: [label] }); | |
| } catch (e) { | |
| core.notice(`Unable to add label '${label}': ${e.message}`); | |
| } | |
| core.setOutput('pr_needed', 'true'); | |
| core.setOutput('pr_url', created.data.html_url); | |
| core.setOutput('pr_number', String(created.data.number)); | |
| - name: Create PR in samples repo (merge dev into main) | |
| if: ${{ env.DRY_RUN == 'false' && env.RERUN_CD_ONLY == 'false' && steps.config.outputs.is_hotfix != 'true' }} | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }} | |
| script: | | |
| const owner = 'OfficeDev'; | |
| const repo = 'microsoft-365-agents-toolkit-samples'; | |
| const base = 'main'; | |
| const head = 'dev'; | |
| const product = '${{ steps.config.outputs.product }}'; | |
| const version = '${{ steps.config.outputs.version }}'; | |
| const branch = '${{ steps.config.outputs.branch }}'; | |
| // Check if dev has new commits not in main | |
| const compare = await github.rest.repos.compareCommits({ owner, repo, base, head }); | |
| const aheadBy = compare.data.ahead_by ?? 0; | |
| if (aheadBy === 0) { | |
| core.notice(`No samples sync PR needed: '${head}' is not ahead of '${base}'.`); | |
| return; | |
| } | |
| // Avoid creating duplicate PRs | |
| const existing = await github.rest.pulls.list({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| base, | |
| head: `${owner}:${head}`, | |
| per_page: 10, | |
| }); | |
| if (existing.data.length > 0) { | |
| const pr = existing.data[0]; | |
| core.notice(`Samples sync PR already exists: ${pr.html_url}`); | |
| return; | |
| } | |
| const title = `chore: merge dev into ${base} (samples)`; | |
| const body = [ | |
| 'This PR is created by Release Automation to sync latest changes from `dev` into `main` in the samples repo.', | |
| '', | |
| `Triggered by release: ${product} ${version} (branch: \`${branch}\`)`, | |
| `- Base (target): \`${base}\``, | |
| `- Head (source): \`${head}\``, | |
| `- Commits ahead: ${aheadBy}`, | |
| ].join('\n'); | |
| const created = await github.rest.pulls.create({ owner, repo, title, head, base, body }); | |
| core.notice(`Created samples sync PR: ${created.data.html_url}`); | |
| core.summary | |
| .addHeading('📌 Samples Repo Sync PR', 2) | |
| .addRaw(`\nCreated: ${created.data.html_url}\n`) | |
| .write(); | |
| - name: Create release branch | |
| id: create_branch | |
| if: ${{ steps.check_branch.outputs.exists == 'false' && env.CREATE_BRANCH == 'true' && env.DRY_RUN == 'false' && env.RERUN_CD_ONLY == 'false' && steps.config.outputs.is_hotfix != 'true' }} | |
| run: | | |
| BRANCH="${{ steps.config.outputs.branch }}" | |
| IS_STABLE="${{ steps.config.outputs.is_stable }}" | |
| SOURCE_BRANCH="${{ steps.config.outputs.source_branch }}" | |
| if [ "$IS_STABLE" = "true" ]; then | |
| echo "Creating branch $BRANCH from source branch $SOURCE_BRANCH..." | |
| git fetch origin "$SOURCE_BRANCH" | |
| git checkout "origin/$SOURCE_BRANCH" | |
| git checkout -b "$BRANCH" | |
| else | |
| echo "Creating branch $BRANCH from dev..." | |
| git checkout dev | |
| git pull origin dev | |
| git checkout -b "$BRANCH" | |
| fi | |
| git push origin "$BRANCH" | |
| echo "✅ Branch $BRANCH created successfully" | |
| echo "created=true" >> $GITHUB_OUTPUT | |
| - name: Decide auto-CD for new release branch | |
| id: decide_auto_cd | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| BRANCH="${{ steps.config.outputs.branch }}" | |
| VERSION="${{ steps.config.outputs.version }}" | |
| BRANCH_CREATED="${{ steps.create_branch.outputs.created }}" | |
| IS_STABLE="${{ steps.config.outputs.is_stable }}" | |
| AUTO_CD=false | |
| if [ "$BRANCH_CREATED" = "true" ]; then | |
| if [ "$IS_STABLE" = "true" ]; then | |
| # Stable releases auto-trigger CD on branch creation (no PR merge gate) | |
| AUTO_CD=true | |
| elif [[ "$BRANCH" =~ ^release/6\.([0-9]+)$ ]]; then | |
| # Prerelease: auto-trigger CD only for odd minor branches | |
| MINOR="${BASH_REMATCH[1]}" | |
| if (( MINOR % 2 == 1 )); then | |
| AUTO_CD=true | |
| fi | |
| fi | |
| fi | |
| echo "auto_cd=$AUTO_CD" >> "$GITHUB_OUTPUT" | |
| echo "Auto CD decision: branch=$BRANCH, version=$VERSION, branch_created=$BRANCH_CREATED, is_stable=$IS_STABLE, auto_cd=$AUTO_CD" >> "$GITHUB_STEP_SUMMARY" | |
| - name: Validate branch exists for rerun mode | |
| if: ${{ env.RERUN_CD_ONLY == 'true' && steps.check_branch.outputs.exists != 'true' }} | |
| run: | | |
| echo "❌ Rerun CD only mode is enabled, but branch '${{ steps.config.outputs.branch }}' does not exist." >&2 | |
| echo " Fix the schedule branch value or create the branch, then rerun." >&2 | |
| exit 1 | |
| - name: Compose notification (shown + email payload) | |
| id: notify | |
| run: | | |
| DATE=$(date -u -d '+8 hours' +%Y-%m-%d) | |
| PRODUCT="${{ steps.config.outputs.product }}" | |
| VERSION="${{ steps.config.outputs.version }}" | |
| RELEASE_TYPE="${{ steps.config.outputs.release_type }}" | |
| BRANCH="${{ steps.config.outputs.branch }}" | |
| PREID="${{ steps.config.outputs.preid }}" | |
| SERIES="${{ steps.config.outputs.series }}" | |
| VSRELEASE="${{ steps.config.outputs.vsrelease }}" | |
| SOURCE_BRANCH="${{ steps.config.outputs.source_branch }}" | |
| IS_PRERELEASE="${{ steps.config.outputs.is_prerelease }}" | |
| IS_STABLE="${{ steps.config.outputs.is_stable }}" | |
| IS_HOTFIX="${{ steps.config.outputs.is_hotfix }}" | |
| DRY_RUN="${{ env.DRY_RUN }}" | |
| RERUN_CD_ONLY="${{ env.RERUN_CD_ONLY }}" | |
| BRANCH_EXISTS="${{ steps.check_branch.outputs.exists }}" | |
| BRANCH_CREATED="${{ steps.create_branch.outputs.created }}" | |
| PR_NEEDED_RAW="${{ steps.sync_pr.outputs.pr_needed }}" | |
| PR_URL="${{ steps.sync_pr.outputs.pr_url }}" | |
| # Normalize values | |
| PR_NEEDED="${PR_NEEDED_RAW:-false}" | |
| if [ -z "$BRANCH_EXISTS" ]; then BRANCH_EXISTS="unknown"; fi | |
| if [ -z "$BRANCH_CREATED" ]; then BRANCH_CREATED="false"; fi | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/workflows/release-automation.yml" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| SUBJECT="ATK Release Automation Notification: ${PRODUCT} ${VERSION} (${BRANCH})" | |
| ACTION_REQUIRED="Approve the environment gate (auto-release) to trigger CD." | |
| if [ "$PR_NEEDED" = "true" ]; then | |
| ACTION_REQUIRED="ACTION REQUIRED: approve & merge the sync PR, then approve the environment gate (auto-release) to trigger CD." | |
| fi | |
| if [ "$RERUN_CD_ONLY" = "true" ]; then | |
| ACTION_REQUIRED="RERUN CD ONLY: skip PR/branch actions; approve the environment gate (auto-release) to trigger CD." | |
| fi | |
| if [ "$DRY_RUN" = "true" ]; then | |
| ACTION_REQUIRED="DRY RUN: no changes were made. Re-run with dry_run=false to create branch/PR and proceed." | |
| fi | |
| { | |
| echo "# 📧 Release Automation Notification" | |
| echo "" | |
| echo "**Subject:** ${SUBJECT}" | |
| echo "" | |
| echo "## Release" | |
| echo "- **Product:** ${PRODUCT}" | |
| echo "- **Release Type:** ${RELEASE_TYPE}" | |
| echo "- **Version:** ${VERSION}" | |
| echo "- **Branch:** ${BRANCH}" | |
| echo "" | |
| echo "## CD Parameters" | |
| echo "- **preid:** ${PREID}" | |
| echo "- **series:** ${SERIES}" | |
| echo "- **vsrelease:** ${VSRELEASE}" | |
| echo "" | |
| echo "## Branch / PR" | |
| if [ "$DRY_RUN" = "true" ]; then | |
| echo "- Dry run only (no branch/PR created)." | |
| else | |
| if [ "$BRANCH_CREATED" = "true" ]; then | |
| if [ "$IS_STABLE" = "true" ]; then | |
| echo "- ✅ Created branch from source branch: ${SOURCE_BRANCH}." | |
| else | |
| echo "- ✅ Created branch from dev." | |
| fi | |
| else | |
| echo "- Branch exists: ${BRANCH_EXISTS}" | |
| fi | |
| if [ "$PR_NEEDED" = "true" ]; then | |
| echo "- 🔁 Sync PR required: ${PR_URL}" | |
| else | |
| echo "- Sync PR required: false" | |
| fi | |
| fi | |
| echo "" | |
| echo "## Next steps" | |
| echo "- ${ACTION_REQUIRED}" | |
| echo "- Workflow: ${WORKFLOW_URL}" | |
| echo "- Run: ${RUN_URL}" | |
| } | tee -a "$GITHUB_STEP_SUMMARY" | |
| BODY=$(jq -c -n \ | |
| --arg date "$DATE" \ | |
| --arg product "$PRODUCT" \ | |
| --arg version "$VERSION" \ | |
| --arg release_type "$RELEASE_TYPE" \ | |
| --arg branch "$BRANCH" \ | |
| --arg preid "$PREID" \ | |
| --arg series "$SERIES" \ | |
| --arg vsrelease "$VSRELEASE" \ | |
| --arg dry_run "$DRY_RUN" \ | |
| --arg pr_needed "$PR_NEEDED" \ | |
| --arg pr_url "$PR_URL" \ | |
| --arg workflow_url "$WORKFLOW_URL" \ | |
| --arg run_url "$RUN_URL" \ | |
| --arg action_required "$ACTION_REQUIRED" \ | |
| '{ | |
| date: $date, | |
| product: $product, | |
| version: $version, | |
| release_type: $release_type, | |
| branch: $branch, | |
| cd_params: { preid: $preid, series: $series, vsrelease: $vsrelease }, | |
| dry_run: $dry_run, | |
| pr_needed: $pr_needed, | |
| pr_url: $pr_url, | |
| action_required: $action_required, | |
| workflow_url: $workflow_url, | |
| run_url: $run_url | |
| }') | |
| echo "subject=$SUBJECT" >> $GITHUB_OUTPUT | |
| echo "body=$BODY" >> $GITHUB_OUTPUT | |
| - name: Send email notification | |
| if: ${{ env.DRY_RUN == 'false' && env.MAIL_CLIENT_ID != '' && env.MAIL_CLIENT_SECRET != '' && env.MAIL_TENANT_ID != '' }} | |
| env: | |
| MAIL_CLIENT_ID: ${{ env.MAIL_CLIENT_ID }} | |
| MAIL_CLIENT_SECRET: ${{ env.MAIL_CLIENT_SECRET }} | |
| MAIL_TENANT_ID: ${{ env.MAIL_TENANT_ID }} | |
| TO: ${{ vars.RELEASE_NOTIFICATION_EMAIL || 'M365AgentsToolkitEngineerTeam@microsoft.com' }} | |
| SUBJECT: ${{ steps.notify.outputs.subject }} | |
| BODY: ${{ steps.notify.outputs.body }} | |
| uses: ./.github/actions/send-email-report | |
| trigger-cd: | |
| needs: prepare-release | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name != 'pull_request' && needs.prepare-release.result == 'success' && ((github.event_name == 'workflow_dispatch' && github.event.inputs.dry_run == 'false' && (github.event.inputs.rerun_cd_only == 'true' || needs.prepare-release.outputs.auto_cd == 'true')) || (github.event_name == 'schedule' && needs.prepare-release.outputs.auto_cd == 'true')) }} | |
| outputs: | |
| product: ${{ needs.prepare-release.outputs.product }} | |
| release_type: ${{ needs.prepare-release.outputs.release_type }} | |
| version: ${{ needs.prepare-release.outputs.version }} | |
| branch: ${{ needs.prepare-release.outputs.branch }} | |
| preid: ${{ needs.prepare-release.outputs.preid }} | |
| series: ${{ needs.prepare-release.outputs.series }} | |
| vsrelease: ${{ needs.prepare-release.outputs.vsrelease }} | |
| goproduct: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.go_product) || 'false' }} | |
| env: | |
| APP_GITHUB_APP_ID: ${{ vars.APP_GITHUB_APP_ID }} | |
| APP_GITHUB_APP_PRIVATE_KEY: ${{ secrets.APP_GITHUB_APP_PRIVATE_KEY }} | |
| steps: | |
| - id: generate-token | |
| if: ${{ env.APP_GITHUB_APP_ID != '' && env.APP_GITHUB_APP_PRIVATE_KEY != '' }} | |
| uses: actions/create-github-app-token@v2 | |
| with: | |
| app-id: ${{ env.APP_GITHUB_APP_ID }} | |
| private-key: ${{ env.APP_GITHUB_APP_PRIVATE_KEY }} | |
| - name: Trigger CD workflow | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ steps.generate-token.outputs.token || github.token }} | |
| script: | | |
| const branch = '${{ needs.prepare-release.outputs.branch }}'; | |
| const preid = '${{ needs.prepare-release.outputs.preid }}'; | |
| const series = '${{ needs.prepare-release.outputs.series }}'; | |
| const vsrelease = '${{ needs.prepare-release.outputs.vsrelease }}' === 'true'; | |
| const goproduct = '${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.go_product) || 'false' }}' === 'true'; | |
| const isPrerelease = '${{ needs.prepare-release.outputs.is_prerelease }}' === 'true'; | |
| const finalPreid = (goproduct && !isPrerelease) ? 'stable' : preid; | |
| console.log('Triggering CD workflow with parameters:'); | |
| console.log(` Branch: ${branch}`); | |
| console.log(` preid: ${finalPreid} (original: ${preid}, goproduct: ${goproduct})`); | |
| console.log(` series: ${series}`); | |
| console.log(` vsrelease: ${vsrelease}`); | |
| console.log(` goproduct: ${goproduct}`); | |
| console.log(` run_test_cases: true`); | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'cd.yml', | |
| ref: branch, | |
| inputs: { | |
| preid: finalPreid, | |
| series: series, | |
| vsrelease: vsrelease.toString(), | |
| vstemplate: 'false', | |
| goproduct: goproduct.toString(), | |
| run_test_cases: 'true' | |
| } | |
| }); | |
| core.summary | |
| .addHeading('🚀 CD Workflow Triggered', 2) | |
| .addRaw('\n') | |
| .addRaw(`Branch: \`${branch}\`\n`) | |
| .addRaw(`View workflow runs: [Actions](../../actions/workflows/cd.yml)\n`) | |
| .write(); | |
| trigger-cd-on-pr-merge: | |
| runs-on: ubuntu-latest | |
| if: ${{ github.event_name == 'pull_request' && github.event.action == 'closed' && github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'dev' && startsWith(github.event.pull_request.base.ref, 'release/') && contains(github.event.pull_request.labels.*.name, 'release-automation-sync') }} | |
| outputs: | |
| product: ${{ steps.trigger.outputs.product }} | |
| release_type: ${{ steps.trigger.outputs.release_type }} | |
| version: ${{ steps.trigger.outputs.version }} | |
| branch: ${{ steps.trigger.outputs.branch }} | |
| preid: ${{ steps.trigger.outputs.preid }} | |
| series: ${{ steps.trigger.outputs.series }} | |
| vsrelease: ${{ steps.trigger.outputs.vsrelease }} | |
| goproduct: ${{ steps.trigger.outputs.goproduct }} | |
| steps: | |
| - name: Trigger CD on automation sync PR merge | |
| id: trigger | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }} | |
| script: | | |
| const pr = context.payload.pull_request; | |
| const base = pr.base?.ref; | |
| const head = pr.head?.ref; | |
| const labels = (pr.labels || []).map(l => l.name); | |
| const body = pr.body || ''; | |
| if (head !== 'dev') { | |
| core.notice(`Skip: head branch is '${head}', expected 'dev'.`); | |
| return; | |
| } | |
| if (!base || !base.startsWith('release/')) { | |
| core.notice(`Skip: base branch '${base}' is not a release/* branch.`); | |
| return; | |
| } | |
| const hasLabel = labels.includes('release-automation-sync'); | |
| const markerMatch = body.match(/<!--\s*release-automation-cd-params:\s*(\{[\s\S]*?\})\s*-->/); | |
| if (!hasLabel && !markerMatch) { | |
| core.notice('Skip: PR is not identified as Release Automation sync PR (missing label/marker).'); | |
| return; | |
| } | |
| if (!markerMatch) { | |
| core.setFailed('Release Automation marker is missing; cannot determine CD parameters.'); | |
| return; | |
| } | |
| let params; | |
| try { | |
| params = JSON.parse(markerMatch[1]); | |
| } catch (e) { | |
| core.setFailed(`Failed to parse CD params marker JSON: ${e.message}`); | |
| return; | |
| } | |
| const product = String(params.product || ''); | |
| const version = String(params.version || ''); | |
| const releaseType = String(params.releaseType || params.release_type || ''); | |
| const preid = String(params.preid || ''); | |
| const series = String(params.series || ''); | |
| const vsrelease = Boolean(params.vsrelease); | |
| const goproduct = Boolean(params.goproduct); | |
| const releaseTypeLower = releaseType.toLowerCase(); | |
| const isPrerelease = releaseTypeLower.includes('prerelease'); | |
| const finalPreid = (goproduct && !isPrerelease) ? 'stable' : preid; | |
| if (!finalPreid) { | |
| core.setFailed('CD param preid is missing in marker.'); | |
| return; | |
| } | |
| core.summary | |
| .addHeading('🚀 Trigger CD on PR merge', 2) | |
| .addRaw(`\nPR: ${pr.html_url}\n`) | |
| .addRaw(`\nBranch: \`${base}\`\n`) | |
| .addRaw(`\nProduct: \`${product}\`\n`) | |
| .addRaw(`\nRelease Type: \`${releaseType}\`\n`) | |
| .addRaw(`\nVersion: \`${version}\`\n`) | |
| .addRaw(`\npreid: \`${finalPreid}\` (original: \`${preid}\`, goproduct: \`${goproduct}\`)\n`) | |
| .addRaw(`\nseries: \`${series}\`\n`) | |
| .addRaw(`\nvsrelease: \`${vsrelease}\`\n`) | |
| .addRaw(`\ngoproduct: \`${goproduct}\`\n`) | |
| .write(); | |
| await github.rest.actions.createWorkflowDispatch({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| workflow_id: 'cd.yml', | |
| ref: base, | |
| inputs: { | |
| preid: finalPreid, | |
| series, | |
| vsrelease: vsrelease.toString(), | |
| vstemplate: 'false', | |
| goproduct: goproduct.toString(), | |
| run_test_cases: 'true' | |
| }, | |
| }); | |
| core.setOutput('product', product); | |
| core.setOutput('release_type', releaseType); | |
| core.setOutput('version', version); | |
| core.setOutput('branch', base); | |
| core.setOutput('preid', preid); | |
| core.setOutput('series', series); | |
| core.setOutput('vsrelease', vsrelease.toString()); | |
| core.setOutput('goproduct', goproduct.toString()); | |
| core.setOutput('run_test_cases', 'true'); | |
| notify-cd-and-completion: | |
| runs-on: ubuntu-latest | |
| needs: | |
| - trigger-cd | |
| - trigger-cd-on-pr-merge | |
| if: ${{ always() && (needs.trigger-cd.result == 'success' || needs.trigger-cd-on-pr-merge.result == 'success') }} | |
| env: | |
| MAIL_CLIENT_ID: ${{ secrets.MAIL_CLIENT_ID }} | |
| MAIL_CLIENT_SECRET: ${{ secrets.MAIL_CLIENT_SECRET }} | |
| MAIL_TENANT_ID: ${{ secrets.MAIL_TENANT_ID }} | |
| steps: | |
| - name: Compose notification payload | |
| id: payload | |
| shell: bash | |
| env: | |
| PRODUCT: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.product) || needs.trigger-cd-on-pr-merge.outputs.product }} | |
| RELEASE_TYPE: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.release_type) || needs.trigger-cd-on-pr-merge.outputs.release_type }} | |
| VERSION: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.version) || needs.trigger-cd-on-pr-merge.outputs.version }} | |
| BRANCH: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.branch) || needs.trigger-cd-on-pr-merge.outputs.branch }} | |
| PREID: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.preid) || needs.trigger-cd-on-pr-merge.outputs.preid }} | |
| SERIES: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.series) || needs.trigger-cd-on-pr-merge.outputs.series }} | |
| VSRELEASE: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.vsrelease) || needs.trigger-cd-on-pr-merge.outputs.vsrelease }} | |
| GOPRODUCT: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.goproduct) || needs.trigger-cd-on-pr-merge.outputs.goproduct }} | |
| run: | | |
| set -euo pipefail | |
| WORKFLOW_URL="${{ github.server_url }}/${{ github.repository }}/actions/workflows/cd.yml" | |
| RUN_URL="${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| SUBJECT="ATK Release Automation Notification: ${PRODUCT} ${VERSION} CD Triggered" | |
| BODY=$(jq -c -n \ | |
| --arg product "$PRODUCT" \ | |
| --arg version "$VERSION" \ | |
| --arg branch "$BRANCH" \ | |
| --arg release_type "$RELEASE_TYPE" \ | |
| --arg preid "$PREID" \ | |
| --arg series "$SERIES" \ | |
| --arg vsrelease "$VSRELEASE" \ | |
| --arg goproduct "$GOPRODUCT" \ | |
| --arg workflow_url "$WORKFLOW_URL" \ | |
| --arg run_url "$RUN_URL" \ | |
| ' { | |
| product: $product, | |
| version: $version, | |
| branch: $branch, | |
| release_type: $release_type, | |
| preid: $preid, | |
| series: $series, | |
| vsrelease: $vsrelease, | |
| goproduct: $goproduct, | |
| workflow_url: $workflow_url, | |
| run_url: $run_url, | |
| message: "The CD pipeline has been triggered. Please monitor the workflow progress." | |
| } ') | |
| echo "subject=$SUBJECT" >> "$GITHUB_OUTPUT" | |
| echo "body=$BODY" >> "$GITHUB_OUTPUT" | |
| - name: Send email notification | |
| if: ${{ env.MAIL_CLIENT_ID != '' && env.MAIL_CLIENT_SECRET != '' && env.MAIL_TENANT_ID != '' }} | |
| env: | |
| MAIL_CLIENT_ID: ${{ env.MAIL_CLIENT_ID }} | |
| MAIL_CLIENT_SECRET: ${{ env.MAIL_CLIENT_SECRET }} | |
| MAIL_TENANT_ID: ${{ env.MAIL_TENANT_ID }} | |
| TO: ${{ vars.RELEASE_NOTIFICATION_EMAIL || 'M365AgentsToolkitEngineerTeam@microsoft.com' }} | |
| SUBJECT: ${{ steps.payload.outputs.subject }} | |
| BODY: ${{ steps.payload.outputs.body }} | |
| uses: ./.github/actions/send-email-report | |
| - name: Post completion comment | |
| uses: actions/github-script@v7 | |
| env: | |
| PRODUCT: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.product) || needs.trigger-cd-on-pr-merge.outputs.product }} | |
| VERSION: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.version) || needs.trigger-cd-on-pr-merge.outputs.version }} | |
| BRANCH: ${{ (needs.trigger-cd.result == 'success' && needs.trigger-cd.outputs.branch) || needs.trigger-cd-on-pr-merge.outputs.branch }} | |
| with: | |
| script: | | |
| const product = process.env.PRODUCT || ''; | |
| const version = process.env.VERSION || ''; | |
| const branch = process.env.BRANCH || ''; | |
| core.notice(`✅ Release automation completed for ${product} ${version} on branch ${branch}`); | |
| core.notice('ℹ️ Email notification is best-effort (may be skipped if mail secrets are missing).'); |