Skip to content

Commit c498a77

Browse files
authored
[andr] Adding steps for CI releases to publish to maven central (#585)
* [andr] Adding steps for CI releases to publish to maven central * Tests * cleanup * Tests * cleanup * rename * Remove conditional execution * bail if no secrets
1 parent 17b3f63 commit c498a77

File tree

4 files changed

+282
-10
lines changed

4 files changed

+282
-10
lines changed

.github/workflows/release_public.yaml

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Release to dl.bitdrift.io
1+
name: Release to public
22
on:
33
workflow_call:
44
inputs:
@@ -61,7 +61,7 @@ jobs:
6161
name: Upload Android artifacts to dl.bitdrift.io
6262
permissions:
6363
id-token: write # required to use OIDC authentication (set up aws credentials)
64-
contents: read
64+
contents: write
6565
runs-on: ubuntu-latest
6666
steps:
6767
- uses: actions/checkout@v4
@@ -93,6 +93,93 @@ jobs:
9393
env:
9494
VERSION: ${{ inputs.version }}
9595
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
96-
- name: Upload Android Artifacts to aws bucket
96+
- name: Upload Android Artifacts to aws bucket and prepare Maven Central bundles
97+
env:
98+
# GPG signing configuration (existing repo/org secrets)
99+
GPG_PRIVATE_KEY: ${{ secrets.GPG_SIGNING_KEY }}
100+
GPG_PASSPHRASE: ${{ secrets.GPG_SIGNING_KEY_PASSPHRASE }}
97101
run: ./ci/capture_android_release.sh ${{ inputs.version }} "Capture-${{ inputs.version }}.android.zip" "capture-timber-${{ inputs.version }}.android.zip" "capture-apollo-${{ inputs.version }}.android.zip" "capture-plugin-${{ inputs.version }}.android.zip" "capture-plugin-marker-${{ inputs.version }}.android.zip"
102+
- name: Upload bundles to Maven Central
103+
env:
104+
MAVEN_CENTRAL_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }}
105+
MAVEN_CENTRAL_TOKEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }}
106+
run: |
107+
# Avoid echoing commands to logs; keep credentials out of output
108+
set -euo pipefail
109+
set +x
110+
if [[ -z "${MAVEN_CENTRAL_TOKEN_USERNAME:-}" || -z "${MAVEN_CENTRAL_TOKEN_PASSWORD:-}" ]]; then
111+
echo "Maven Central tokens not provided; skipping upload."
112+
exit 0
113+
fi
114+
if [[ ! -d dist/maven-central ]]; then
115+
echo "No Maven Central bundles directory found; skipping."
116+
exit 0
117+
fi
118+
cd dist/maven-central
119+
shopt -s nullglob
120+
bundles=( *.maven-central.zip )
121+
shopt -u nullglob
122+
if (( ${#bundles[@]} == 0 )); then
123+
echo "No bundle zips found to upload."
124+
exit 0
125+
fi
126+
: > deployment_ids.txt
127+
for b in "${bundles[@]}"; do
128+
[[ -f "$b" ]] || continue
129+
echo "Uploading $b to Sonatype Central Portal..."
130+
resp=$(curl -sS -u "${MAVEN_CENTRAL_TOKEN_USERNAME}:${MAVEN_CENTRAL_TOKEN_PASSWORD}" \
131+
-F "bundle=@${b}" \
132+
"https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC" || true)
133+
# Echo server response for visibility
134+
printf '%s\n' "$resp" | tee /dev/stderr
135+
# Try to extract a UUID-like deployment id and save for the polling step
136+
dep_id=$(printf '%s' "$resp" | grep -Eo '[0-9a-fA-F]{8}(-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}' | head -n1 || true)
137+
if [[ -n "$dep_id" ]]; then
138+
echo "$dep_id" >> deployment_ids.txt
139+
fi
140+
done
141+
echo "Saved deployment IDs (if any) to dist/maven-central/deployment_ids.txt"
142+
143+
- name: Poll Maven Central deployment status
144+
env:
145+
MAVEN_CENTRAL_TOKEN_USERNAME: ${{ secrets.MAVEN_CENTRAL_TOKEN_USERNAME }}
146+
MAVEN_CENTRAL_TOKEN_PASSWORD: ${{ secrets.MAVEN_CENTRAL_TOKEN_PASSWORD }}
147+
run: |
148+
set -euo pipefail
149+
set +x
150+
ids_file="dist/maven-central/deployment_ids.txt"
151+
if [[ ! -f "$ids_file" ]]; then
152+
echo "No deployment IDs file found; nothing to poll."
153+
exit 0
154+
fi
155+
mapfile -t ids < "$ids_file" || true
156+
if (( ${#ids[@]} == 0 )); then
157+
echo "No deployment IDs captured; nothing to poll."
158+
exit 0
159+
fi
160+
for id in "${ids[@]}"; do
161+
echo "Polling deployment status for $id..."
162+
deadline=$((SECONDS + 300)) # 5 minutes per artifact
163+
while (( SECONDS < deadline )); do
164+
resp=$(curl -sS -X POST -u "${MAVEN_CENTRAL_TOKEN_USERNAME}:${MAVEN_CENTRAL_TOKEN_PASSWORD}" \
165+
"https://central.sonatype.com/api/v1/publisher/status?id=$id" || true)
166+
state=$(printf '%s' "$resp" | sed -n 's/.*"deploymentState"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p')
167+
if [[ -z "$state" ]]; then
168+
state="UNKNOWN"
169+
fi
170+
echo "Deployment $id state: $state"
171+
if [[ "$state" == "FAILED" ]]; then
172+
echo "Deployment $id failed." >&2
173+
exit 1
174+
fi
175+
if [[ "$state" == "VALIDATED" || "$state" == "PUBLISHING" || "$state" == "PUBLISHED" ]]; then
176+
break
177+
fi
178+
sleep 15
179+
done
180+
if (( SECONDS >= deadline )); then
181+
echo "Deployment $id polling timed out after 5 minutes." >&2
182+
exit 1
183+
fi
184+
done
98185

ci/capture_android_release.sh

Lines changed: 189 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,155 @@ readonly capture_apollo_archive="$4"
1313
readonly capture_plugin_archive="$5"
1414
readonly capture_plugin_marker_archive="$6"
1515

16+
#############################################
17+
# Helpers for Maven Central bundle creation #
18+
#############################################
19+
20+
# Import a GPG private key if provided via env vars.
21+
# Supports either raw ASCII-armored key in GPG_PRIVATE_KEY or base64-encoded in GPG_PRIVATE_KEY_BASE64.
22+
function import_gpg_key_if_available() {
23+
if [[ -z "${GPG_PRIVATE_KEY:-${GPG_PRIVATE_KEY_BASE64:-}}" ]]; then
24+
echo "GPG signing key not provided; cannot proceed with Maven Central bundle creation." >&2
25+
exit 1
26+
fi
27+
28+
# Ensure gpg is available
29+
if ! command -v gpg >/dev/null 2>&1; then
30+
echo "gpg is required to sign artifacts; please install it in the CI runner." >&2
31+
exit 1
32+
fi
33+
34+
# Create a temp GNUPGHOME to avoid polluting the runner account
35+
GNUPGHOME="$(mktemp -d)"
36+
export GNUPGHOME
37+
chmod 700 "$GNUPGHOME"
38+
39+
# Decode if necessary and import (ensure no command echo)
40+
local _xtrace_was_on=0
41+
case $- in
42+
*x*) _xtrace_was_on=1; set +x ;;
43+
esac
44+
45+
# Prepare temp files
46+
local -r key_raw_file="$(mktemp)"
47+
local -r key_sanitized_file="$(mktemp)"
48+
49+
if [[ -n "${GPG_PRIVATE_KEY_BASE64:-}" ]]; then
50+
printf '%s' "$GPG_PRIVATE_KEY_BASE64" | base64 -d >"$key_raw_file"
51+
else
52+
printf '%s' "$GPG_PRIVATE_KEY" >"$key_raw_file"
53+
fi
54+
55+
# Normalize line endings and extract only the private key block(s)
56+
tr -d '\r' <"$key_raw_file" | \
57+
sed -n '/-----BEGIN PGP .*PRIVATE KEY BLOCK-----/,/-----END PGP .*PRIVATE KEY BLOCK-----/p' >"$key_sanitized_file"
58+
59+
# If sanitize produced nothing, fall back to raw file
60+
local import_file="$key_sanitized_file"
61+
if [[ ! -s "$import_file" ]]; then
62+
import_file="$key_raw_file"
63+
fi
64+
65+
# Import, but don't fail the whole script if gpg returns non-zero due to warnings
66+
set +e
67+
gpg --batch --yes --import "$import_file"
68+
local gpg_rc=$?
69+
set -e
70+
# If gpg returned a non-zero code, log and continue; we'll verify presence of a secret key below.
71+
if (( gpg_rc != 0 )); then
72+
echo "gpg import exited with code $gpg_rc; continuing and verifying secret key presence." >&2
73+
fi
74+
75+
# Verify a secret key is available after import
76+
if ! gpg --batch --list-secret-keys >/dev/null 2>&1; then
77+
echo "Failed to import GPG private key for signing." >&2
78+
rm -f "$key_raw_file" "$key_sanitized_file"
79+
exit 1
80+
fi
81+
82+
# Cleanup
83+
rm -f "$key_raw_file" "$key_sanitized_file"
84+
if [[ $_xtrace_was_on -eq 1 ]]; then set -x; fi
85+
86+
# Do not print any key details to logs to avoid leaking identifiers
87+
}
88+
89+
function sign_file() {
90+
local -r file="$1"
91+
# --armor --detach-sign to create .asc
92+
local _xtrace_was_on=0
93+
case $- in *x*) _xtrace_was_on=1; set +x ;; esac
94+
if [[ -n "${GPG_PASSPHRASE:-}" ]]; then
95+
if [[ -n "${GPG_KEY_ID:-}" ]]; then
96+
printf '%s' "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 -u "$GPG_KEY_ID" --armor --detach-sign "$file"
97+
else
98+
printf '%s' "$GPG_PASSPHRASE" | gpg --batch --yes --pinentry-mode loopback --passphrase-fd 0 --armor --detach-sign "$file"
99+
fi
100+
else
101+
if [[ -n "${GPG_KEY_ID:-}" ]]; then
102+
gpg --batch --yes --pinentry-mode loopback -u "$GPG_KEY_ID" --armor --detach-sign "$file"
103+
else
104+
gpg --batch --yes --pinentry-mode loopback --armor --detach-sign "$file"
105+
fi
106+
fi
107+
if [[ $_xtrace_was_on -eq 1 ]]; then set -x; fi
108+
}
109+
110+
function generate_all_checksums() {
111+
local -r file="$1"
112+
"$sdk_repo/ci/checksum.sh" md5 "$file"
113+
"$sdk_repo/ci/checksum.sh" sha1 "$file"
114+
"$sdk_repo/ci/checksum.sh" sha256 "$file"
115+
"$sdk_repo/ci/checksum.sh" sha512 "$file"
116+
}
117+
118+
# Create Maven Central-ready structure under dist/maven-central for a single artifact
119+
# Args:
120+
# $1 - group path (e.g., io/bitdrift)
121+
# $2 - artifact id (e.g., capture)
122+
# $3 - version
123+
# $4.. - files to include (must be located in current CWD)
124+
function package_maven_central_bundle() {
125+
local -r group_path="$1"
126+
local -r artifact_id="$2"
127+
local -r ver="$3"
128+
shift 3
129+
local -a files=("$@")
130+
131+
local -r out_root="$sdk_repo/dist/maven-central/$group_path/$artifact_id/$ver"
132+
mkdir -p "$out_root"
133+
134+
# Copy, sign, and checksum primary files
135+
for f in "${files[@]}"; do
136+
if [[ ! -f "$f" ]]; then
137+
echo "Warning: expected file '$f' not found; skipping." >&2
138+
continue
139+
fi
140+
# Exclude LICENSE/NOTICE from Maven Central bundles (even if accidentally provided)
141+
case "$(basename "$f" | tr '[:upper:]' '[:lower:]')" in
142+
license|license.*|notice|notice.*)
143+
echo "Excluding $(basename "$f") from Maven Central bundle"
144+
continue
145+
;;
146+
esac
147+
cp -f "$f" "$out_root/"
148+
pushd "$out_root" >/dev/null
149+
sign_file "$(basename "$f")"
150+
generate_all_checksums "$(basename "$f")"
151+
popd >/dev/null
152+
done
153+
154+
# Zip the group root to ease upload via Sonatype UI/API
155+
local -r zip_out_dir="$sdk_repo/dist/maven-central"
156+
mkdir -p "$zip_out_dir"
157+
local -r zip_name="${artifact_id}-${ver}.maven-central.zip"
158+
pushd "$zip_out_dir" >/dev/null
159+
# Create a zip that contains the repo path starting from the group root
160+
zip -r "$zip_name" "$group_path/$artifact_id/$ver" >/dev/null
161+
popd >/dev/null
162+
echo "Created Maven Central bundle: $zip_out_dir/$zip_name"
163+
}
164+
16165
function upload_file() {
17166
local -r location="$1"
18167
local -r file="$2"
@@ -23,8 +172,10 @@ function upload_file() {
23172
"$sdk_repo/ci/checksum.sh" sha512 "$file"
24173

25174
for f in "$file" "$file.md5" "$file.sha1" "$file.sha256" "$file.sha512"; do
26-
echo "Uploading $file..."
27-
aws s3 cp "$f" "$location/$f" --region us-east-1
175+
local base
176+
base="$(basename "$f")"
177+
echo "Uploading $base to $location/"
178+
aws s3 cp "$f" "$location/$base" --region us-east-1
28179
done
29180
}
30181

@@ -40,12 +191,12 @@ function generate_maven_file() {
40191
awk '{print $2}' |
41192
sed 's/^\///;s/\/$//')
42193

43-
python3 "$sdk_repo/ci/generate_maven_metadata.py" --releases "${releases//$'\n'/,}" --library "library_name"
194+
python3 "$sdk_repo/ci/generate_maven_metadata.py" --releases "${releases//$'\n'/,}" --library "$library_name"
44195

45196
echo "+++ Generated maven-metadata.xml:"
46197
cat maven-metadata.xml
47198

48-
upload_file "$remote_location_prefix" "maven-metadata.xml"
199+
upload_file "$location" "maven-metadata.xml"
49200
}
50201

51202
function release_capture_sdk() {
@@ -78,6 +229,10 @@ function release_capture_sdk() {
78229
done
79230

80231
generate_maven_file "$remote_location_prefix" "capture"
232+
233+
# Prepare Maven Central bundle (group: io/bitdrift, artifact: capture)
234+
package_maven_central_bundle "io/bitdrift" "capture" "$version" \
235+
"$name.pom" "$name-javadoc.jar" "$name-sources.jar" "$name.aar"
81236
popd
82237
}
83238

@@ -100,6 +255,32 @@ function release_gradle_library() {
100255
aws s3 cp . "$remote_location_prefix/$version/" --recursive --region us-east-1
101256

102257
generate_maven_file "$remote_location_prefix" "$library_name"
258+
259+
# Prepare Maven Central bundle (group: io/bitdrift, artifact: $library_name)
260+
# Try to derive base from .pom name
261+
shopt -s nullglob
262+
local poms=( *.pom )
263+
if (( ${#poms[@]} > 0 )); then
264+
local base="${poms[0]%.pom}"
265+
# Select common files if present
266+
local -a bundle_files=( "$base.pom" )
267+
[[ -f "$base.jar" ]] && bundle_files+=( "$base.jar" )
268+
[[ -f "$base.aar" ]] && bundle_files+=( "$base.aar" )
269+
[[ -f "$base-sources.jar" ]] && bundle_files+=( "$base-sources.jar" )
270+
[[ -f "$base-javadoc.jar" ]] && bundle_files+=( "$base-javadoc.jar" )
271+
# Explicitly exclude LICENSE/NOTICE from bundle files if present nearby
272+
for i in "${!bundle_files[@]}"; do
273+
case "$(basename "${bundle_files[$i]}" | tr '[:upper:]' '[:lower:]')" in
274+
license|license.*|notice|notice.*)
275+
unset 'bundle_files[$i]'
276+
;;
277+
esac
278+
done
279+
package_maven_central_bundle "io/bitdrift" "$library_name" "$version" "${bundle_files[@]}"
280+
else
281+
echo "Warning: No .pom found for $library_name; skipping Maven Central bundle."
282+
fi
283+
shopt -u nullglob
103284
popd
104285
}
105286

@@ -121,9 +302,13 @@ function release_gradle_plugin() {
121302
aws s3 cp . "$remote_location_prefix/$version/" --recursive --region us-east-1
122303

123304
generate_maven_file "$remote_location_prefix" "$plugin_marker"
305+
124306
popd
125307
}
126308

309+
# If requested, set up GPG for signing
310+
import_gpg_key_if_available
311+
127312
release_capture_sdk
128313
release_gradle_library "capture-timber" "$capture_timber_archive"
129314
release_gradle_library "capture-apollo" "$capture_apollo_archive"

gradle/app/build.gradle

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ android {
6060
}
6161

6262
dependencies {
63-
implementation 'io.bitdrift:capture:0.18.0'
64-
implementation 'io.bitdrift:capture-timber:0.18.0'
63+
implementation 'io.bitdrift:capture:0.18.5'
64+
implementation 'io.bitdrift:capture-timber:0.18.5'
6565

6666
implementation 'androidx.appcompat:appcompat:1.7.1'
6767
implementation 'com.google.android.material:material:1.12.0'

gradle/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ buildscript {
66
}
77
dependencies {
88
// TODO: Start publishing plugin to gradlePluginPortal
9-
classpath 'io.bitdrift:capture-plugin:0.18.0'
9+
classpath 'io.bitdrift:capture-plugin:0.18.5'
1010
}
1111
}
1212
plugins {

0 commit comments

Comments
 (0)