Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/branch-suggestion-autorelease.yml
Original file line number Diff line number Diff line change
@@ -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
28 changes: 26 additions & 2 deletions .github/workflows/example-usage.yml.template
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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 }}
32 changes: 31 additions & 1 deletion branch-suggestion/branch_suggestion.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 <branch>"
# 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
136 changes: 136 additions & 0 deletions branch-suggestion/scripts/github/post-release-commands.js
Original file line number Diff line number Diff line change
@@ -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 `<!-- auto-release: ${branch} -->`;
}

/**
* 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<string>} 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 <branch>` 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 <owner/repo> <pr-number> <match-results-json>');
console.log('\nOr read match results from a file:');
console.log(' node post-release-commands.js <owner/repo> <pr-number> --file <path>');
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();
}