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