-
Notifications
You must be signed in to change notification settings - Fork 0
208 lines (201 loc) · 9.34 KB
/
Copy pathpublish-java.yml
File metadata and controls
208 lines (201 loc) · 9.34 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
name: publish-java
on:
workflow_dispatch:
inputs:
tag:
description: Existing immutable public release tag (for example v0.2.0)
required: true
type: string
diagnose:
description: Check credential handling without uploading or publishing
type: boolean
default: false
permissions:
contents: read
concurrency:
group: publish-java-${{ inputs.tag }}
cancel-in-progress: false
jobs:
diagnose:
if: ${{ inputs.diagnose }}
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
persist-credentials: false
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
- name: Check secret formatting, Maven resolution and Portal authentication
working-directory: wire/java
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
shell: python
run: |
import base64, os, subprocess, tempfile
import urllib.request, urllib.error
import xml.etree.ElementTree as ET
username = os.environ['MAVEN_CENTRAL_USERNAME']
password = os.environ['MAVEN_CENTRAL_PASSWORD']
for label, value in [('username', username), ('password', password)]:
print(f'{label}: present={bool(value)}, surrounding_whitespace={value != value.strip()}, contains_control={any(ord(c) < 32 for c in value)}')
if not username or not password:
raise SystemExit('A required credential is missing.')
# Never emit effective settings, process output, headers or response bodies.
with tempfile.TemporaryDirectory() as directory:
output = os.path.join(directory, 'settings.xml')
result = subprocess.run([
'mvn', '-B', '-ntp', '-s', 'release-settings.xml',
'org.apache.maven.plugins:maven-help-plugin:3.5.1:effective-settings',
'-DshowPasswords=true', '-Doutput=' + output,
], stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
if result.returncode:
raise SystemExit('Could not calculate effective settings; output withheld.')
ns = {'m': 'http://maven.apache.org/SETTINGS/1.2.0'}
root = ET.parse(output).getroot()
# Maven may serialize effective settings using another schema version.
ns['m'] = root.tag.split('}')[0][1:]
servers = [s for s in root.findall('m:servers/m:server', ns)
if s.findtext('m:id', namespaces=ns) == 'central']
if len(servers) != 1:
raise SystemExit('Expected exactly one central server.')
server = servers[0]
matches = (server.findtext('m:username', namespaces=ns) == username
and server.findtext('m:password', namespaces=ns) == password)
print(f'Maven effective credentials exactly match GitHub secrets: {matches}')
if not matches:
raise SystemExit('Maven altered or failed to resolve the credentials.')
# Read-only status query for a nonexistent deployment, never an upload.
# Compare with an anonymous control; a 404 alone is not publication evidence.
url = 'https://central.sonatype.com/api/v1/publisher/status?id=00000000-0000-0000-0000-000000000000'
def probe(label, pair=None):
headers = {}
if pair is not None:
token = base64.b64encode((':'.join(pair)).encode()).decode()
headers['Authorization'] = 'Bearer ' + token
request = urllib.request.Request(url, data=b'', headers=headers, method='POST')
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, *args, **kwargs):
return None
try:
with urllib.request.build_opener(NoRedirect()).open(request, timeout=30) as response:
status = response.status
except urllib.error.HTTPError as error:
status = error.code
error.close()
except Exception:
raise SystemExit('Portal probe failed; diagnostic details withheld.')
print(f'{label}: HTTP {status}')
probe('Anonymous control')
probe('Configured credentials', (username, password))
if username != username.strip() or password != password.strip():
probe('Surrounding whitespace removed in memory only', (username.strip(), password.strip()))
publish:
if: ${{ !inputs.diagnose }}
runs-on: ubuntu-latest
timeout-minutes: 30
env:
RELEASE_TAG: ${{ inputs.tag }}
steps:
- name: Require an immutable public release
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
[[ "$RELEASE_TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Expected a vX.Y.Z release tag.'; exit 1; }
[[ "$(gh api "repos/$GITHUB_REPOSITORY" --jq .visibility)" == public ]] || { echo 'The repository must be public.'; exit 1; }
gh api "repos/$GITHUB_REPOSITORY/releases/tags/$RELEASE_TAG" > "$RUNNER_TEMP/bitwire-java-release.json"
python3 - <<'PY'
import json, os
from pathlib import Path
release = json.loads((Path(os.environ["RUNNER_TEMP"]) / "bitwire-java-release.json").read_text())
if release.get("draft") is not False or release.get("immutable") is not True:
raise SystemExit("Publishing requires an existing published immutable GitHub release.")
if release.get("tag_name") != os.environ["RELEASE_TAG"]:
raise SystemExit("Release tag does not match the requested tag.")
PY
- uses: actions/checkout@v7
with:
ref: ${{ inputs.tag }}
persist-credentials: false
- name: Match the package to the release
shell: bash
run: |
set -euo pipefail
python3 - <<'PY'
import os
import xml.etree.ElementTree as ET
p = ET.parse("wire/java/pom.xml").getroot()
ns = {"m": "http://maven.apache.org/POM/4.0.0"}
def field(name):
return p.findtext("m:" + name, namespaces=ns)
version = field("version")
if (field("groupId"), field("artifactId")) != ("dev.bitspark", "bitwire"):
raise SystemExit("Unexpected Maven package coordinates.")
if "v" + version != os.environ["RELEASE_TAG"]:
raise SystemExit("The POM version must match the immutable release tag.")
if p.findtext("m:scm/m:tag", namespaces=ns) != os.environ["RELEASE_TAG"]:
raise SystemExit("The POM source tag must match the immutable release tag.")
with open(os.environ["GITHUB_ENV"], "a") as f:
f.write("BITWIRE_VERSION=" + version + "\n")
PY
- uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
- uses: actions/setup-go@v7
with:
go-version-file: go.mod
cache: false
- uses: pnpm/action-setup@v6
- uses: actions/setup-node@v7
with:
node-version: 24
cache: pnpm
- name: Check the shared contract and Java package
shell: bash
run: |
set -euo pipefail
pnpm install --frozen-lockfile
if [[ -f scripts/check.mjs ]]; then node scripts/check.mjs; fi
mvn -B -ntp -f wire/java/pom.xml verify
node wire/java/check-consumer.mjs
- name: Import the release signing key
uses: actions/setup-java@v6
with:
distribution: temurin
java-version: 21
overwrite-settings: false
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
- name: Publish signed artifacts to Maven Central
working-directory: wire/java
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
shell: bash
run: |
set -euo pipefail
: "${MAVEN_CENTRAL_USERNAME:?Configure the Central Portal user-token username.}"
: "${MAVEN_CENTRAL_PASSWORD:?Configure the Central Portal user-token password.}"
: "${MAVEN_GPG_PASSPHRASE:?Configure the signing-key passphrase.}"
mvn -B -ntp -s release-settings.xml -Prelease deploy
- name: Verify the public registry consumer
shell: bash
run: |
set -euo pipefail
pom="https://repo.maven.apache.org/maven2/dev/bitspark/bitwire/$BITWIRE_VERSION/bitwire-$BITWIRE_VERSION.pom"
for attempt in {1..20}; do
if curl --fail --silent --show-error --max-time 20 "$pom" -o /dev/null; then break; fi
if [[ "$attempt" == 20 ]]; then echo 'Published Maven coordinates did not become readable in time.'; exit 1; fi
sleep 15
done
node wire/java/check-consumer.mjs --registry