Skip to content

Commit 97e2b72

Browse files
CopilotthomasturrellCopilot
authored
Switch to manual draft release workflow (#446)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: thomasturrell <1552612+thomasturrell@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent af1292b commit 97e2b72

3 files changed

Lines changed: 434 additions & 222 deletions

File tree

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
1+
name: Manual Release
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
title:
7+
description: "Draft release title (e.g., v1.2.3)"
8+
required: true
9+
type: string
10+
11+
permissions:
12+
contents: write
13+
14+
jobs:
15+
draft-release:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- name: Resolve release details
19+
id: release
20+
env:
21+
GH_TOKEN: ${{ github.token }}
22+
run: |
23+
set -euo pipefail
24+
25+
REPO_FULL_NAME="${{ github.repository }}"
26+
TITLE_INPUT="${{ inputs.title }}"
27+
28+
echo "Resolving draft release by title in $REPO_FULL_NAME..."
29+
30+
# Validate title format (should be vX.Y.Z where X.Y.Z is a semver version)
31+
if [[ ! "$TITLE_INPUT" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
32+
echo "::error::Invalid title format. Expected format: vX.Y.Z (e.g., v1.2.3)"
33+
echo "Title must be a semver version number prepended with 'v'."
34+
exit 1
35+
fi
36+
37+
echo "Title format validated: $TITLE_INPUT"
38+
39+
# Get the version number without the 'v' prefix
40+
VERSION="${TITLE_INPUT:1}"
41+
42+
# Find an existing draft release by exact title match
43+
if gh api \
44+
-H "Accept: application/vnd.github+json" \
45+
--paginate \
46+
"/repos/$REPO_FULL_NAME/releases" \
47+
> /tmp/releases_list.json 2>/dev/null; then
48+
:
49+
else
50+
echo "::error::Failed to list releases for $REPO_FULL_NAME."
51+
exit 1
52+
fi
53+
54+
jq -r --arg TITLE "$TITLE_INPUT" \
55+
'[ .[] | select(.draft == true and .name == $TITLE) ] | first // empty' \
56+
/tmp/releases_list.json > /tmp/release.json
57+
58+
if [ ! -s /tmp/release.json ]; then
59+
echo "ERROR: No draft release found with title '$TITLE_INPUT' in $REPO_FULL_NAME."
60+
exit 1
61+
fi
62+
63+
echo "Release JSON:"
64+
cat /tmp/release.json
65+
66+
# Extract fields with jq
67+
TAG_NAME=$(jq -r '.tag_name // .tagName' /tmp/release.json)
68+
NAME=$(jq -r '.name' /tmp/release.json)
69+
TARGET_COMMITISH=$(jq -r '.target_commitish // .targetCommitish' /tmp/release.json)
70+
BODY=$(jq -r '.body' /tmp/release.json)
71+
DRAFT=$(jq -r '.draft' /tmp/release.json)
72+
73+
# Ensure the release is a draft
74+
if [ "$DRAFT" != "true" ]; then
75+
echo "::error::Release '$NAME' exists but is not a draft (draft=$DRAFT)."
76+
echo "Please set the release to draft and rerun this workflow."
77+
exit 1
78+
fi
79+
80+
# Ensure the draft has a tag name we can use downstream
81+
if [ -z "$TAG_NAME" ] || [ "$TAG_NAME" = "null" ]; then
82+
echo "::error::Draft release '$NAME' has no tag name set."
83+
echo "Please set a tag name on the draft release and rerun this workflow."
84+
exit 1
85+
fi
86+
87+
# Fallback for target_commitish: default to repository default branch if missing
88+
if [ -z "$TARGET_COMMITISH" ] || [ "$TARGET_COMMITISH" = "null" ]; then
89+
DEFAULT_BRANCH=$(gh repo view "$REPO_FULL_NAME" --json defaultBranchRef -q '.defaultBranchRef.name')
90+
TARGET_COMMITISH="$DEFAULT_BRANCH"
91+
fi
92+
93+
# Fallbacks
94+
if [ -z "$NAME" ] || [ "$NAME" = "null" ]; then NAME="$TAG_NAME"; fi
95+
if [ -z "$BODY" ] || [ "$BODY" = "null" ]; then BODY="No release notes provided."; fi
96+
97+
echo "tag_name=$TAG_NAME" >> "$GITHUB_OUTPUT"
98+
echo "name=$NAME" >> "$GITHUB_OUTPUT"
99+
echo "target_commitish=$TARGET_COMMITISH" >> "$GITHUB_OUTPUT"
100+
echo "body=$BODY" >> "$GITHUB_OUTPUT"
101+
echo "draft=$DRAFT" >> "$GITHUB_OUTPUT"
102+
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
103+
104+
echo "Resolved: tag=$TAG_NAME, name=$NAME, target_commitish=$TARGET_COMMITISH, draft=$DRAFT version=$VERSION"
105+
106+
- name: Manual dispatch triggered
107+
run: |
108+
echo "Manual draft release for tag: ${{ steps.release.outputs.tag_name }}"
109+
110+
- uses: actions/create-github-app-token@v2
111+
id: app-token
112+
with:
113+
app-id: ${{ secrets.APP_ID }}
114+
private-key: ${{ secrets.APP_PRIVATE_KEY }}
115+
116+
- name: Checkout target commitish with full history (needed to commit & tag)
117+
uses: actions/checkout@v6
118+
with:
119+
ref: ${{ steps.release.outputs.target_commitish }}
120+
token: ${{ steps.app-token.outputs.token }}
121+
fetch-depth: 0
122+
persist-credentials: true
123+
124+
- name: Verify tag does not already exist
125+
run: |
126+
TAG_NAME="${{ steps.release.outputs.tag_name }}"
127+
128+
echo "Checking if tag $TAG_NAME already exists..."
129+
130+
# Check if tag exists locally (use show-ref to check specifically for tags)
131+
if git show-ref --tags "$TAG_NAME" >/dev/null 2>&1; then
132+
echo "::error::Tag $TAG_NAME already exists in the repository."
133+
echo "Please use a different version number or delete the existing tag first."
134+
exit 1
135+
fi
136+
137+
# Check if tag exists on remote (use grep -F for literal string matching)
138+
if git ls-remote --tags origin "$TAG_NAME" | grep -qF "refs/tags/$TAG_NAME"; then
139+
echo "::error::Tag $TAG_NAME already exists on remote."
140+
echo "Please use a different version number or delete the existing tag first."
141+
exit 1
142+
fi
143+
144+
echo "Tag $TAG_NAME does not exist. Proceeding with release."
145+
146+
- name: Set up JDK 25
147+
uses: actions/setup-java@v5
148+
with:
149+
java-version: "25"
150+
distribution: "temurin"
151+
cache: maven
152+
cache-dependency-path: |
153+
pom.xml
154+
xapi-model/pom.xml
155+
xapi-client/pom.xml
156+
xapi-model-spring-boot-starter/pom.xml
157+
server-id: central
158+
server-username: MAVEN_USERNAME
159+
server-password: MAVEN_PASSWORD
160+
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
161+
gpg-passphrase: MAVEN_GPG_PASSPHRASE
162+
163+
- name: Configure Git
164+
run: |
165+
git config user.name "github-actions[bot]"
166+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
167+
168+
- name: Run Maven release:prepare
169+
run: |
170+
VERSION="${{ steps.release.outputs.version }}"
171+
TAG_NAME="${{ steps.release.outputs.tag_name }}"
172+
173+
echo "Preparing release version: $VERSION"
174+
echo "Tag name: $TAG_NAME"
175+
176+
# Run release:prepare with explicit release version
177+
# Maven will automatically calculate the next development version
178+
# Only prepare production modules, exclude all sample modules
179+
# Pass -pl/-am to forked Maven invocations via -Darguments
180+
./mvnw -B release:prepare \
181+
-DreleaseVersion="${VERSION}" \
182+
-Dtag="${TAG_NAME}" \
183+
-DpushChanges=false \
184+
-Darguments="-pl xapi-model,xapi-client,xapi-model-spring-boot-starter -am"
185+
env:
186+
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
187+
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
188+
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
189+
190+
- name: Run Maven release:perform
191+
run: |
192+
echo "Performing release and deploying to Maven Central"
193+
194+
# Run release:perform to build and deploy
195+
# Only release production modules, exclude all sample modules
196+
# Pass -pl/-am to forked Maven invocations via -Darguments
197+
./mvnw -B release:perform \
198+
-DlocalCheckout=true \
199+
-DeployAtEnd=true \
200+
-Darguments="-pl xapi-model,xapi-client,xapi-model-spring-boot-starter -am"
201+
env:
202+
MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }}
203+
MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }}
204+
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
205+
206+
- name: Push changes to target branch
207+
run: |
208+
TARGET_BRANCH="${{ steps.release.outputs.target_commitish }}"
209+
TAG_NAME="${{ steps.release.outputs.tag_name }}"
210+
211+
echo "Pushing changes to branch: $TARGET_BRANCH"
212+
213+
# Push the commits created by release:prepare
214+
if ! git push --force-with-lease origin "HEAD:${TARGET_BRANCH}"; then
215+
echo "::error::Failed to push release commits to ${TARGET_BRANCH} due to branch divergence."
216+
echo "The remote branch may have new commits. Please resolve the conflict manually:"
217+
echo " 1. Fetch the latest changes: git fetch origin"
218+
echo " 2. Rebase or merge as needed, then push again with --force-with-lease."
219+
exit 1
220+
fi
221+
222+
# Push the tag created by release:prepare
223+
git push origin "$TAG_NAME"
224+
225+
echo "Pushed release commits and tag to $TARGET_BRANCH"
226+
env:
227+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
228+
229+
- name: Upload artifacts to draft release
230+
env:
231+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
232+
run: |
233+
set -euo pipefail
234+
235+
REPO_FULL_NAME="${{ github.repository }}"
236+
TAG="${{ steps.release.outputs.tag_name }}"
237+
238+
echo "Uploading artifacts to draft release $TAG..."
239+
240+
# Find and upload jar files from target directories
241+
# Exclude SNAPSHOT jars, only include release artifacts
242+
for module in xapi-client xapi-model xapi-model-spring-boot-starter; do
243+
echo "Processing module: $module"
244+
245+
# Upload all jar files (main, sources, javadoc, etc.)
246+
for jar in "$module/target"/*.jar; do
247+
# Skip if glob didn't match anything
248+
[ -e "$jar" ] || continue
249+
250+
# Skip SNAPSHOT jars
251+
if [[ "$jar" == *-SNAPSHOT.jar ]]; then
252+
echo "Skipping SNAPSHOT jar: $jar"
253+
continue
254+
fi
255+
256+
echo "Uploading: $jar"
257+
gh release upload "$TAG" "$jar" \
258+
--repo "$REPO_FULL_NAME" \
259+
--clobber
260+
done
261+
done
262+
263+
echo "All artifacts uploaded successfully!"
264+
265+
- name: Associate draft release with created tag
266+
env:
267+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
268+
run: |
269+
set -euo pipefail
270+
271+
REPO_FULL_NAME="${{ github.repository }}"
272+
TAG="${{ steps.release.outputs.tag_name }}"
273+
274+
echo "Updating draft release to point to tag $TAG..."
275+
276+
# Update the release to point to the new tag
277+
gh release edit "$TAG" --repo "$REPO_FULL_NAME" --tag "$TAG" --draft
278+
279+
echo "Draft release updated successfully!"
280+
281+
- name: Publish GitHub release
282+
env:
283+
GH_TOKEN: ${{ steps.app-token.outputs.token }}
284+
run: |
285+
set -euo pipefail
286+
287+
REPO_FULL_NAME="${{ github.repository }}"
288+
TAG="${{ steps.release.outputs.tag_name }}"
289+
VERSION="${{ steps.release.outputs.version }}"
290+
291+
echo "Publishing GitHub release for $TAG..."
292+
293+
# Publish the release (remove draft status)
294+
gh release edit "$TAG" --repo "$REPO_FULL_NAME" --draft=false
295+
296+
echo "✅ Release $VERSION published successfully!"
297+
echo "View at: https://github.com/$REPO_FULL_NAME/releases/tag/$TAG"
298+
299+
- name: Workflow Summary
300+
if: always()
301+
run: |
302+
echo "## Draft Release Workflow Summary" >> $GITHUB_STEP_SUMMARY
303+
echo "**Version:** ${{ steps.release.outputs.version }}" >> $GITHUB_STEP_SUMMARY
304+
echo "**Tag:** ${{ steps.release.outputs.tag_name }}" >> $GITHUB_STEP_SUMMARY
305+
echo "**Branch:** ${{ steps.release.outputs.target_commitish }}" >> $GITHUB_STEP_SUMMARY
306+
echo "**Status:** ${{ job.status }}" >> $GITHUB_STEP_SUMMARY

0 commit comments

Comments
 (0)