Skip to content

Commit 2fa6bb7

Browse files
committed
Merge main into feat/update-default-series
2 parents ed4bf72 + ea2c770 commit 2fa6bb7

3 files changed

Lines changed: 268 additions & 87 deletions

File tree

.github/workflows/publish.yml

Lines changed: 245 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ on:
44
push:
55
branches:
66
- main
7+
workflow_dispatch:
78

89
permissions:
910
contents: write
@@ -16,6 +17,8 @@ jobs:
1617
CENTRAL_TOKEN_USERNAME: ${{ secrets.CENTRAL_TOKEN_USERNAME }}
1718
CENTRAL_TOKEN_PASSWORD: ${{ secrets.CENTRAL_TOKEN_PASSWORD }}
1819
GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
20+
CENTRAL_GROUP_ID: io.facturapi
21+
CENTRAL_ARTIFACT_ID: facturapi-java
1922
steps:
2023
- name: Checkout
2124
uses: actions/checkout@v4
@@ -40,10 +43,16 @@ jobs:
4043
run: |
4144
set -euo pipefail
4245
46+
python3 -m pip install --disable-pip-version-check --quiet semver
47+
4348
python3 <<'PY' >> "$GITHUB_OUTPUT"
49+
import base64
50+
import os
4451
import sys
4552
import urllib.request
53+
import urllib.error
4654
import xml.etree.ElementTree as ET
55+
import semver
4756
4857
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
4958
pom = ET.parse("pom.xml").getroot()
@@ -68,119 +77,271 @@ jobs:
6877
except Exception:
6978
latest = "0.0.0"
7079
71-
def parse_semver(value: str):
72-
core = value.split("+", 1)[0]
73-
main, _, prerelease = core.partition("-")
74-
major, minor, patch = (int(part) for part in main.split(".")[:3])
75-
return major, minor, patch, prerelease
76-
77-
def is_greater(left: str, right: str) -> bool:
78-
left_parts = parse_semver(left)
79-
right_parts = parse_semver(right)
80-
if left_parts[:3] != right_parts[:3]:
81-
return left_parts[:3] > right_parts[:3]
82-
left_pre = left_parts[3]
83-
right_pre = right_parts[3]
84-
if left_pre == right_pre:
85-
return False
86-
if not left_pre:
87-
return True
88-
if not right_pre:
89-
return False
90-
return left_pre > right_pre
91-
92-
def is_greater_or_equal(left: str, right: str) -> bool:
93-
left_parts = parse_semver(left)
94-
right_parts = parse_semver(right)
95-
if left_parts[:3] != right_parts[:3]:
96-
return left_parts[:3] > right_parts[:3]
97-
left_pre = left_parts[3]
98-
right_pre = right_parts[3]
99-
if left_pre == right_pre:
100-
return True
101-
if not left_pre:
102-
return True
103-
if not right_pre:
104-
return False
105-
return left_pre >= right_pre
106-
107-
publish = is_greater(version, latest)
108-
release = is_greater_or_equal(version, latest)
80+
def to_semver(value: str):
81+
try:
82+
return semver.Version.parse(value)
83+
except ValueError:
84+
return None
85+
86+
current_semver = to_semver(version)
87+
latest_semver = to_semver(latest) or semver.Version.parse("0.0.0")
88+
if current_semver is None:
89+
print(f"current_version={version}")
90+
print(f"latest_version={latest}")
91+
print("publish=false")
92+
print("reason=current version is not valid semver")
93+
sys.exit(0)
94+
95+
publish = current_semver > latest_semver
96+
reason = "current version is newer than published version"
97+
if publish:
98+
# If a deployment for this exact version is already staged/ongoing in the Portal,
99+
# skip a duplicate publish attempt and leave the workflow green.
100+
username = os.environ.get("CENTRAL_TOKEN_USERNAME", "")
101+
password = os.environ.get("CENTRAL_TOKEN_PASSWORD", "")
102+
group_id = os.environ.get("CENTRAL_GROUP_ID", "io.facturapi")
103+
artifact_id = os.environ.get("CENTRAL_ARTIFACT_ID", "facturapi-java")
104+
if username and password:
105+
token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8")
106+
rel_path = f"{group_id.replace('.', '/')}/{artifact_id}/{version}/{artifact_id}-{version}.pom"
107+
url = f"https://central.sonatype.com/api/v1/publisher/deployments/download/{rel_path}"
108+
req = urllib.request.Request(url, headers={"Authorization": f"Bearer {token}"}, method="HEAD")
109+
try:
110+
with urllib.request.urlopen(req, timeout=20) as response:
111+
if response.status == 200:
112+
publish = False
113+
reason = "version already staged or publishing in Central Portal"
114+
except urllib.error.HTTPError as err:
115+
if err.code != 404:
116+
print(f"portal_stage_check_status={err.code}")
117+
except Exception:
118+
pass
119+
109120
print(f"current_version={version}")
110-
print(f"release_tag=v{version}")
111121
print(f"latest_version={latest}")
112122
print(f"publish={'true' if publish else 'false'}")
113-
print(f"release={'true' if release else 'false'}")
114-
print(f"reason={'current version is newer than published version' if publish else 'published version is current or newer'}")
123+
if publish:
124+
reason = "current version is newer than published version"
125+
elif reason == "current version is newer than published version":
126+
reason = "published version is current or newer"
127+
print(f"reason={reason}")
115128
PY
116129
117-
- name: Extract release notes
118-
if: steps.gate.outputs.release == 'true'
119-
id: notes
130+
- name: Publish
131+
id: publish_step
132+
if: steps.gate.outputs.publish == 'true'
133+
run: mvn -B -ntp -DskipTests -DpublishRelease=true deploy
134+
135+
- name: Ensure release tag is in sync
136+
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
120137
env:
121-
RELEASE_VERSION: ${{ steps.gate.outputs.current_version }}
138+
VERSION: ${{ steps.gate.outputs.current_version }}
122139
shell: bash
123140
run: |
124141
set -euo pipefail
142+
tag="v${VERSION}"
143+
current_sha="$(git rev-parse HEAD)"
144+
145+
if git rev-parse "$tag" >/dev/null 2>&1; then
146+
tag_sha="$(git rev-list -n 1 "$tag")"
147+
if [ "$tag_sha" != "$current_sha" ]; then
148+
echo "::error::Tag $tag already exists at $tag_sha, expected $current_sha"
149+
exit 1
150+
fi
151+
echo "Tag $tag already in sync with $current_sha"
152+
exit 0
153+
fi
154+
155+
git config user.name "github-actions[bot]"
156+
git config user.email "github-actions[bot]@users.noreply.github.com"
157+
git tag -a "$tag" -m "Release $tag"
158+
git push origin "$tag"
159+
echo "Created and pushed tag $tag"
125160
126-
python3 <<'PY'
161+
- name: Create or update GitHub release notes
162+
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
163+
env:
164+
GH_TOKEN: ${{ github.token }}
165+
VERSION: ${{ steps.gate.outputs.current_version }}
166+
shell: bash
167+
run: |
168+
set -euo pipefail
169+
tag="v${VERSION}"
170+
notes_file="/tmp/release_notes.md"
171+
172+
python3 - <<'PY'
127173
import os
128174
from pathlib import Path
129175
130-
version = os.environ["RELEASE_VERSION"]
176+
version = os.environ["VERSION"]
177+
header = f"## [{version}]"
131178
changelog = Path("CHANGELOG.md")
179+
output = Path("/tmp/release_notes.md")
180+
132181
if not changelog.exists():
133-
raise SystemExit("CHANGELOG.md is required to publish a release.")
182+
output.write_text(f"Release {version}", encoding="utf-8")
183+
raise SystemExit(0)
134184
135-
section_header = f"## [{version}]"
136185
lines = changelog.read_text(encoding="utf-8").splitlines()
137-
section = []
138-
in_section = False
139-
186+
in_target = False
187+
captured = []
140188
for line in lines:
141-
if line.startswith("## ["):
142-
if in_section:
143-
break
144-
if line.startswith(section_header):
145-
in_section = True
146-
if in_section:
147-
section.append(line)
189+
if line.startswith(header):
190+
in_target = True
191+
continue
192+
if in_target and line.startswith("## ["):
193+
break
194+
if in_target:
195+
captured.append(line)
148196
149-
if not section:
150-
raise SystemExit(f"Could not find changelog entry for version {version}.")
197+
notes = "\n".join(captured).strip()
198+
if not notes:
199+
notes = f"Release {version}"
200+
output.write_text(notes + "\n", encoding="utf-8")
201+
PY
151202
152-
notes_path = Path("release-notes.md")
153-
notes_path.write_text("\n".join(section).strip() + "\n", encoding="utf-8")
203+
if gh release view "$tag" >/dev/null 2>&1; then
204+
gh release edit "$tag" --title "$tag" --notes-file "$notes_file"
205+
echo "Updated release $tag"
206+
else
207+
gh release create "$tag" --title "$tag" --notes-file "$notes_file"
208+
echo "Created release $tag"
209+
fi
154210
155-
output_path = Path(os.environ["GITHUB_OUTPUT"])
156-
with output_path.open("a", encoding="utf-8") as output:
157-
output.write(f"notes_file={notes_path}\n")
211+
- name: Notify Slack (success)
212+
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'success'
213+
env:
214+
SLACK_DEPLOY_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
215+
VERSION: ${{ steps.gate.outputs.current_version }}
216+
shell: bash
217+
run: |
218+
set -euo pipefail
219+
if [ -z "${SLACK_DEPLOY_WEBHOOK_URL:-}" ]; then
220+
echo "SLACK_DEPLOY_WEBHOOK_URL not set; skipping Slack notification"
221+
exit 0
222+
fi
223+
224+
python3 - <<'PY'
225+
import json
226+
import os
227+
from pathlib import Path
228+
229+
version = os.environ["VERSION"]
230+
ref_name = os.environ["GITHUB_REF_NAME"]
231+
sha = os.environ["GITHUB_SHA"]
232+
actor = os.environ["GITHUB_ACTOR"]
233+
server_url = os.environ["GITHUB_SERVER_URL"]
234+
repo = os.environ["GITHUB_REPOSITORY"]
235+
run_id = os.environ["GITHUB_RUN_ID"]
236+
237+
changelog = Path("CHANGELOG.md")
238+
latest_changes = "See CHANGELOG for details."
239+
section_header = f"## [{version}]"
240+
if changelog.exists():
241+
lines = changelog.read_text(encoding="utf-8").splitlines()
242+
in_target = False
243+
bullets = []
244+
for line in lines:
245+
if line.startswith(section_header):
246+
in_target = True
247+
continue
248+
if in_target and line.startswith("## ["):
249+
break
250+
if in_target and line.startswith("- "):
251+
bullets.append(line[2:].strip())
252+
if bullets:
253+
latest_changes = "\n".join(f"• {b}" for b in bullets[:5])
254+
255+
payload = {
256+
"text": f"🚀 Facturapi Java SDK {version} validated and submitted",
257+
"blocks": [
258+
{
259+
"type": "header",
260+
"text": {
261+
"type": "plain_text",
262+
"text": f"🚀 Java SDK {version} validated and submitted to Maven Central"
263+
}
264+
},
265+
{
266+
"type": "section",
267+
"fields": [
268+
{"type": "mrkdwn", "text": f"*Package:* `io.facturapi:facturapi-java:{version}`"},
269+
{"type": "mrkdwn", "text": f"*Branch:* `{ref_name}`"},
270+
{"type": "mrkdwn", "text": f"*Commit:* `{sha}`"},
271+
{"type": "mrkdwn", "text": f"*Actor:* `{actor}`"}
272+
]
273+
},
274+
{
275+
"type": "section",
276+
"text": {
277+
"type": "mrkdwn",
278+
"text": (
279+
"*Useful links*\n"
280+
f"• Maven Central: <https://central.sonatype.com/artifact/io.facturapi/facturapi-java/{version}|View artifact>\n"
281+
f"• Central deployment status: <https://central.sonatype.com/publishing/deployments|Check progress>\n"
282+
f"• Workflow run: <{server_url}/{repo}/actions/runs/{run_id}|Open run>\n"
283+
f"• Changelog: <{server_url}/{repo}/blob/{sha}/CHANGELOG.md|Read changes>"
284+
)
285+
}
286+
},
287+
{
288+
"type": "section",
289+
"text": {
290+
"type": "mrkdwn",
291+
"text": f"*Latest changes*\n{latest_changes}"
292+
}
293+
}
294+
]
295+
}
296+
297+
Path("/tmp/slack_payload.json").write_text(json.dumps(payload), encoding="utf-8")
158298
PY
159299
160-
- name: Publish
161-
if: steps.gate.outputs.publish == 'true'
162-
run: mvn -B -ntp -DskipTests -DpublishRelease=true deploy
300+
curl -sS -X POST -H "Content-type: application/json" --data "@/tmp/slack_payload.json" "$SLACK_DEPLOY_WEBHOOK_URL" || true
163301
164-
- name: Create or update GitHub Release
165-
if: steps.gate.outputs.release == 'true'
302+
- name: Notify Slack (failure)
303+
if: steps.gate.outputs.publish == 'true' && steps.publish_step.outcome == 'failure'
166304
env:
167-
GH_TOKEN: ${{ github.token }}
168-
RELEASE_TAG: ${{ steps.gate.outputs.release_tag }}
169-
RELEASE_NOTES_FILE: ${{ steps.notes.outputs.notes_file }}
305+
SLACK_DEPLOY_WEBHOOK_URL: ${{ secrets.SLACK_DEPLOY_WEBHOOK_URL }}
306+
VERSION: ${{ steps.gate.outputs.current_version }}
170307
shell: bash
171308
run: |
172309
set -euo pipefail
173-
174-
if gh release view "$RELEASE_TAG" >/dev/null 2>&1; then
175-
gh release edit "$RELEASE_TAG" \
176-
--title "$RELEASE_TAG" \
177-
--notes-file "$RELEASE_NOTES_FILE"
178-
else
179-
gh release create "$RELEASE_TAG" \
180-
--target "$GITHUB_SHA" \
181-
--title "$RELEASE_TAG" \
182-
--notes-file "$RELEASE_NOTES_FILE"
310+
if [ -z "${SLACK_DEPLOY_WEBHOOK_URL:-}" ]; then
311+
echo "SLACK_DEPLOY_WEBHOOK_URL not set; skipping Slack notification"
312+
exit 0
183313
fi
314+
python3 - <<'PY'
315+
import json
316+
import os
317+
from pathlib import Path
318+
319+
version = os.environ["VERSION"]
320+
server_url = os.environ["GITHUB_SERVER_URL"]
321+
repo = os.environ["GITHUB_REPOSITORY"]
322+
run_id = os.environ["GITHUB_RUN_ID"]
323+
sha = os.environ["GITHUB_SHA"]
324+
325+
payload = {
326+
"text": f"❌ Facturapi Java SDK {version} publish failed",
327+
"blocks": [
328+
{
329+
"type": "section",
330+
"text": {
331+
"type": "mrkdwn",
332+
"text": (
333+
f"❌ *Java SDK {version} publish failed*\n"
334+
f"• Workflow run: <{server_url}/{repo}/actions/runs/{run_id}|Open run>\n"
335+
f"• Commit: `{sha}`"
336+
)
337+
}
338+
}
339+
]
340+
}
341+
342+
Path("/tmp/slack_payload_failure.json").write_text(json.dumps(payload), encoding="utf-8")
343+
PY
344+
curl -sS -X POST -H "Content-type: application/json" --data "@/tmp/slack_payload_failure.json" "$SLACK_DEPLOY_WEBHOOK_URL" || true
184345
185346
- name: Skip publish
186347
if: steps.gate.outputs.publish != 'true'

0 commit comments

Comments
 (0)