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 12 * * *" # Run daily at 20:00 (UTC+8) => 12: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) TOMORROW_UTC8=$(date -u -d '+8 hours +1 day' +%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=$TOMORROW_UTC8 (tomorrow UTC+8)" RELEASE=$(jq -c --arg d "$TOMORROW_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 branch name (release/6.N -> release/6.(N-1)) SOURCE_BRANCH="" if [ "$IS_STABLE" = "true" ] && [ "$RERUN_CD_ONLY" != "true" ]; then if [[ "$BRANCH" =~ ^release/([0-9]+)\.([0-9]+)$ ]]; then MAJOR="${BASH_REMATCH[1]}" MINOR="${BASH_REMATCH[2]}" if (( MINOR > 0 )); then SOURCE_BRANCH="release/${MAJOR}.$((MINOR - 1))" else SOURCE_BRANCH="dev" fi else echo "âš ī¸ Cannot derive source branch from '$BRANCH'. Falling back to dev." 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 = ``; 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 = //; 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: Bump versions and create PR for stable release id: version_bump_pr if: ${{ steps.create_branch.outputs.created == 'true' && steps.config.outputs.is_stable == 'true' && env.DRY_RUN == 'false' }} uses: actions/github-script@v7 with: script: | const { execSync } = require('child_process'); const fs = require('fs'); const branch = '${{ steps.config.outputs.branch }}'; const version = '${{ steps.config.outputs.version }}'; const majorMinor = version.replace(/\.[^.]*$/, ''); const bumpBranch = `chore/bump-version-${majorMinor}`; // --- Checkout release branch first --- execSync(`git fetch origin ${branch}`, { stdio: 'inherit' }); execSync(`git checkout origin/${branch}`, { stdio: 'inherit' }); execSync(`git checkout -b ${bumpBranch}`, { stdio: 'inherit' }); // --- 1. Bump SampleConfigTag (v3.N.0 -> v3.(N+1).0) --- const samplesFile = 'packages/fx-core/src/common/samples.ts'; let samplesContent = fs.readFileSync(samplesFile, 'utf8'); const tagMatch = samplesContent.match(/SampleConfigTag = "v(\d+)\.(\d+)\.(\d+)"/); if (tagMatch) { const newTag = `v${tagMatch[1]}.${parseInt(tagMatch[2]) + 1}.0`; samplesContent = samplesContent.replace(tagMatch[0], `SampleConfigTag = "${newTag}"`); fs.writeFileSync(samplesFile, samplesContent); core.info(`SampleConfigTag: v${tagMatch[1]}.${tagMatch[2]}.${tagMatch[3]} -> ${newTag}`); } // --- 2. Update templates-config.json --- const configFile = 'packages/fx-core/src/common/templates-config.json'; const config = JSON.parse(fs.readFileSync(configFile, 'utf8')); config.version = `~${majorMinor}`; config.localVersion = version; fs.writeFileSync(configFile, JSON.stringify(config, null, 4) + '\n'); core.info(`templates-config.json: version=~${majorMinor}, localVersion=${version}`); // --- 3. Update templates/package.json --- const pkgFile = 'templates/package.json'; const pkg = JSON.parse(fs.readFileSync(pkgFile, 'utf8')); pkg.version = `${version}-alpha`; fs.writeFileSync(pkgFile, JSON.stringify(pkg, null, 2) + '\n'); core.info(`templates/package.json: version=${version}-alpha`); // --- 4. Update vscode-extension/package.json --- const vscPkgFile = 'packages/vscode-extension/package.json'; const vscPkg = JSON.parse(fs.readFileSync(vscPkgFile, 'utf8')); vscPkg.version = `${version}-rc`; fs.writeFileSync(vscPkgFile, JSON.stringify(vscPkg, null, 2) + '\n'); core.info(`vscode-extension/package.json: version=${version}-rc`); // --- Commit and push --- execSync('git add packages/fx-core/src/common/samples.ts packages/fx-core/src/common/templates-config.json templates/package.json packages/vscode-extension/package.json', { stdio: 'inherit' }); execSync(`git commit -m "chore: bump template and sample version for ${version}"`, { stdio: 'inherit' }); execSync(`git push origin ${bumpBranch}`, { stdio: 'inherit' }); core.setOutput('version', version); core.setOutput('major_minor', majorMinor); core.setOutput('bump_branch', bumpBranch); - name: Create version bump PR if: ${{ steps.version_bump_pr.outputs.bump_branch != '' }} uses: actions/github-script@v7 with: github-token: ${{ secrets.RELEASE_AUTOMATION_TOKEN || github.token }} script: | const owner = context.repo.owner; const repo = context.repo.repo; const base = '${{ steps.config.outputs.branch }}'; const head = '${{ steps.version_bump_pr.outputs.bump_branch }}'; const version = '${{ steps.version_bump_pr.outputs.version }}'; const majorMinor = '${{ steps.version_bump_pr.outputs.major_minor }}'; const product = '${{ steps.config.outputs.product }}'; const releaseType = '${{ steps.config.outputs.release_type }}'; 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 = ``; const label = 'release-automation-sync'; const title = `chore: bump template and sample version for ${version}`; const body = [ 'This PR is created by Release Automation to bump version numbers for the stable release.', '', `- **Version:** ${version}`, `- **SampleConfigTag:** bumped minor version`, `- **templates-config.json:** version=\`~${majorMinor}\`, localVersion=\`${version}\``, `- **templates/package.json:** version=\`${version}-alpha\``, `- **vscode-extension/package.json:** version=\`${version}-rc\``, '', `Release Automation Run: ${workflowRunUrl}`, marker, '', 'After merging, the CD pipeline will be triggered automatically.', ].join('\n'); const pr = await github.rest.pulls.create({ owner, repo, title, head, base, body }); core.notice(`Created version bump PR: ${pr.data.html_url}`); try { await github.rest.issues.addLabels({ owner, repo, issue_number: pr.data.number, labels: [label] }); } catch (e) { core.notice(`Unable to add label '${label}': ${e.message}`); } core.summary .addHeading('📌 Version Bump PR', 2) .addRaw(`\nCreated: ${pr.data.html_url}\n`) .write(); - 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 [[ "$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', skipmarkdownandvulnerabilitycheck: goproduct.toString() } }); 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.head.ref, 'chore/bump-version-')) && 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' && !head.startsWith('chore/bump-version-')) { core.notice(`Skip: head branch is '${head}', expected 'dev' or 'chore/bump-version-*'.`); 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(//); 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', skipmarkdownandvulnerabilitycheck: goproduct.toString() }, }); 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).');