Skip to content

Release Notification #9

Release Notification

Release Notification #9

name: Release Notification
on:
workflow_run:
workflows: ["Publish"]
types: [completed]
jobs:
notify:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
permissions:
contents: read
issues: write
pull-requests: write
env:
TARGET_OWNER: koxudaxi
TARGET_REPO: datamodel-code-generator
steps:
- name: Post release notifications
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
// Get tag from the workflow run that triggered this
let tag = context.payload.workflow_run.head_branch;
// Fallback: if head_branch is empty, resolve tag from head_sha
if (!tag) {
const headSha = context.payload.workflow_run.head_sha;
console.log(`head_branch is empty, resolving tag from head_sha: ${headSha}`);
try {
for await (const response of github.paginate.iterator(
github.rest.repos.listTags,
{ owner: context.repo.owner, repo: context.repo.repo, per_page: 100 }
)) {
const matchingTag = response.data.find(t => t.commit.sha === headSha);
if (matchingTag) {
tag = matchingTag.name;
console.log(`Resolved tag from sha: ${tag}`);
break;
}
}
} catch (e) {
console.log(`Failed to resolve tag from sha: ${e.message}`);
}
}
if (!tag) {
console.log('Could not determine tag, skipping notification');
return;
}
// Fetch release info for this tag
let release;
try {
const { data } = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
release = data;
} catch (e) {
console.log(`No release found for tag ${tag}: ${e.message}`);
return;
}
const version = release.tag_name;
const releaseUrl = release.html_url;
const releaseBody = release.body || '';
// Target repo from environment variables (set in jobs.notify.env)
const repoOwner = process.env.TARGET_OWNER;
const repoName = process.env.TARGET_REPO;
// Validate environment variables
if (!repoOwner || !repoName) {
throw new Error('TARGET_OWNER and TARGET_REPO are required');
}
// Verify we're running in the correct repo
if (context.repo.owner !== repoOwner || context.repo.repo !== repoName) {
console.log(`Skipping: running in ${context.repo.owner}/${context.repo.repo}, not ${repoOwner}/${repoName}`);
return;
}
// Helper: Get all comments with pagination
async function getAllComments(issueNumber) {
const comments = [];
for await (const response of github.paginate.iterator(
github.rest.issues.listComments,
{ owner: repoOwner, repo: repoName, issue_number: issueNumber, per_page: 100 }
)) {
comments.push(...response.data);
}
return comments;
}
// Extract PR numbers from release notes (this repo only, PRs only)
const prNumbers = new Set();
// Pattern 1: Full PR URL (this repo only)
const prUrlRegex = new RegExp(
'https://github\\.com/' + repoOwner + '/' + repoName + '/pull/(\\d+)', 'gi'
);
let m;
while ((m = prUrlRegex.exec(releaseBody)) !== null) {
prNumbers.add(parseInt(m[1]));
}
// Pattern 2: owner/repo#123 (this repo only) - verify it's a PR later
const repoRefRegex = new RegExp(
'(?:^|[\\s({\\[-])' + repoOwner + '/' + repoName + '#(\\d+)', 'gi'
);
while ((m = repoRefRegex.exec(releaseBody)) !== null) {
prNumbers.add(parseInt(m[1]));
}
// Pattern 3: Standalone #123 - verify it's a PR later
const standaloneRefRegex = /(?:^|[\s({\[-])#(\d+)(?=[\s,.)\]}\]:;]|$)/g;
while ((m = standaloneRefRegex.exec(releaseBody)) !== null) {
prNumbers.add(parseInt(m[1]));
}
console.log(`Found ${prNumbers.size} potential PRs in release ${version}`);
// Helper: Extract issue numbers with word boundary
function extractIssueNumbers(text, keywordPattern) {
const issues = new Set();
let m;
// Pattern 1: keyword #123 (with word boundary)
const simplePattern = new RegExp(
'(?:^|\\s|[({\\[-])' + keywordPattern + '\\s*[:#]?\\s*#(\\d+)', 'gi'
);
while ((m = simplePattern.exec(text)) !== null) {
issues.add(parseInt(m[1]));
}
// Pattern 2: keyword owner/repo#123 (same repo only)
const crossRepoPattern = new RegExp(
'(?:^|\\s|[({\\[-])' + keywordPattern + '\\s*[:#]?\\s*' +
repoOwner + '/' + repoName + '#(\\d+)', 'gi'
);
while ((m = crossRepoPattern.exec(text)) !== null) {
issues.add(parseInt(m[1]));
}
// Pattern 3: keyword URL (same repo only)
const urlPattern = new RegExp(
'(?:^|\\s|[({\\[-])' + keywordPattern + '\\s*[:#]?\\s*https://github\\.com/' +
repoOwner + '/' + repoName + '/issues/(\\d+)', 'gi'
);
while ((m = urlPattern.exec(text)) !== null) {
issues.add(parseInt(m[1]));
}
return issues;
}
// Word boundary patterns for keywords
const closingKeywordBase = '(?:fix(?:es|ed)?|close[sd]?|resolve[sd]?)';
const relatedKeywordBase = '(?:related(?:\\s+to)?|see|ref(?:s|erences)?)';
const processedIssues = new Set();
const processedPRs = new Set();
for (const prNumber of prNumbers) {
try {
// Get PR details (skip if not a PR)
let prBody = '';
try {
const { data: pr } = await github.rest.pulls.get({
owner: repoOwner,
repo: repoName,
pull_number: prNumber
});
prBody = pr.body || '';
} catch (prError) {
if (prError.status === 404) {
console.log(`#${prNumber} is not a PR (likely an Issue), skipping - only PRs are notified`);
continue;
}
throw prError;
}
// Post comment to PR
if (!processedPRs.has(prNumber)) {
const prComment = `🎉 **Released in [${version}](${releaseUrl})**\n\nThis PR is now available in the latest release. See the [release notes](${releaseUrl}) for details.`;
const prComments = await getAllComments(prNumber);
const hasExistingComment = prComments.some(c =>
c.body && c.body.includes(`Released in [${version}]`)
);
if (!hasExistingComment) {
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: prComment
});
console.log(`Posted release comment to PR #${prNumber}`);
}
processedPRs.add(prNumber);
}
// Find closing issues (Fixes/Closes/Resolves)
const closingIssues = extractIssueNumbers(prBody, closingKeywordBase);
// Find related issues (Related/See/Ref)
const relatedIssues = extractIssueNumbers(prBody, relatedKeywordBase);
// Remove closing issues from related issues to prevent duplicate comments
for (const issueNum of closingIssues) {
relatedIssues.delete(issueNum);
}
// Find referenced PRs in PR body
const referencedPRs = new Set();
const prRefUrlPattern = new RegExp(
'https://github\\.com/' + repoOwner + '/' + repoName + '/pull/(\\d+)', 'gi'
);
let prRefMatch;
while ((prRefMatch = prRefUrlPattern.exec(prBody)) !== null) {
const refPrNum = parseInt(prRefMatch[1]);
if (refPrNum !== prNumber) {
referencedPRs.add(refPrNum);
}
}
// Find standalone issue/PR references not in closing/related
const standalonePattern = /(?:^|[\s({\[-])#(\d+)(?=[\s,.)\]}\]:;]|$)/g;
const issueUrlPattern = new RegExp(
'https://github\\.com/' + repoOwner + '/' + repoName + '/issues/(\\d+)', 'gi'
);
let standaloneMatch;
while ((standaloneMatch = standalonePattern.exec(prBody)) !== null) {
const refNum = parseInt(standaloneMatch[1]);
if (!closingIssues.has(refNum) && refNum !== prNumber) {
relatedIssues.add(refNum);
}
}
while ((standaloneMatch = issueUrlPattern.exec(prBody)) !== null) {
const issueNum = parseInt(standaloneMatch[1]);
if (!closingIssues.has(issueNum) && !relatedIssues.has(issueNum)) {
relatedIssues.add(issueNum);
}
}
// Post to closing issues
for (const issueNumber of closingIssues) {
const key = `closing-${issueNumber}`;
if (processedIssues.has(key)) continue;
processedIssues.add(key);
try {
const issueComments = await getAllComments(issueNumber);
const hasExistingComment = issueComments.some(c =>
c.body && c.body.includes(`Released in [${version}]`)
);
if (!hasExistingComment) {
const comment = `🎉 **Released in [${version}](${releaseUrl})**\n\nThe fix/feature from PR #${prNumber} has been included in this release. See the [release notes](${releaseUrl}) for details.\n\nThank you for your contribution!`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: issueNumber,
body: comment
});
console.log(`Posted closing comment to issue #${issueNumber}`);
}
} catch (e) {
console.log(`Failed to comment on issue #${issueNumber}: ${e.message}`);
}
}
// Post to related issues (not closing)
for (const issueNumber of relatedIssues) {
const key = `related-${issueNumber}`;
if (processedIssues.has(key)) continue;
processedIssues.add(key);
try {
// Check if it's actually an issue (not a PR)
try {
await github.rest.pulls.get({
owner: repoOwner,
repo: repoName,
pull_number: issueNumber
});
// It's a PR, add to referencedPRs instead
referencedPRs.add(issueNumber);
continue;
} catch (checkError) {
if (checkError.status !== 404) throw checkError;
// It's an issue, continue
}
const issueComments = await getAllComments(issueNumber);
const hasExistingComment = issueComments.some(c =>
c.body && (c.body.includes(`Released in [${version}]`) ||
c.body.includes(`Related PR Released`))
);
if (!hasExistingComment) {
const comment = `📢 **Related PR Released: [${version}](${releaseUrl})**\n\nPR #${prNumber}, which references this issue, has been released. See the [release notes](${releaseUrl}) for details.\n\nNote: This issue was not explicitly closed by the PR.`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: issueNumber,
body: comment
});
console.log(`Posted related comment to issue #${issueNumber}`);
}
} catch (e) {
console.log(`Failed to comment on issue #${issueNumber}: ${e.message}`);
}
}
// Post to referenced PRs
for (const refPrNumber of referencedPRs) {
const key = `refpr-${refPrNumber}`;
if (processedPRs.has(refPrNumber) || processedIssues.has(key)) continue;
processedIssues.add(key);
try {
const refPrComments = await getAllComments(refPrNumber);
const hasExistingComment = refPrComments.some(c =>
c.body && c.body.includes(`Released in [${version}]`)
);
if (!hasExistingComment) {
const comment = `📢 **Related PR Released: [${version}](${releaseUrl})**\n\nPR #${prNumber}, which references this PR, has been released. See the [release notes](${releaseUrl}) for details.`;
await github.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: refPrNumber,
body: comment
});
console.log(`Posted related comment to PR #${refPrNumber}`);
}
} catch (e) {
console.log(`Failed to comment on PR #${refPrNumber}: ${e.message}`);
}
}
} catch (e) {
console.log(`Failed to process PR #${prNumber}: ${e.message}`);
}
}
console.log(`Release notification complete for ${version}`);