From aaddc92692f572a022734e7bec110a5ca07e8f81 Mon Sep 17 00:00:00 2001 From: sredny buitrago Date: Wed, 15 Apr 2026 22:13:03 -0400 Subject: [PATCH] potential resolution of TT-16103 --- .../branch-suggestion-autorelease.yml | 51 +++++++ .github/workflows/example-usage.yml.template | 28 +++- branch-suggestion/branch_suggestion.yml | 32 ++++- .../scripts/github/post-release-commands.js | 136 ++++++++++++++++++ 4 files changed, 244 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/branch-suggestion-autorelease.yml create mode 100644 branch-suggestion/scripts/github/post-release-commands.js diff --git a/.github/workflows/branch-suggestion-autorelease.yml b/.github/workflows/branch-suggestion-autorelease.yml new file mode 100644 index 0000000..a786061 --- /dev/null +++ b/.github/workflows/branch-suggestion-autorelease.yml @@ -0,0 +1,51 @@ +name: Auto Release Commands on Merge + +on: + pull_request: + types: [closed] + workflow_call: + secrets: + JIRA_TOKEN: + required: true + description: 'Pre-encoded JIRA token (base64(email:api_token))' + +permissions: + pull-requests: write + contents: read + +jobs: + post-release-commands: + # Only run when the PR was actually merged (not just closed) + if: github.event.pull_request.merged == true + runs-on: ubuntu-latest + + steps: + - name: Checkout github-actions repository + uses: actions/checkout@v4 + with: + repository: TykTechnologies/github-actions + ref: main + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install dependencies + working-directory: branch-suggestion + run: npm install + + - name: Install Visor + run: npm install -g @probelabs/visor + + - name: Analyze and post release commands + working-directory: branch-suggestion + env: + JIRA_TOKEN: ${{ secrets.JIRA_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPOSITORY: ${{ github.repository }} + BRANCH_NAME: ${{ github.head_ref }} + run: | + visor --config branch_suggestion.yml --debug --tags auto-release diff --git a/.github/workflows/example-usage.yml.template b/.github/workflows/example-usage.yml.template index 7393598..e31d7de 100644 --- a/.github/workflows/example-usage.yml.template +++ b/.github/workflows/example-usage.yml.template @@ -1,5 +1,10 @@ -# Branch Suggestion Workflow +# Branch Suggestion Workflows # Documentation: https://github.com/TykTechnologies/github-actions/tree/main/branch-suggestion +# +# Two workflows work together: +# 1. branch-suggestions: Posts suggested merge targets on open PRs +# 2. auto-release-commands: On merge, automatically posts /release commands so +# the backport bot cherry-picks to release branches without developer action name: PR Branch Suggestions @@ -13,6 +18,25 @@ permissions: jobs: branch-suggestions: - uses: TykTechnologies/REFINE/.github/workflows/branch-suggestion.yml@main + uses: TykTechnologies/github-actions/.github/workflows/branch-suggestion.yml@main + secrets: + JIRA_TOKEN: ${{ secrets.JIRA_API_TOKEN }} + +--- +# Save this as a separate file: .github/workflows/branch-suggestion-autorelease.yml + +name: Auto Release Commands on Merge + +on: + pull_request: + types: [closed] + +permissions: + pull-requests: write + contents: read + +jobs: + auto-release-commands: + uses: TykTechnologies/github-actions/.github/workflows/branch-suggestion-autorelease.yml@main secrets: JIRA_TOKEN: ${{ secrets.JIRA_API_TOKEN }} diff --git a/branch-suggestion/branch_suggestion.yml b/branch-suggestion/branch_suggestion.yml index 5044bef..45c1397 100644 --- a/branch-suggestion/branch_suggestion.yml +++ b/branch-suggestion/branch_suggestion.yml @@ -62,7 +62,8 @@ checks: # Match branches and generate suggestions MATCH_RESULT=$(node scripts/common/match-branches.js "$JIRA_RESULT" "$BRANCHES" "$REPO") - # Extract markdown and save for PR comment (use printf to avoid escape sequence issues) + # Save full result for downstream steps (use printf to avoid escape sequence issues) + printf '%s\n' "$MATCH_RESULT" > /tmp/branch_suggestion_result.json printf '%s\n' "$MATCH_RESULT" | jq -r '.markdown' > /tmp/branch_suggestion_markdown.txt # Output full result to stdout @@ -96,3 +97,32 @@ checks: # Clean up rm -f /tmp/branch_suggestion_markdown.txt + + # ============================================================================ + # STEP 3: POST /release COMMANDS ON MERGED PR + # ============================================================================ + # After a PR is merged to master/main, posts one "/release to " + # comment per required/recommended release branch so the backport bot + # picks them up automatically without developer intervention. + # Only runs with --tags auto-release (triggered by the merge workflow). + # ============================================================================ + post-release-commands: + type: command + depends_on: [analyze-and-suggest] + tags: ["auto-release"] + timeout: 60 + exec: | + set -e + + REPO="${REPOSITORY:-TykTechnologies/tyk}" + PR_NUMBER="${PR_NUMBER:-123}" + + if [ ! -f /tmp/branch_suggestion_result.json ]; then + echo "❌ Error: branch_suggestion_result.json not found. analyze-and-suggest step may have failed." >&2 + exit 1 + fi + + node scripts/github/post-release-commands.js "$REPO" "$PR_NUMBER" --file /tmp/branch_suggestion_result.json + + # Clean up + rm -f /tmp/branch_suggestion_result.json diff --git a/branch-suggestion/scripts/github/post-release-commands.js b/branch-suggestion/scripts/github/post-release-commands.js new file mode 100644 index 0000000..b0b2c1f --- /dev/null +++ b/branch-suggestion/scripts/github/post-release-commands.js @@ -0,0 +1,136 @@ +#!/usr/bin/env node +import dotenv from 'dotenv'; +import { + createPRComment, + findCommentByMarker +} from './github-api.js'; + +// Silence dotenv v17+ logging +process.env.DOTENV_LOG_LEVEL = 'error'; +dotenv.config(); + +/** + * Build the unique HTML marker for a given branch so we never post the same + * release command twice (e.g., on a workflow re-run after merge). + * @param {string} branch + * @returns {string} + */ +function markerForBranch(branch) { + return ``; +} + +/** + * Collect all non-master branches that are required or recommended across + * all fix-version match results, deduplicated and in order. + * @param {Array} matchResults - Output of match-branches.js + * @returns {Array} Branch names + */ +function collectReleaseBranches(matchResults) { + const seen = new Set(); + const branches = []; + + for (const result of matchResults) { + for (const b of result.branches) { + if (b.branch === 'master' || b.branch === 'main') continue; + if (b.priority === 'optional') continue; + if (seen.has(b.branch)) continue; + seen.add(b.branch); + branches.push(b.branch); + } + } + + return branches; +} + +/** + * Post one `/release to ` comment per release branch on the merged PR. + * Skips branches that already have an auto-release comment (idempotent). + * @param {string} owner + * @param {string} repo + * @param {number} prNumber + * @param {Array} matchResults - Output of match-branches.js + */ +async function postReleaseCommands(owner, repo, prNumber, matchResults) { + const branches = collectReleaseBranches(matchResults); + + if (branches.length === 0) { + console.log('No release branches to post commands for.'); + return; + } + + console.log(`Posting release commands for ${branches.length} branch(es): ${branches.join(', ')}`); + + for (const branch of branches) { + const marker = markerForBranch(branch); + + // Idempotency check — skip if already posted + const existing = await findCommentByMarker(owner, repo, prNumber, marker); + if (existing) { + console.log(`⏭️ Skipping ${branch} — command already posted (comment #${existing.id})`); + continue; + } + + const body = `/release to ${branch}\n\n${marker}`; + const created = await createPRComment(owner, repo, prNumber, body); + console.log(`✅ Posted /release to ${branch} (comment #${created.id})`); + } +} + +// Main execution when run directly +async function main() { + const args = process.argv.slice(2); + + if (args.length < 3) { + console.log('Usage: node post-release-commands.js '); + console.log('\nOr read match results from a file:'); + console.log(' node post-release-commands.js --file '); + process.exit(1); + } + + const [repoArg, prArg] = args; + const parts = repoArg.split('/'); + if (parts.length !== 2) { + console.error('❌ Repository must be in format "owner/repo"'); + process.exit(1); + } + + const [owner, repo] = parts; + const prNumber = parseInt(prArg, 10); + + if (isNaN(prNumber) || prNumber < 1) { + console.error('❌ PR number must be a positive integer'); + process.exit(1); + } + + let matchResults; + + try { + let raw; + if (args[2] === '--file' && args[3]) { + const fs = await import('fs'); + raw = fs.readFileSync(args[3], 'utf-8'); + } else { + raw = args[2]; + } + + const parsed = JSON.parse(raw); + // Accept either the full pipeline output { matchResults: [...] } or a bare array + matchResults = parsed.matchResults || parsed; + } catch (err) { + console.error(`❌ Failed to parse match results: ${err.message}`); + process.exit(1); + } + + try { + await postReleaseCommands(owner, repo, prNumber, matchResults); + } catch (err) { + console.error(`❌ Error posting release commands: ${err.message}`); + process.exit(1); + } +} + +export { postReleaseCommands, collectReleaseBranches, markerForBranch }; + +if (import.meta.url === `file://${process.argv[1]}`) { + main(); +}