diff --git a/.github/scripts/github_actions_utils.py b/.github/scripts/github_actions_utils.py new file mode 100644 index 00000000..8b90c6c4 --- /dev/null +++ b/.github/scripts/github_actions_utils.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Shared helpers for GitHub Actions scripts.""" + +from __future__ import annotations + +import os + + +def write_output(key: str, value: str) -> None: + output_path = os.environ.get("GITHUB_OUTPUT", "").strip() + if not output_path: + return + with open(output_path, "a", encoding="utf-8") as output: + output.write(f"{key}={value}\n") diff --git a/.github/scripts/release_extract_upload_context.py b/.github/scripts/release_extract_upload_context.py new file mode 100644 index 00000000..52981515 --- /dev/null +++ b/.github/scripts/release_extract_upload_context.py @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Extract uploaded artifact URLs from Maven deploy logs.""" + +from __future__ import annotations + +import json +import os +import re +from pathlib import Path + +from github_actions_utils import write_output + + +def main() -> int: + repository_name = os.environ.get("TARGET_REPOSITORY", "").strip() + log_file = Path(os.environ.get("DEPLOY_LOG", "maven-deploy.log")) + context_file = Path(os.environ.get("DEPLOY_ARTIFACTS_FILE", "deploy-artifacts.json")) + + log_text = log_file.read_text(encoding="utf-8") + pattern = re.compile(r"Uploaded to (\S+):\s+(\S+)") + + uploaded_urls: list[str] = [] + for target_repo, url in pattern.findall(log_text): + if target_repo == repository_name: + uploaded_urls.append(url) + + deduped_urls = sorted(set(uploaded_urls)) + jar_urls = [url for url in deduped_urls if url.endswith(".jar")] + pom_urls = [url for url in deduped_urls if url.endswith(".pom")] + + payload = { + "target_repository": repository_name, + "uploaded_urls": deduped_urls, + "jar_urls": jar_urls, + "pom_urls": pom_urls, + } + context_file.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8") + + write_output("uploaded_urls_count", str(len(deduped_urls))) + write_output("jar_urls_count", str(len(jar_urls))) + write_output("pom_urls_count", str(len(pom_urls))) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/release_resolve_repository_context.py b/.github/scripts/release_resolve_repository_context.py new file mode 100644 index 00000000..226bb99a --- /dev/null +++ b/.github/scripts/release_resolve_repository_context.py @@ -0,0 +1,160 @@ +#!/usr/bin/env python3 +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Resolve Sonatype repository context for release deployments.""" + +from __future__ import annotations + +import base64 +import json +import os +import urllib.error +import urllib.parse +import urllib.request +from pathlib import Path +from typing import Any + +from github_actions_utils import write_output + +OSSRH_BASE = "https://ossrh-staging-api.central.sonatype.com" + + +def request_json(url: str, headers: dict[str, str]) -> tuple[int | None, dict[str, Any]]: + request = urllib.request.Request(url=url, method="GET", headers=headers) + try: + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read().decode("utf-8") + if not body: + return response.status, {} + try: + return response.status, json.loads(body) + except json.JSONDecodeError: + return response.status, {"raw": body} + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8") + try: + payload = json.loads(body) if body else {} + except json.JSONDecodeError: + payload = {"raw": body} + payload.setdefault("error", f"HTTP {error.code}") + return error.code, payload + except Exception as error: # noqa: BLE001 + return None, {"error": str(error)} + + +def main() -> int: + target_repository = os.environ.get("TARGET_REPOSITORY", "").strip() + namespace = os.environ.get("TARGET_NAMESPACE", "").strip() + username = os.environ.get("MAVEN_USERNAME", "") + password = os.environ.get("MAVEN_CENTRAL_TOKEN", "") + context_path = Path( + os.environ.get("REPOSITORY_CONTEXT_FILE", "repository-context.json") + ) + + context: dict[str, Any] = { + "target_repository": target_repository, + "namespace": namespace, + "status": "not_applicable", + "reason": "repository input is not releases", + "repository_key": "", + "portal_deployment_id": "", + "search_candidates": [], + } + + if target_repository == "releases": + if not username or not password: + context["status"] = "manual_required" + context["reason"] = "Missing MAVEN_USERNAME/MAVEN_CENTRAL_TOKEN" + else: + token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8") + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/json", + } + + searches = [ + ("open", "client"), + ("closed", "client"), + ("open", "any"), + ("closed", "any"), + ] + selected: dict[str, Any] | None = None + last_error = "" + + for state, ip in searches: + url = ( + f"{OSSRH_BASE}/manual/search/repositories?" + f"profile_id={urllib.parse.quote(namespace)}" + f"&state={urllib.parse.quote(state)}" + f"&ip={urllib.parse.quote(ip)}" + ) + status, payload = request_json(url, headers) + if status is None: + last_error = payload.get("error", "unknown error") + context["search_candidates"].append( + { + "state": state, + "ip": ip, + "status": None, + "count": 0, + "error": last_error, + } + ) + continue + + if status < 200 or status >= 300: + http_error = payload.get("error", f"HTTP {status}") + last_error = http_error + context["search_candidates"].append( + { + "state": state, + "ip": ip, + "status": status, + "count": 0, + "error": http_error, + } + ) + continue + + repositories = ( + payload.get("repositories", []) if isinstance(payload, dict) else [] + ) + context["search_candidates"].append( + {"state": state, "ip": ip, "status": status, "count": len(repositories)} + ) + if repositories: + selected = repositories[0] + break + + if selected: + context["status"] = "resolved" + context["reason"] = "" + context["repository_key"] = selected.get("key", "") or "" + context["portal_deployment_id"] = ( + selected.get("portal_deployment_id", "") or "" + ) + else: + context["status"] = "manual_required" + context["reason"] = last_error or "No staging repository key found" + + context_path.write_text(json.dumps(context, indent=2) + "\n", encoding="utf-8") + write_output("repository_key", context.get("repository_key", "")) + write_output("portal_deployment_id", context.get("portal_deployment_id", "")) + write_output("status", context.get("status", "")) + write_output("reason", context.get("reason", "")) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/release_write_summary.py b/.github/scripts/release_write_summary.py new file mode 100644 index 00000000..4558e5db --- /dev/null +++ b/.github/scripts/release_write_summary.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Write release publish context summary for GitHub Actions.""" + +from __future__ import annotations + +import json +import os +from pathlib import Path +from typing import Any + + +def read_json(path: Path) -> dict[str, Any]: + if not path.exists(): + return {} + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError: + return {} + + +def main() -> int: + summary_path = os.environ.get("GITHUB_STEP_SUMMARY", "").strip() + if not summary_path: + return 0 + + deploy_path = Path(os.environ.get("DEPLOY_ARTIFACTS_FILE", "deploy-artifacts.json")) + repository_path = Path( + os.environ.get("REPOSITORY_CONTEXT_FILE", "repository-context.json") + ) + + deploy = read_json(deploy_path) + repository = read_json(repository_path) + + lines = [ + "## Publish Context", + "", + f"- target repository: {deploy.get('target_repository', '')}", + f"- uploaded URLs: {len(deploy.get('uploaded_urls', []))}", + f"- jar URLs: {len(deploy.get('jar_urls', []))}", + f"- pom URLs: {len(deploy.get('pom_urls', []))}", + f"- staging key status: {repository.get('status', '')}", + f"- repository_key: {repository.get('repository_key', '')}", + f"- portal_deployment_id: {repository.get('portal_deployment_id', '')}", + ] + reason = repository.get("reason", "") + if reason: + lines.append(f"- reason: {reason}") + + with open(summary_path, "a", encoding="utf-8") as output: + output.write("\n".join(lines) + "\n") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/scripts/sonatype_publish.py b/.github/scripts/sonatype_publish.py new file mode 100644 index 00000000..559c57d5 --- /dev/null +++ b/.github/scripts/sonatype_publish.py @@ -0,0 +1,285 @@ +#!/usr/bin/env python3 +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Trigger and monitor Sonatype portal publish flow.""" + +from __future__ import annotations + +import base64 +import json +import os +import time +import urllib.error +import urllib.parse +import urllib.request +from typing import Any + +from github_actions_utils import write_output + +OSSRH_BASE = "https://ossrh-staging-api.central.sonatype.com" +PORTAL_BASE = "https://central.sonatype.com" + + +def request_json( + method: str, + url: str, + headers: dict[str, str], +) -> tuple[int | None, dict[str, Any]]: + request = urllib.request.Request(url=url, method=method, headers=headers) + try: + with urllib.request.urlopen(request, timeout=30) as response: + body = response.read().decode("utf-8") + if not body: + return response.status, {} + try: + return response.status, json.loads(body) + except json.JSONDecodeError: + return response.status, {"raw": body} + except urllib.error.HTTPError as error: + body = error.read().decode("utf-8") + try: + payload = json.loads(body) if body else {} + except json.JSONDecodeError: + payload = {"raw": body} + payload.setdefault("error", f"HTTP {error.code}") + return error.code, payload + except Exception as error: # noqa: BLE001 + return None, {"error": str(error)} + + +def extract_deployment_state(payload: dict[str, Any]) -> str: + for key in ("deploymentState", "deployment_state", "state"): + value = payload.get(key) + if isinstance(value, str): + return value + return "unknown" + + +def to_int(value: str, default: int) -> int: + try: + return int(value) + except ValueError: + return default + + +def main() -> int: + namespace = os.environ.get("INPUT_NAMESPACE", "com.ctrip.framework.apollo").strip() + repository_key = os.environ.get("INPUT_REPOSITORY_KEY", "").strip() + timeout_minutes = to_int(os.environ.get("INPUT_TIMEOUT_MINUTES", "60"), 60) + mode = os.environ.get("INPUT_MODE", "portal_api").strip().lower() + + username = os.environ.get("MAVEN_USERNAME", "") + password = os.environ.get("MAVEN_CENTRAL_TOKEN", "") + + result = "manual_required" + final_state = "unknown" + deployment_id = "" + deployment_url = "" + reason = "" + + if not username or not password: + reason = "Missing MAVEN_USERNAME/MAVEN_CENTRAL_TOKEN secrets" + else: + token = base64.b64encode(f"{username}:{password}".encode("utf-8")).decode("utf-8") + headers = { + "Authorization": f"Bearer {token}", + "Accept": "application/json", + } + + if not repository_key: + searches = ("open", "closed") + search_errors: list[str] = [] + successful_search = False + for state in searches: + search_url = ( + f"{OSSRH_BASE}/manual/search/repositories?" + f"ip=any&profile_id={urllib.parse.quote(namespace)}" + f"&state={urllib.parse.quote(state)}" + ) + search_status, payload = request_json("GET", search_url, headers) + if search_status is None or search_status < 200 or search_status >= 300: + search_error = payload.get("error") if isinstance(payload, dict) else "" + if not search_error: + search_error = ( + f"HTTP {search_status}" + if search_status is not None + else "HTTP unknown" + ) + search_errors.append( + f"Repository search failed ({state}): {search_error}" + ) + continue + + successful_search = True + repositories = payload.get("repositories", []) if isinstance(payload, dict) else [] + if repositories: + repository_key = repositories[0].get("key", "") or "" + break + + if not repository_key: + if search_errors and not successful_search: + reason = "; ".join(search_errors) + else: + reason = "No staging repository key found" + else: + upload_url = ( + f"{OSSRH_BASE}/manual/upload/repository/{urllib.parse.quote(repository_key)}" + f"?publishing_type={urllib.parse.quote(mode)}" + ) + upload_status, upload_payload = request_json("POST", upload_url, headers) + if upload_status is None: + reason = f"Upload API failed: {upload_payload.get('error', 'unknown error')}" + elif upload_status >= 400: + upload_error = upload_payload.get("error") + if not upload_error: + upload_error = f"HTTP {upload_status}" + reason = ( + f"Upload API failed: {upload_error}" + ) + + if not reason: + list_url = ( + f"{OSSRH_BASE}/manual/search/repositories?" + f"ip=any&profile_id={urllib.parse.quote(namespace)}" + ) + list_status, list_payload = request_json("GET", list_url, headers) + if list_status is None or list_status < 200 or list_status >= 300: + list_error = list_payload.get("error") if isinstance(list_payload, dict) else "" + if not list_error: + list_error = ( + f"HTTP {list_status}" + if list_status is not None + else "HTTP unknown" + ) + reason = f"Repository list API failed after upload: {list_error}" + else: + repositories = ( + list_payload.get("repositories", []) + if isinstance(list_payload, dict) + else [] + ) + for item in repositories: + if item.get("key") == repository_key and item.get("portal_deployment_id"): + deployment_id = item.get("portal_deployment_id") + break + + if deployment_id: + deployment_url = f"{PORTAL_BASE}/publishing/deployments/{deployment_id}" + publish_triggered = False + deadline = time.time() + timeout_minutes * 60 + + while time.time() <= deadline: + status_url = ( + f"{PORTAL_BASE}/api/v1/publisher/status?" + f"id={urllib.parse.quote(deployment_id)}" + ) + poll_status, status_payload = request_json( + "POST", + status_url, + headers, + ) + if poll_status is None or poll_status < 200 or poll_status >= 300: + poll_error = ( + status_payload.get("error") + if isinstance(status_payload, dict) + else "" + ) + if not poll_error: + poll_error = ( + f"HTTP {poll_status}" + if poll_status is not None + else "HTTP unknown" + ) + reason = f"Status polling API failed: {poll_error}" + break + + final_state = extract_deployment_state(status_payload) + + if final_state == "PUBLISHED": + result = "published" + reason = "" + break + + if final_state in {"FAILED", "BROKEN", "ERROR"}: + reason = f"Deployment entered terminal state: {final_state}" + break + + if ( + mode == "portal_api" + and final_state == "VALIDATED" + and not publish_triggered + ): + publish_url = ( + f"{PORTAL_BASE}/api/v1/publisher/deployment/" + f"{urllib.parse.quote(deployment_id)}" + ) + publish_status, publish_payload = request_json( + "POST", + publish_url, + headers, + ) + if publish_status is None or publish_status >= 400: + publish_error = publish_payload.get("error") + if not publish_error: + publish_error = ( + f"HTTP {publish_status}" + if publish_status is not None + else "HTTP unknown" + ) + reason = f"Publish API failed: {publish_error}" + break + publish_triggered = True + + if mode == "user_managed" and final_state == "VALIDATED": + reason = "Mode user_managed requires manual publish in portal" + break + + time.sleep(10) + + if result != "published" and not reason: + reason = ( + "Timed out waiting for deployment status. " + f"Latest state={final_state}" + ) + else: + reason = "No portal deployment id found for repository" + + if result != "published" and not reason: + reason = "Automatic publish did not complete" + + write_output("result", result) + write_output("repository_key", repository_key) + write_output("deployment_id", deployment_id) + write_output("deployment_url", deployment_url) + write_output("final_state", final_state) + write_output("reason", reason) + + display_key = repository_key or "-" + display_deployment = deployment_id or "-" + display_url = deployment_url or "-" + print( + "SONATYPE_RESULT " + f"result={result} " + f"repository_key={display_key} " + f"deployment_id={display_deployment} " + f"final_state={final_state} " + f"deployment_url={display_url}" + ) + if reason: + print(f"SONATYPE_REASON {reason}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fd02a1ac..5e466865 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,8 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. # -# This workflow will build a Java project with Maven -# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven +# This workflow publishes Apollo Java artifacts. name: publish sdks @@ -25,24 +24,81 @@ on: description: 'Maven Repository(snapshots or releases)' required: true default: 'snapshots' + namespace: + description: 'Sonatype namespace used to search staging repositories' + required: true + default: 'com.ctrip.framework.apollo' jobs: publish: runs-on: ubuntu-latest + outputs: + repository_key: ${{ steps.repository_context.outputs.repository_key }} + portal_deployment_id: ${{ steps.repository_context.outputs.portal_deployment_id }} + uploaded_urls_count: ${{ steps.upload_context.outputs.uploaded_urls_count }} + jar_urls_count: ${{ steps.upload_context.outputs.jar_urls_count }} + pom_urls_count: ${{ steps.upload_context.outputs.pom_urls_count }} steps: - - uses: actions/checkout@v2 - - name: Set up Maven Central Repository - uses: actions/setup-java@v1 - with: - java-version: 8 - server-id: ${{ github.event.inputs.repository }} - server-username: MAVEN_USERNAME - server-password: MAVEN_CENTRAL_TOKEN - gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} - gpg-passphrase: MAVEN_GPG_PASSPHRASE - - name: Publish to Apache Maven Central - run: mvn clean deploy -DskipTests=true -Prelease "-Dreleases.repo=https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" "-Dsnapshots.repo=https://central.sonatype.com/repository/maven-snapshots/" - env: - MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} - MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} - MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + - uses: actions/checkout@v4 + + - name: Set up Maven Central Repository + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: 8 + server-id: ${{ github.event.inputs.repository }} + server-username: MAVEN_USERNAME + server-password: MAVEN_CENTRAL_TOKEN + gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} + gpg-passphrase: MAVEN_GPG_PASSPHRASE + + - name: Publish to Apache Maven Central + run: | + set -eo pipefail + mvn clean deploy -DskipTests=true -Prelease \ + "-Dreleases.repo=https://ossrh-staging-api.central.sonatype.com/service/local/staging/deploy/maven2/" \ + "-Dsnapshots.repo=https://central.sonatype.com/repository/maven-snapshots/" \ + 2>&1 | tee maven-deploy.log + env: + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} + + - name: Extract uploaded artifact URLs + id: upload_context + env: + TARGET_REPOSITORY: ${{ github.event.inputs.repository }} + DEPLOY_LOG: maven-deploy.log + DEPLOY_ARTIFACTS_FILE: deploy-artifacts.json + run: | + python3 .github/scripts/release_extract_upload_context.py + + - name: Resolve staging repository key + id: repository_context + env: + TARGET_REPOSITORY: ${{ github.event.inputs.repository }} + TARGET_NAMESPACE: ${{ github.event.inputs.namespace }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + REPOSITORY_CONTEXT_FILE: repository-context.json + run: | + python3 .github/scripts/release_resolve_repository_context.py + + - name: Upload release publish context + if: always() + uses: actions/upload-artifact@v4 + with: + name: release-deploy-context + if-no-files-found: warn + path: | + maven-deploy.log + deploy-artifacts.json + repository-context.json + + - name: Publish summary + if: always() + env: + DEPLOY_ARTIFACTS_FILE: deploy-artifacts.json + REPOSITORY_CONTEXT_FILE: repository-context.json + run: | + python3 .github/scripts/release_write_summary.py diff --git a/.github/workflows/sonatype-publish.yml b/.github/workflows/sonatype-publish.yml new file mode 100644 index 00000000..a96cd600 --- /dev/null +++ b/.github/workflows/sonatype-publish.yml @@ -0,0 +1,85 @@ +# +# Copyright 2026 Apollo Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +name: sonatype publish + +on: + workflow_dispatch: + inputs: + namespace: + description: 'Sonatype namespace (e.g. com.ctrip.framework.apollo)' + required: true + default: 'com.ctrip.framework.apollo' + repository_key: + description: 'Optional staging repository key; leave blank to auto-detect' + required: false + default: '' + timeout_minutes: + description: 'Max minutes to wait for deployment status polling' + required: true + default: '60' + mode: + description: 'manual upload mode: portal_api|automatic|user_managed' + required: true + default: 'portal_api' + type: choice + options: + - portal_api + - automatic + - user_managed + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Publish via Sonatype API + id: publish + env: + INPUT_NAMESPACE: ${{ github.event.inputs.namespace }} + INPUT_REPOSITORY_KEY: ${{ github.event.inputs.repository_key }} + INPUT_TIMEOUT_MINUTES: ${{ github.event.inputs.timeout_minutes }} + INPUT_MODE: ${{ github.event.inputs.mode }} + MAVEN_USERNAME: ${{ secrets.MAVEN_USERNAME }} + MAVEN_CENTRAL_TOKEN: ${{ secrets.MAVEN_CENTRAL_TOKEN }} + run: | + python3 .github/scripts/sonatype_publish.py + + - name: Sonatype publish summary + if: always() + env: + PUBLISH_RESULT: ${{ steps.publish.outputs.result }} + PUBLISH_REPOSITORY_KEY: ${{ steps.publish.outputs.repository_key }} + PUBLISH_DEPLOYMENT_ID: ${{ steps.publish.outputs.deployment_id }} + PUBLISH_FINAL_STATE: ${{ steps.publish.outputs.final_state }} + PUBLISH_DEPLOYMENT_URL: ${{ steps.publish.outputs.deployment_url }} + PUBLISH_REASON: ${{ steps.publish.outputs.reason }} + run: | + { + echo "## Sonatype Publish Result" + echo "" + echo "- result: ${PUBLISH_RESULT}" + echo "- repository_key: ${PUBLISH_REPOSITORY_KEY}" + echo "- deployment_id: ${PUBLISH_DEPLOYMENT_ID}" + echo "- final_state: ${PUBLISH_FINAL_STATE}" + echo "- deployment_url: ${PUBLISH_DEPLOYMENT_URL}" + if [ -n "${PUBLISH_REASON}" ]; then + echo "- reason: ${PUBLISH_REASON}" + echo "" + echo "Manual fallback: open https://central.sonatype.com/publishing/deployments and complete publish by deployment id." + fi + } >> "$GITHUB_STEP_SUMMARY"