Skip to content

Merge pull request #3 from FacturAPI/feat/update-default-series #16

Merge pull request #3 from FacturAPI/feat/update-default-series

Merge pull request #3 from FacturAPI/feat/update-default-series #16

Workflow file for this run

name: Publish Java SDK
on:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: write
jobs:
publish:
name: Publish to Maven Central
runs-on: ubuntu-latest
env:
CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
CENTRAL_GROUP_ID: io.facturapi
CENTRAL_ARTIFACT_ID: facturapi-java
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
cache: maven
server-id: central
server-username: CENTRAL_TOKEN_USERNAME
server-password: CENTRAL_TOKEN_PASSWORD
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: GPG_PASSPHRASE
- name: Check release gate
id: gate
shell: bash
run: |
set -euo pipefail
python3 -m pip install --disable-pip-version-check --quiet semver
python3 <<'PY' >> "$GITHUB_OUTPUT"
import base64
import os
import sys
import urllib.request
import urllib.error
import xml.etree.ElementTree as ET
import semver
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
pom = ET.parse("pom.xml").getroot()
version = pom.findtext("m:version", namespaces=ns)
if version is None:
print("publish=false")
print("reason=missing version")
sys.exit(0)
if version.endswith("-SNAPSHOT"):
print("publish=false")
print(f"current_version={version}")
print("reason=snapshot version")
sys.exit(0)
metadata_url = "https://repo.maven.apache.org/maven2/io/facturapi/facturapi-java/maven-metadata.xml"
latest = "0.0.0"
try:
with urllib.request.urlopen(metadata_url, timeout=20) as response:
metadata = ET.fromstring(response.read())
latest = metadata.findtext("./versioning/release") or metadata.findtext("./versioning/latest") or latest
except Exception:
latest = "0.0.0"
def to_semver(value: str):
try:
return semver.Version.parse(value)
except ValueError:
return None
current_semver = to_semver(version)
latest_semver = to_semver(latest) or semver.Version.parse("0.0.0")
if current_semver is None:
print(f"current_version={version}")
print(f"latest_version={latest}")
print("publish=false")
print("reason=current version is not valid semver")
sys.exit(0)
publish = current_semver > latest_semver
reason = "current version is newer than published version"
if publish:
# If a deployment for this exact version is already staged/ongoing in the Portal,
# skip a duplicate publish attempt and leave the workflow green.
username = os.environ.get("CENTRAL_TOKEN_USERNAME", "")
password = os.environ.get("CENTRAL_TOKEN_PASSWORD", "")
group_id = os.environ.get("CENTRAL_GROUP_ID", "io.facturapi")
artifact_id = os.environ.get("CENTRAL_ARTIFACT_ID", "facturapi-java")
if username and password:
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
rel_path = f"{group_id.replace('.', '/')}/{artifact_id}/{version}/{artifact_id}-{version}.pom"
url = f"https://central.sonatype.com/api/v1/publisher/deployments/download/{rel_path}"
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}, method="HEAD")
try:
with urllib.request.urlopen(req, timeout=20) as response:
if response.status == 200:
publish = False
reason = "version already staged or publishing in Central Portal"
except urllib.error.HTTPError as err:
if err.code != 404:
print(f"portal_stage_check_status={err.code}")
except Exception:
pass
print(f"current_version={version}")
print(f"latest_version={latest}")
print(f"publish={'true' if publish else 'false'}")
if publish:
reason = "current version is newer than published version"
elif reason == "current version is newer than published version":
reason = "published version is current or newer"
print(f"reason={reason}")
PY
- name: Publish
id: publish_step
if: steps.gate.outputs.publish == 'true'
run: mvn -B -ntp -DskipTests -DpublishRelease=true deploy
- name: Ensure release tag is in sync
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
env:
VERSION: ${{ steps.gate.outputs.current_version }}
shell: bash
run: |
set -euo pipefail
tag="v${VERSION}"
current_sha="$(git rev-parse HEAD)"
if git rev-parse "$tag" >/dev/null 2>&1; then
tag_sha="$(git rev-list -n 1 "$tag")"
if [ "$tag_sha" != "$current_sha" ]; then
echo "::error::Tag $tag already exists at $tag_sha, expected $current_sha"
exit 1
fi
echo "Tag $tag already in sync with $current_sha"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$tag" -m "Release $tag"
git push origin "$tag"
echo "Created and pushed tag $tag"
- name: Create or update GitHub release notes
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ steps.gate.outputs.current_version }}
shell: bash
run: |
set -euo pipefail
tag="v${VERSION}"
notes_file="/tmp/release_notes.md"
python3 - <<'PY'
import os
from pathlib import Path
version = os.environ["VERSION"]
header = f"## [{version}]"
changelog = Path("CHANGELOG.md")
output = Path("/tmp/release_notes.md")
if not changelog.exists():
output.write_text(f"Release {version}", encoding="utf-8")
raise SystemExit(0)
lines = changelog.read_text(encoding="utf-8").splitlines()
in_target = False
captured = []
for line in lines:
if line.startswith(header):
in_target = True
continue
if in_target and line.startswith("## ["):
break
if in_target:
captured.append(line)
notes = "\n".join(captured).strip()
if not notes:
notes = f"Release {version}"
output.write_text(notes + "\n", encoding="utf-8")
PY
if gh release view "$tag" >/dev/null 2>&1; then
gh release edit "$tag" --title "$tag" --notes-file "$notes_file"
echo "Updated release $tag"
else
gh release create "$tag" --title "$tag" --notes-file "$notes_file"
echo "Created release $tag"
fi
- name: Notify Slack (success)
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
env:
SLACK_DEPLOY_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
VERSION: ${{ steps.gate.outputs.current_version }}
shell: bash
run: |
set -euo pipefail
if [ -z "${SLACK_DEPLOY_WEBHOOK_URL:-}" ]; then
echo "SLACK_DEPLOY_WEBHOOK_URL not set; skipping Slack notification"
exit 0
fi
python3 - <<'PY'
import json
import os
from pathlib import Path
version = os.environ["VERSION"]
ref_name = os.environ["GITHUB_REF_NAME"]
sha = os.environ["GITHUB_SHA"]
actor = os.environ["GITHUB_ACTOR"]
server_url = os.environ["GITHUB_SERVER_URL"]
repo = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]
changelog = Path("CHANGELOG.md")
latest_changes = "See CHANGELOG for details."
section_header = f"## [{version}]"
if changelog.exists():
lines = changelog.read_text(encoding="utf-8").splitlines()
in_target = False
bullets = []
for line in lines:
if line.startswith(section_header):
in_target = True
continue
if in_target and line.startswith("## ["):
break
if in_target and line.startswith("- "):
bullets.append(line[2:].strip())
if bullets:
latest_changes = "\n".join(f"• {b}" for b in bullets[:5])
payload = {
"text": f"🚀 Facturapi Java SDK {version} validated and submitted",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": f"🚀 Java SDK {version} validated and submitted to Maven Central"
}
},
{
"type": "section",
"fields": [
{"type": "mrkdwn", "text": f"*Package:* `io.facturapi:facturapi-java:{version}`"},
{"type": "mrkdwn", "text": f"*Branch:* `{ref_name}`"},
{"type": "mrkdwn", "text": f"*Commit:* `{sha}`"},
{"type": "mrkdwn", "text": f"*Actor:* `{actor}`"}
]
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
"*Useful links*\n"
f"• Maven Central: <https://central.sonatype.com/artifact/io.facturapi/facturapi-java/{version}|View artifact>\n"
f"• Central deployment status: <https://central.sonatype.com/publishing/deployments|Check progress>\n"
f"• Workflow run: <{server_url}/{repo}/actions/runs/{run_id}|Open run>\n"
f"• Changelog: <{server_url}/{repo}/blob/{sha}/CHANGELOG.md|Read changes>"
)
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Latest changes*\n{latest_changes}"
}
}
]
}
Path("/tmp/slack_payload.json").write_text(json.dumps(payload), encoding="utf-8")
PY
curl -sS -X POST -H "Content-type: application/json" --data "@/tmp/slack_payload.json" "$SLACK_DEPLOY_WEBHOOK_URL" || true
- name: Notify Slack (failure)
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'failure'
env:
SLACK_DEPLOY_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
VERSION: ${{ steps.gate.outputs.current_version }}
shell: bash
run: |
set -euo pipefail
if [ -z "${SLACK_DEPLOY_WEBHOOK_URL:-}" ]; then
echo "SLACK_DEPLOY_WEBHOOK_URL not set; skipping Slack notification"
exit 0
fi
python3 - <<'PY'
import json
import os
from pathlib import Path
version = os.environ["VERSION"]
server_url = os.environ["GITHUB_SERVER_URL"]
repo = os.environ["GITHUB_REPOSITORY"]
run_id = os.environ["GITHUB_RUN_ID"]
sha = os.environ["GITHUB_SHA"]
payload = {
"text": f"❌ Facturapi Java SDK {version} publish failed",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": (
f"❌ *Java SDK {version} publish failed*\n"
f"• Workflow run: <{server_url}/{repo}/actions/runs/{run_id}|Open run>\n"
f"• Commit: `{sha}`"
)
}
}
]
}
Path("/tmp/slack_payload_failure.json").write_text(json.dumps(payload), encoding="utf-8")
PY
curl -sS -X POST -H "Content-type: application/json" --data "@/tmp/slack_payload_failure.json" "$SLACK_DEPLOY_WEBHOOK_URL" || true
- name: Skip publish
if: steps.gate.outputs.publish != 'true'
run: |
echo "Skipping publish: ${{ steps.gate.outputs.reason }} (${{ steps.gate.outputs.current_version }} vs ${{ steps.gate.outputs.latest_version }})"