diff --git a/.github/workflows/deploy-maven-central.yml b/.github/workflows/deploy-maven-central.yml
index a723e83c..9a262d5b 100644
--- a/.github/workflows/deploy-maven-central.yml
+++ b/.github/workflows/deploy-maven-central.yml
@@ -5,14 +5,22 @@ on:
tags:
- 'v*'
- '[0-9]+.[0-9]+.[0-9]+*'
+ workflow_run:
+ workflows: ["Finalize GPULlama3 Release"]
+ types: [completed]
workflow_dispatch:
inputs:
+ tag:
+ description: 'Tag to deploy (e.g., v0.2.3) - leave empty to deploy latest tag'
+ required: false
+ type: string
dry_run:
description: 'Dry run (skip actual deploy)'
required: false
default: false
type: boolean
+
jobs:
deploy:
name: Deploy to Maven Central
@@ -85,4 +93,18 @@ jobs:
--batch-mode
env:
GPG_KEYNAME: ${{ secrets.GPG_KEYNAME }}
- GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
\ No newline at end of file
+ GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
+
+ - name: Deployment Summary
+ if: ${{ !inputs.dry_run }}
+ run: |
+ echo "## š Maven Central Deployment" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Detail | Value |" >> $GITHUB_STEP_SUMMARY
+ echo "|--------|-------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Version | ${{ steps.version.outputs.version }} |" >> $GITHUB_STEP_SUMMARY
+ echo "| GroupId | io.github.beehive-lab |" >> $GITHUB_STEP_SUMMARY
+ echo "| ArtifactId | gpu-llama3 |" >> $GITHUB_STEP_SUMMARY
+ echo "| Status | ā
Deployed |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "š [View on Maven Central](https://central.sonatype.com/artifact/io.github.beehive-lab/gpu-llama3/${{ steps.version.outputs.version }})" >> $GITHUB_STEP_SUMMARY
\ No newline at end of file
diff --git a/.github/workflows/finalize-release.yml b/.github/workflows/finalize-release.yml
new file mode 100644
index 00000000..ef71adf6
--- /dev/null
+++ b/.github/workflows/finalize-release.yml
@@ -0,0 +1,150 @@
+name: Finalize GPULlama3 Release
+
+on:
+ pull_request:
+ types: [closed]
+ branches: [main]
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Release version to tag (e.g., 0.2.3)'
+ required: true
+ type: string
+
+jobs:
+ create-release-tag:
+ # Run when release PR merges OR manual trigger
+ if: |
+ (github.event.pull_request.merged == true && startsWith(github.event.pull_request.head.ref, 'release/')) ||
+ github.event_name == 'workflow_dispatch'
+ runs-on: [self-hosted, Linux, x64]
+ permissions:
+ contents: write
+ timeout-minutes: 10
+
+ outputs:
+ version: ${{ steps.get_version.outputs.version }}
+
+ steps:
+ - name: Get version
+ id: get_version
+ run: |
+ if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
+ echo "version=${{ inputs.version }}" >> $GITHUB_OUTPUT
+ else
+ BRANCH="${{ github.event.pull_request.head.ref }}"
+ echo "version=${BRANCH#release/}" >> $GITHUB_OUTPUT
+ fi
+
+ - name: Checkout main
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ inputs.tag || github.ref }}
+ fetch-depth: 0
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create and push tag
+ run: |
+ VERSION="${{ steps.get_version.outputs.version }}"
+ git tag -a "v${VERSION}" -m "Release ${VERSION}"
+ git push origin "v${VERSION}"
+ echo "ā
Created tag v${VERSION}"
+
+ - name: Extract changelog for release notes
+ id: changelog
+ run: |
+ VERSION="${{ steps.get_version.outputs.version }}"
+ CHANGELOG_FILE="CHANGELOG.md"
+
+ if [ -f "$CHANGELOG_FILE" ]; then
+ # Extract section for this version
+ awk -v ver="## \\[${VERSION}\\]" '
+ $0 ~ ver { found=1; next }
+ found && /^## \[/ { exit }
+ found { print }
+ ' "$CHANGELOG_FILE" > /tmp/release_notes.txt
+
+ if [ -s /tmp/release_notes.txt ]; then
+ echo "Found changelog section for ${VERSION}"
+ else
+ echo "See CHANGELOG.md for details." > /tmp/release_notes.txt
+ fi
+ else
+ echo "See commit history for changes." > /tmp/release_notes.txt
+ fi
+
+ - name: Create GitHub Release
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const fs = require('fs');
+ const version = '${{ steps.get_version.outputs.version }}';
+
+ let releaseNotes;
+ try {
+ releaseNotes = fs.readFileSync('/tmp/release_notes.txt', 'utf8').trim();
+ if (!releaseNotes) {
+ releaseNotes = `Release ${version}`;
+ }
+ } catch (e) {
+ releaseNotes = `Release ${version}`;
+ }
+
+ // Add installation instructions
+ releaseNotes += `\n\n---\n\n### š¦ Installation\n\n`;
+ releaseNotes += `**Maven**\n\`\`\`xml\n\n io.github.beehive-lab\n gpu-llama3\n ${version}\n\n\`\`\`\n\n`;
+ releaseNotes += `**Gradle**\n\`\`\`groovy\nimplementation 'io.github.beehive-lab:gpu-llama3:${version}'\n\`\`\`\n\n`;
+ releaseNotes += `---\n\nš [Documentation](https://github.com/beehive-lab/GPULlama3.java#readme) | š [Maven Central](https://central.sonatype.com/artifact/io.github.beehive-lab/gpu-llama3/${version})`;
+
+ const { data: release } = await github.rest.repos.createRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ tag_name: `v${version}`,
+ name: `GPULlama3.java ${version}`,
+ body: releaseNotes,
+ draft: false,
+ prerelease: false
+ });
+
+ console.log(`ā
Created release: ${release.html_url}`);
+
+ - name: Summary
+ run: |
+ VERSION="${{ steps.get_version.outputs.version }}"
+ echo "## š Release v${VERSION} Finalized" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "| Step | Status |" >> $GITHUB_STEP_SUMMARY
+ echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
+ echo "| Tag created | ā
v${VERSION} |" >> $GITHUB_STEP_SUMMARY
+ echo "| GitHub Release | ā
Created |" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### š Next:" >> $GITHUB_STEP_SUMMARY
+ echo "**Deploy to Maven Central** workflow will trigger automatically" >> $GITHUB_STEP_SUMMARY
+
+ cleanup-branch:
+ needs: create-release-tag
+ runs-on: [self-hosted, Linux, x64]
+ permissions:
+ contents: write
+ if: github.event_name == 'pull_request'
+
+ steps:
+ - name: Delete release branch
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const branch = '${{ github.event.pull_request.head.ref }}';
+ try {
+ await github.rest.git.deleteRef({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ ref: `heads/${branch}`
+ });
+ console.log(`ā
Deleted branch: ${branch}`);
+ } catch (e) {
+ console.log(`Could not delete branch: ${e.message}`);
+ }
diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml
new file mode 100644
index 00000000..a011495e
--- /dev/null
+++ b/.github/workflows/prepare-release.yml
@@ -0,0 +1,290 @@
+name: Prepare GPULlama3 Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ version:
+ description: 'Release version (e.g., 0.2.3)'
+ required: true
+ type: string
+ previous_version:
+ description: 'Previous version for changelog (e.g., 0.2.2)'
+ required: true
+ type: string
+ dry_run:
+ description: 'Dry run - show changes without creating PR'
+ required: false
+ type: boolean
+ default: false
+
+env:
+ VERSION: ${{ inputs.version }}
+ PREV_VERSION: ${{ inputs.previous_version }}
+
+jobs:
+ prepare-release:
+ runs-on: [self-hosted, Linux, x64]
+ permissions:
+ contents: write
+ pull-requests: write
+ timeout-minutes: 15
+ env:
+ JAVA_HOME: /opt/jenkins/jdks/graal-23.1.0/jdk-21.0.3
+
+ steps:
+ - name: Validate version format
+ run: |
+ if [[ ! "${{ inputs.version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
+ echo "ā Invalid version format. Expected: X.Y.Z (e.g., 0.2.3)"
+ exit 1
+ fi
+ echo "ā
Version format valid: ${{ inputs.version }}"
+
+ - name: Checkout main branch
+ uses: actions/checkout@v4
+ with:
+ ref: main
+ fetch-depth: 0
+ token: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Setup environment
+ run: |
+ echo "$JAVA_HOME/bin" >> $GITHUB_PATH
+
+ - name: Configure Git
+ run: |
+ git config user.name "github-actions[bot]"
+ git config user.email "github-actions[bot]@users.noreply.github.com"
+
+ - name: Create release branch
+ run: |
+ git checkout -b release/${{ env.VERSION }}
+ echo "ā
Created branch: release/${{ env.VERSION }}"
+
+ # ============================================
+ # VERSION UPDATES
+ # ============================================
+
+ - name: Update Maven version
+ run: |
+ ./mvnw versions:set -DnewVersion=${{ env.VERSION }} -DgenerateBackupPoms=false
+ echo "ā
Maven version updated to ${{ env.VERSION }}"
+
+ - name: Update README.md
+ run: |
+ if [ -f "README.md" ]; then
+ # Update version in Maven dependency example
+ sed -i 's|[0-9]\+\.[0-9]\+\.[0-9]\+|${{ env.VERSION }}|g' README.md
+ echo "ā
Updated README.md"
+ fi
+
+ - name: Update CITATION.cff
+ run: |
+ if [ -f "CITATION.cff" ]; then
+ sed -i "s/^version: .*/version: ${{ env.VERSION }}/" CITATION.cff
+ RELEASE_DATE=$(date +"%Y-%m-%d")
+ sed -i "s/^date-released: .*/date-released: $RELEASE_DATE/" CITATION.cff
+ echo "ā
Updated CITATION.cff"
+ fi
+
+ # ============================================
+ # CHANGELOG GENERATION
+ # ============================================
+
+ - name: Fetch merged PRs for changelog
+ id: fetch_prs
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const prevVersion = '${{ env.PREV_VERSION }}';
+ const newVersion = '${{ env.VERSION }}';
+
+ let sinceDate;
+ try {
+ const { data: releases } = await github.rest.repos.listReleases({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ per_page: 10
+ });
+
+ const prevRelease = releases.find(r =>
+ r.tag_name === `v${prevVersion}` || r.tag_name === prevVersion
+ );
+ if (prevRelease) {
+ sinceDate = prevRelease.published_at;
+ console.log(`Found previous release ${prevVersion} from ${sinceDate}`);
+ }
+ } catch (e) {
+ console.log('Could not fetch releases:', e.message);
+ }
+
+ if (!sinceDate) {
+ const date = new Date();
+ date.setDate(date.getDate() - 90);
+ sinceDate = date.toISOString();
+ console.log(`Using fallback date: ${sinceDate}`);
+ }
+
+ const { data: prs } = await github.rest.pulls.list({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'closed',
+ sort: 'updated',
+ direction: 'desc',
+ per_page: 100
+ });
+
+ const mergedPRs = prs.filter(pr =>
+ pr.merged_at && new Date(pr.merged_at) > new Date(sinceDate)
+ );
+
+ console.log(`Found ${mergedPRs.length} merged PRs since ${sinceDate}`);
+
+ const features = [];
+ const bugfixes = [];
+ const models = [];
+ const performance = [];
+ const other = [];
+
+ for (const pr of mergedPRs) {
+ const labels = pr.labels.map(l => l.name.toLowerCase());
+ const title = pr.title;
+ const entry = `- ${title} ([#${pr.number}](${pr.html_url}))`;
+
+ if (labels.some(l => l.includes('bug') || l.includes('fix'))) {
+ bugfixes.push(entry);
+ } else if (labels.some(l => l.includes('model')) || title.toLowerCase().includes('model')) {
+ models.push(entry);
+ } else if (labels.some(l => l.includes('perf') || l.includes('optim'))) {
+ performance.push(entry);
+ } else if (labels.some(l => l.includes('feature') || l.includes('enhancement'))) {
+ features.push(entry);
+ } else {
+ other.push(entry);
+ }
+ }
+
+ const today = new Date().toISOString().split('T')[0];
+ let changelog = `## [${newVersion}] - ${today}\n\n`;
+
+ if (features.length > 0) {
+ changelog += '### Features\n\n' + features.join('\n') + '\n\n';
+ }
+ if (models.length > 0) {
+ changelog += '### Model Support\n\n' + models.join('\n') + '\n\n';
+ }
+ if (performance.length > 0) {
+ changelog += '### Performance\n\n' + performance.join('\n') + '\n\n';
+ }
+ if (bugfixes.length > 0) {
+ changelog += '### Bug Fixes\n\n' + bugfixes.join('\n') + '\n\n';
+ }
+ if (other.length > 0) {
+ changelog += '### Other Changes\n\n' + other.join('\n') + '\n\n';
+ }
+ if (mergedPRs.length === 0) {
+ changelog += '\n\n';
+ }
+
+ const fs = require('fs');
+ fs.writeFileSync('/tmp/changelog_entry.txt', changelog);
+ core.setOutput('pr_count', mergedPRs.length);
+
+ - name: Update CHANGELOG.md
+ run: |
+ CHANGELOG_FILE="CHANGELOG.md"
+
+ if [ ! -f "$CHANGELOG_FILE" ]; then
+ echo "# Changelog" > "$CHANGELOG_FILE"
+ echo "" >> "$CHANGELOG_FILE"
+ echo "All notable changes to GPULlama3.java will be documented in this file." >> "$CHANGELOG_FILE"
+ echo "" >> "$CHANGELOG_FILE"
+ fi
+
+ if grep -q "^## \[" "$CHANGELOG_FILE"; then
+ FIRST_VERSION_LINE=$(grep -n "^## \[" "$CHANGELOG_FILE" | head -1 | cut -d: -f1)
+ {
+ head -n $((FIRST_VERSION_LINE - 1)) "$CHANGELOG_FILE"
+ cat /tmp/changelog_entry.txt
+ tail -n +$FIRST_VERSION_LINE "$CHANGELOG_FILE"
+ } > /tmp/changelog_new.md
+ mv /tmp/changelog_new.md "$CHANGELOG_FILE"
+ else
+ cat /tmp/changelog_entry.txt >> "$CHANGELOG_FILE"
+ fi
+
+ echo "ā
Updated CHANGELOG.md"
+
+ - name: Show changes summary
+ run: |
+ echo "## š Release ${{ env.VERSION }} Preparation" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### Files Modified:" >> $GITHUB_STEP_SUMMARY
+ git diff --name-only | while read file; do
+ echo "- \`$file\`" >> $GITHUB_STEP_SUMMARY
+ done
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "### PRs included: ${{ steps.fetch_prs.outputs.pr_count }}" >> $GITHUB_STEP_SUMMARY
+
+ - name: Commit and push
+ if: ${{ inputs.dry_run == false }}
+ run: |
+ git add -A
+ git commit -m "Prepare release ${{ env.VERSION }}"
+ git push origin release/${{ env.VERSION }}
+
+ - name: Create Pull Request
+ if: ${{ inputs.dry_run == false }}
+ uses: actions/github-script@v7
+ with:
+ script: |
+ const version = process.env.VERSION;
+ const prCount = '${{ steps.fetch_prs.outputs.pr_count }}';
+
+ const { data: pr } = await github.rest.pulls.create({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ title: `Release ${version}`,
+ head: `release/${version}`,
+ base: 'main',
+ body: `## š Release ${version}
+
+ ### š Changes
+ - ${prCount} PRs included in changelog
+ - Version bumped to ${version}
+
+ ### ā
Review Checklist
+ - [ ] Version number correct in pom.xml
+ - [ ] CHANGELOG.md reviewed
+ - [ ] CI passes
+
+ ### š After Merge
+ 1. **Finalize Release** ā tag \`v${version}\` + GitHub release
+ 2. **Deploy to Maven Central** ā publish artifacts
+ `
+ });
+
+ console.log(`ā
Created PR #${pr.number}: ${pr.html_url}`);
+
+ const reviewers = ['mikepapadim', 'orionpapadakis', 'mairooni' , 'stratika' ,'kotselidis'];
+ try {
+ await github.rest.pulls.requestReviewers({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: pr.number,
+ reviewers: reviewers
+ });
+ } catch (e) {
+ console.log('Could not request reviewers:', e.message);
+ }
+
+ - name: Dry run output
+ if: ${{ inputs.dry_run == true }}
+ run: |
+ echo "š DRY RUN MODE"
+ echo ""
+ echo "=== Files changed ==="
+ git diff --stat
+ echo ""
+ echo "=== Changelog entry ==="
+ cat /tmp/changelog_entry.txt
diff --git a/docs/RELEASE-AUTOMATION.md b/docs/RELEASE-AUTOMATION.md
new file mode 100644
index 00000000..0dfd5922
--- /dev/null
+++ b/docs/RELEASE-AUTOMATION.md
@@ -0,0 +1,58 @@
+# GPULlama3.java Release Workflows
+
+GitHub Actions workflows for automating releases to Maven Central.
+
+## š Files
+
+Available in `.github/workflows/`:
+
+| File | Purpose |
+|------|---------|
+| `prepare-release.yml` | Creates release branch, bumps versions, generates changelog, opens PR |
+| `finalize-release.yml` | Creates git tag and GitHub Release when release PR merges |
+| `deploy-maven-central.yml` | Deploys to Maven Central when tag is pushed |
+
+## š Release Flow
+
+```
+1. PREPARE (manual trigger)
+ āāā Creates release/X.Y.Z branch + PR
+
+2. REVIEW & MERGE (manual)
+ āāā Review PR, CI runs, merge when ready
+
+3. FINALIZE (auto on PR merge)
+ āāā Creates tag vX.Y.Z + GitHub Release
+
+4. DEPLOY (auto on tag push)
+ āāā Publishes to Maven Central
+```
+
+## š Usage
+
+### Starting a Release
+
+1. Go to **Actions** ā **Prepare GPULlama3 Release**
+2. Click **Run workflow**
+3. Enter version (e.g., `0.2.3`) and previous version (e.g., `0.2.2`)
+4. Review and merge the created PR
+5. Everything else is automatic!
+
+### Manual Override
+
+```bash
+# Just create tag manually (skips prepare/finalize)
+git tag -a v0.2.3 -m "Release 0.2.3"
+git push origin v0.2.3
+# ā deploy-maven-central triggers automatically
+```
+
+## š Required Secrets
+
+| Secret | Description |
+|--------|-------------|
+| `OSSRH_USERNAME` | Maven Central username |
+| `OSSRH_TOKEN` | Maven Central token |
+| `GPG_PRIVATE_KEY` | `gpg --armor --export-secret-keys KEY_ID` |
+| `GPG_KEYNAME` | GPG key ID |
+| `GPG_PASSPHRASE` | GPG passphrase |