Skip to content

Maven Release

Maven Release #12

Workflow file for this run

name: Maven Release
on:
workflow_dispatch:
inputs:
release_version:
description: 'Release version (e.g. 1.2.0)'
required: true
type: string
next_version:
description: 'Next development version (e.g. 1.3.0-SNAPSHOT)'
required: true
type: string
is_pre_release:
description: 'Is pre-release?'
required: false
default: false
type: boolean
permissions:
contents: write
id-token: write
issues: read
pull-requests: read
env:
AWS_REGION: us-west-2
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Determine previous release tag
id: previous_release
shell: bash
run: |
previous_tag="$(git tag --merged HEAD --list 'v*' --sort=-version:refname | head -n 1)"
if [ -z "$previous_tag" ]; then
echo "No previous release tag found. Skipping BREAKING change validation."
echo "tag=" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "Found previous release tag: $previous_tag"
echo "tag=$previous_tag" >> "$GITHUB_OUTPUT"
- name: Collect release commit SHAs
id: release_commits
if: steps.previous_release.outputs.tag != ''
shell: bash
run: |
{
echo "shas<<EOF"
git rev-list "${{ steps.previous_release.outputs.tag }}..HEAD"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Collect release pull request numbers from git history
id: release_pull_requests
if: steps.previous_release.outputs.tag != ''
shell: bash
run: |
{
echo "numbers<<EOF"
git log --format='%s' "${{ steps.previous_release.outputs.tag }}..HEAD" |
sed -nE 's/^.*\(#([0-9]+)\)$/\1/p; s/^Merge pull request #([0-9]+).*$/\1/p' |
sort -u
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Validate BREAKING changes require major version bump
if: steps.previous_release.outputs.tag != ''
uses: actions/github-script@v8
env:
PREVIOUS_TAG: ${{ steps.previous_release.outputs.tag }}
RELEASE_VERSION: ${{ github.event.inputs.release_version }}
RELEASE_COMMITS: ${{ steps.release_commits.outputs.shas }}
RELEASE_PULL_REQUESTS: ${{ steps.release_pull_requests.outputs.numbers }}
with:
script: |
const previousTag = process.env.PREVIOUS_TAG;
const releaseVersion = process.env.RELEASE_VERSION;
const commitShas = process.env.RELEASE_COMMITS
.split('\n')
.map((sha) => sha.trim())
.filter(Boolean);
const releasePullRequestNumbers = process.env.RELEASE_PULL_REQUESTS
.split('\n')
.map((number) => number.trim())
.filter(Boolean)
.map((number) => Number.parseInt(number, 10))
.filter((number) => Number.isInteger(number));
const owner = context.repo.owner;
const repo = context.repo.repo;
const breakingLabel = 'BREAKING';
const parseMajor = (version) => {
const match = version.match(/^v?(\d+)\.\d+\.\d+(?:[-+].*)?$/);
if (!match) {
throw new Error(`Unable to parse semantic version from "${version}"`);
}
return Number.parseInt(match[1], 10);
};
const previousMajor = parseMajor(previousTag);
const releaseMajor = parseMajor(releaseVersion);
if (commitShas.length === 0) {
core.info(`No commits found between ${previousTag} and HEAD. Skipping BREAKING change validation.`);
return;
}
const prsByNumber = new Map();
const linkedIssuesByPullRequest = new Map();
const issuesByNumber = new Map();
for (const commitSha of commitShas) {
const { data: pullRequests } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner,
repo,
commit_sha: commitSha,
});
for (const pullRequest of pullRequests) {
if (pullRequest.merged_at) {
prsByNumber.set(pullRequest.number, pullRequest);
}
}
}
for (const pullRequestNumber of releasePullRequestNumbers) {
if (prsByNumber.has(pullRequestNumber)) {
continue;
}
const { data: pullRequest } = await github.rest.pulls.get({
owner,
repo,
pull_number: pullRequestNumber,
});
if (pullRequest.merged_at) {
prsByNumber.set(pullRequest.number, pullRequest);
}
}
const hasBreakingLabel = (labels) =>
labels.some((label) => label.name?.toUpperCase() === breakingLabel);
const getIssue = async (issueNumber) => {
if (issuesByNumber.has(issueNumber)) {
return issuesByNumber.get(issueNumber);
}
let issue;
try {
const response = await github.rest.issues.get({
owner,
repo,
issue_number: issueNumber,
});
issue = response.data;
} catch (error) {
if (error.status === 404) {
core.info(`Skipping unresolved linked issue #${issueNumber}.`);
issuesByNumber.set(issueNumber, null);
return null;
}
throw error;
}
if (issue.pull_request) {
issuesByNumber.set(issueNumber, null);
return null;
}
const normalizedIssue = {
number: issue.number,
labels: issue.labels.map((label) => ({
name: typeof label === 'string' ? label : label.name,
})),
};
issuesByNumber.set(issueNumber, normalizedIssue);
return normalizedIssue;
};
const getTimelineLinkedIssueNumbers = (event) => {
const currentRepoApiUrl = `https://api.github.com/repos/${owner}/${repo}`.toLowerCase();
const candidateIssues = [
event.source?.issue,
event.subject,
].filter(
(item) =>
item &&
Number.isInteger(item.number) &&
!item.pull_request &&
item.repository_url?.toLowerCase() === currentRepoApiUrl
);
return [...new Set(candidateIssues.map((item) => item.number))];
};
const getLinkedIssues = async (pullRequestNumber) => {
if (linkedIssuesByPullRequest.has(pullRequestNumber)) {
return linkedIssuesByPullRequest.get(pullRequestNumber);
}
const query = `
query($owner: String!, $repo: String!, $number: Int!) {
repository(owner: $owner, name: $repo) {
pullRequest(number: $number) {
closingIssuesReferences(first: 50) {
nodes {
number
repository {
name
owner {
login
}
}
labels(first: 50) {
nodes {
name
}
}
}
}
}
}
}
`;
const result = await github.graphql(query, {
owner,
repo,
number: pullRequestNumber,
});
const isCurrentRepoIssue = (issue) =>
issue.repository?.name === repo && issue.repository.owner?.login === owner;
const linkedIssues = new Map(
result.repository.pullRequest.closingIssuesReferences.nodes
.filter(isCurrentRepoIssue)
.map((issue) => [
issue.number,
{
number: issue.number,
labels: issue.labels.nodes,
},
])
);
const timelineEvents = await github.paginate(github.rest.issues.listEventsForTimeline, {
owner,
repo,
issue_number: pullRequestNumber,
per_page: 100,
});
for (const event of timelineEvents) {
if (!['connected', 'cross-referenced'].includes(event.event)) {
continue;
}
const candidateIssueNumbers = getTimelineLinkedIssueNumbers(event);
for (const issueNumber of candidateIssueNumbers) {
if (linkedIssues.has(issueNumber)) {
continue;
}
const issue = await getIssue(issueNumber);
if (issue) {
linkedIssues.set(issue.number, issue);
}
}
}
const resolvedIssues = [...linkedIssues.values()];
linkedIssuesByPullRequest.set(pullRequestNumber, resolvedIssues);
return resolvedIssues;
};
core.info(
`Evaluating ${prsByNumber.size} pull requests since ${previousTag}: ${[...prsByNumber.keys()]
.map((number) => `#${number}`)
.join(', ')}`
);
const breakingPullRequests = [];
for (const pullRequest of prsByNumber.values()) {
const prHasBreakingLabel = hasBreakingLabel(pullRequest.labels);
const linkedIssues = await getLinkedIssues(pullRequest.number);
const breakingIssues = linkedIssues.filter((issue) => hasBreakingLabel(issue.labels));
if (prHasBreakingLabel || breakingIssues.length > 0) {
breakingPullRequests.push({
number: pullRequest.number,
breakingIssues,
});
}
}
if (breakingPullRequests.length === 0) {
core.info(`No BREAKING-labeled pull requests or linked issues found since ${previousTag}.`);
return;
}
const formatBreakingReferences = (pullRequest) => {
const issueSuffix =
pullRequest.breakingIssues.length === 0
? ''
: ` (linked issues: ${pullRequest.breakingIssues.map((issue) => `#${issue.number}`).join(', ')})`;
return `#${pullRequest.number}${issueSuffix}`;
};
if (releaseMajor > previousMajor) {
core.info(
`Found BREAKING-labeled pull requests or linked issues (${breakingPullRequests
.map(formatBreakingReferences)
.join(', ')}), and release version ${releaseVersion} bumps the major version from ${previousMajor} to ${releaseMajor}.`
);
return;
}
core.setFailed(
`Release ${releaseVersion} includes BREAKING-labeled pull requests or linked issues (${breakingPullRequests
.map(formatBreakingReferences)
.join(', ')}) since ${previousTag}, but the major version was not bumped from ${previousMajor}.`
);
- name: Setup Java
uses: actions/setup-java@v5
with:
java-version: '17'
distribution: 'corretto'
cache: maven
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v6.2.0
with:
role-to-assume: "${{ secrets.ACTIONS_MVN_ROLE_NAME }}"
role-session-name: mavenreleasesession
aws-region: ${{ env.AWS_REGION }}
- name: Set release version
run: mvn -q versions:set -DnewVersion=${{ github.event.inputs.release_version }} -DgenerateBackupPoms=false
- name: Commit release version
run: |
git config user.email "${{ github.actor }}+github-actions[bot]@users.noreply.github.com"
git config user.name "${{ github.actor }}+github-actions[bot]"
git add .
git commit -m "chore: release version ${{ github.event.inputs.release_version }}"
- name: Push changes
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: Build artifacts
run: mvn clean install -q -Dlog4j2.level=WARN -Dlog4j.configurationFile=log4j2-quiet.xml --no-transfer-progress
- name: Create GitHub Release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3
with:
tag_name: v${{ github.event.inputs.release_version }}
name: Release v${{ github.event.inputs.release_version }}
generate_release_notes: true
prerelease: ${{ github.event.inputs.is_pre_release }}
files: |
sdk/target/aws-durable-execution-sdk-java-${{ github.event.inputs.release_version }}.jar
sdk-testing/target/aws-durable-execution-sdk-java-testing-${{ github.event.inputs.release_version }}.jar
- name: Get Env variables
uses: aws-actions/aws-secretsmanager-get-secrets@v3
with:
secret-ids: |
mvn_gpg_keys
mvn_account_keys
parse-json-secrets: true
- name: Sign and publish
run: bash .github/scripts/maven_publish.sh
env:
RELEASE_VERSION: ${{ github.event.inputs.release_version }}
- name: Set next development version
run: mvn -q versions:set -DnewVersion=${{ github.event.inputs.next_version }} -DgenerateBackupPoms=false
- name: Commit release version
run: |
git add .
git commit -m "chore: bump version to ${{ github.event.inputs.next_version }}"
- name: Push changes
uses: ad-m/github-push-action@881a6320fdb16eb5318c5054f31c218aec2b324c # master
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
notify-release:
needs: [release]
uses: ./.github/workflows/notify-release.yml
with:
tag_name: v${{ github.event.inputs.release_version }}
release_url: ${{ github.server_url }}/${{ github.repository }}/releases/tag/v${{ github.event.inputs.release_version }}
secrets: inherit