Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 149 additions & 0 deletions .github/workflows/publish-central.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
name: Publish validated Central candidate

on:
workflow_dispatch:
inputs:
version:
description: Release version, for example 0.2.2
required: true
type: string
deployment_id:
description: Validated deployment ID from the successful release workflow
required: true
type: string
release_run_id:
description: Successful release workflow run that uploaded this candidate
required: true
type: string
confirm_publish:
description: Publish this exact validated candidate permanently to Maven Central
required: true
default: false
type: boolean

permissions:
contents: read
actions: read

concurrency:
group: central-publication-${{ inputs.version }}
cancel-in-progress: false

jobs:
provenance:
name: verify candidate provenance
runs-on: ubuntu-latest
outputs:
sha: ${{ steps.check.outputs.sha }}
steps:
- name: Require signed tag, successful upload and sustained-load validation
id: check
env:
GH_TOKEN: ${{ github.token }}
VERSION: ${{ inputs.version }}
DEPLOYMENT_ID: ${{ inputs.deployment_id }}
RELEASE_RUN_ID: ${{ inputs.release_run_id }}
CONFIRM_PUBLISH: ${{ inputs.confirm_publish }}
shell: bash
run: |
set -euo pipefail
[[ "$CONFIRM_PUBLISH" == true ]]
[[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]
[[ "$DEPLOYMENT_ID" =~ ^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$ ]]
[[ "$RELEASE_RUN_ID" =~ ^[0-9]+$ ]]

gh api "repos/$GITHUB_REPOSITORY/git/ref/tags/v$VERSION" > tag-ref.json
jq -e '.object.type == "tag"' tag-ref.json >/dev/null
tag_sha="$(jq -r '.object.sha' tag-ref.json)"
gh api "repos/$GITHUB_REPOSITORY/git/tags/$tag_sha" > tag.json
jq -e '.verification.verified == true and .object.type == "commit"' tag.json >/dev/null
release_sha="$(jq -r '.object.sha' tag.json)"
gh api "repos/$GITHUB_REPOSITORY/compare/${release_sha}...main" > ancestry.json
jq -e '.status == "ahead" or .status == "identical"' ancestry.json >/dev/null

gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RELEASE_RUN_ID" > upload-run.json
jq -e --arg sha "$release_sha" '
.head_sha == $sha and .conclusion == "success"
and .path == ".github/workflows/release.yml"
' upload-run.json >/dev/null
gh run view "$RELEASE_RUN_ID" --repo "$GITHUB_REPOSITORY" --log > upload-run.log
grep -Fq "$DEPLOYMENT_ID" upload-run.log

gh run list --repo "$GITHUB_REPOSITORY" --workflow core-operations.yml \
--commit "$release_sha" --status success --json databaseId,headSha > soak-runs.json
jq -e --arg sha "$release_sha" 'any(.[]; .headSha == $sha)' soak-runs.json >/dev/null
echo "sha=$release_sha" >> "$GITHUB_OUTPUT"
echo "Verified v$VERSION at $release_sha, upload run $RELEASE_RUN_ID, deployment $DEPLOYMENT_ID." >> "$GITHUB_STEP_SUMMARY"

publish:
name: publish the existing candidate
needs: [provenance]
runs-on: ubuntu-latest
timeout-minutes: 15
environment:
name: central-validation
url: https://central.sonatype.com/publishing/deployments
steps:
- name: Validate coordinates and publish without rebuilding or re-uploading
env:
VERSION: ${{ inputs.version }}
DEPLOYMENT_ID: ${{ inputs.deployment_id }}
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_TOKEN: ${{ secrets.CENTRAL_TOKEN }}
shell: bash
run: |
python3 - <<'PY'
import base64
import json
import os
import time
import urllib.error
import urllib.request

version = os.environ['VERSION']
deployment = os.environ['DEPLOYMENT_ID']
credentials = os.environ['CENTRAL_USERNAME'] + ':' + os.environ['CENTRAL_TOKEN']
authorization = 'Bearer ' + base64.b64encode(credentials.encode()).decode()
origin = 'https://central.sonatype.com/api/v1/publisher'

class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None

client = urllib.request.build_opener(NoRedirect())

def post(path):
request = urllib.request.Request(origin + path, data=b'', method='POST',
headers={'Authorization': authorization})
try:
with client.open(request, timeout=30) as response:
data = response.read()
return json.loads(data) if data else None
except urllib.error.HTTPError as error:
raise SystemExit('Central API returned HTTP ' + str(error.code)) from None

status = post('/status?id=' + deployment)
assert status['deploymentId'] == deployment, 'Unexpected deployment ID'
assert status['deploymentName'] == 'io.github.chomingi:langfuse-otel-java:' + version, 'Unexpected deployment name'
expected = {'pkg:maven/io.github.chomingi/' + artifact + '@' + version for artifact in
('langfuse-otel-java', 'langfuse-otel-core', 'langfuse-otel-spring-boot-starter')}
assert expected.issubset(set(status.get('purls', []))), 'Missing expected Maven coordinates'
state = status['deploymentState']
if state == 'VALIDATED':
post('/deployment/' + deployment)
elif state not in ('PUBLISHING', 'PUBLISHED'):
raise SystemExit('Refusing to publish state ' + state)

deadline = time.monotonic() + 600
while state != 'PUBLISHED':
if time.monotonic() >= deadline:
raise SystemExit('Publication is still pending; inspect this deployment before retrying.')
time.sleep(15)
status = post('/status?id=' + deployment)
state = status['deploymentState']
if state not in ('VALIDATED', 'PUBLISHING', 'PUBLISHED'):
raise SystemExit('Unexpected publication state ' + state)
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as summary:
summary.write('Maven Central deployment `' + deployment + '` is **PUBLISHED**.\n')
summary.write('Verify public consumer resolution before publishing the GitHub draft.\n')
PY
19 changes: 19 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,25 @@ The `central-validation` environment is entered only after every gate succeeds.

`VALIDATED` is not a public release. Do not publish the GitHub draft while Central still reports `VALIDATED` or `PUBLISHING`.

Alternatively, a maintainer can dispatch **Publish validated Central candidate** from `main` with
`version`, `deployment_id`, `release_run_id` and `confirm_publish=true`. This uses the existing
`central-validation` environment approval and Central secrets; it does not require a browser login,
rebuild artifacts or upload another bundle. Before credentials are exposed it verifies the signed
tag, main ancestry, successful release run containing that deployment ID, and a successful sustained-load
run for the exact release commit. It then checks the Central deployment name and Maven coordinates,
publishes only a validated candidate, and waits for `PUBLISHED`. A retry can resume polling an
already-publishing/published candidate. Public consumer resolution and GitHub draft publication remain
separate final steps.

```bash
gh workflow run publish-central.yml --ref main \
-f version=0.2.2 -f deployment_id="$central_deployment_id" \
-f release_run_id="$release_run_id" -F confirm_publish=true
```

The workflow uses the official [Central Publisher API](https://central.sonatype.org/publish/publish-portal-api/#publish-or-drop-the-deployment).


## Verify public resolution

Avoid a warm local Maven cache. Resolve both public artifacts into a new temporary repository, then
Expand Down
Loading