Skip to content
Open
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
11 changes: 10 additions & 1 deletion .github/workflows/java-agent.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
name: Java Agent

on:
# Deliberately unfiltered: the Woodpecker pipeline this replaced ran the
# matrix on every pull request regardless of target, and this repo has
# several long-lived integration branches that would otherwise get no CI.
pull_request:
branches: [main]
push:
branches: [main]

# Superseded runs are pointless work: a push to a PR branch makes the
# previous run's result irrelevant. Matters more now that the trigger above
# is unfiltered and this repo carries a lot of long-lived branches.
concurrency:
group: java-agent-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify:
name: JDK ${{ matrix.java-version }}
Expand Down
269 changes: 231 additions & 38 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,96 +1,289 @@
name: Release

# Triggers on a v*.*.* tag push and creates a GitHub Release with the
# Keploy Java agent jar attached as a downloadable asset. Maven Central
# publishing happens in parallel via the Woodpecker pipeline at
# .woodpecker/release.yml — this workflow does not touch Central.
# Triggers on a v*.*.* tag push and performs the complete release: builds
# and signs the agent, smoke tests the exact jar that will ship, publishes
# it to Maven Central, and creates the GitHub Release from those same
# signed artifacts.
#
# Split into two jobs on purpose. Publishing to Central is irreversible
# and non-idempotent — the Portal rejects a re-upload of a version that
# already exists — so everything that can fail cheaply (build, smoke test,
# asset staging) runs *before* it, and everything after it lives in a
# separate job that can be re-run on its own without touching Central.
#
# This matters because the one release incident this repo has actually
# had, on v2.0.6, was exactly that shape: the Woodpecker deploy succeeded
# and the pipeline then failed in the repo1 wait step (a ${VERSION}
# templating bug, fixed in c1b96c1). v2.0.6 is on Central and is fine. Had
# the wait step been able to strand the release, that cosmetic failure
# would have been unrecoverable.
#
# workflow_dispatch is available as a manual recovery path; run it from
# the tag ref, not a branch.

on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:

permissions:
contents: write

# Never let two releases of the same ref overlap. cancel-in-progress is
# deliberately false: cancelling mid-deploy is the one thing that could
# strand a half-published version.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

jobs:
release:
publish:
name: Build, sign and publish to Maven Central
runs-on: ubuntu-latest
# The pom's central-publishing-maven-plugin blocks until Central
# reports the deployment PUBLISHED (waitMaxTime=7200, i.e. 120
# minutes), plus the build and smoke test ahead of it.
timeout-minutes: 150
outputs:
version: ${{ steps.ver.outputs.version }}
env:
# Public key id of the Keploy release signing key. Selects the right
# secret key when more than one is present in the keyring.
GPG_KEY_ID: 8541784E4EC36FB8
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Resolve release version
- name: Resolve and validate release version
id: ver
run: echo "version=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
# Runs before the signing key is imported, and rejects anything
# that is not a plain vMAJOR.MINOR.PATCH[-suffix] tag. The tag name
# is attacker-influenced (git accepts backticks and semicolons in
# ref names, and `v*.*.*` happily matches them), and it is
# interpolated into Maven arguments below.
run: |
set -euo pipefail
version="${GITHUB_REF_NAME#v}"
# Hyphens are allowed inside the pre-release identifier (v2.1.0-alpha-1
# is legal and `v*.*.*` matches it). Leading zeros are not: 02.1.0 and
# 2.1.0 are distinct, permanent coordinates on Central.
if [[ ! "$version" =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z.-]+)?$ ]]; then
echo "Refusing to release from ref '${GITHUB_REF_NAME}'." >&2
echo "Expected a tag of the form vMAJOR.MINOR.PATCH[-prerelease]," >&2
echo "with no leading zeros in the numeric components." >&2
exit 1
fi
# Central only accepts release versions, and a SNAPSHOT tag would
# otherwise burn a full build and smoke test before being rejected.
if [[ "$version" == *[Ss][Nn][Aa][Pp][Ss][Hh][Oo][Tt]* ]]; then
echo "Refusing to release a SNAPSHOT version: ${version}" >&2
exit 1
fi
echo "version=${version}" >> "$GITHUB_OUTPUT"

- name: Set up JDK 17
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '17'
cache: maven
# Writes ~/.m2/settings.xml with a `central` server entry that
# resolves from the env vars named below, and imports the release
# signing key (removed again when the job ends). The key must be
# stored as a plain ASCII-armored block: unlike the Woodpecker
# script this replaces, setup-java has no base64 / escaped-newline
# fallback.
server-id: central
server-username: CENTRAL_USERNAME
server-password: CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE

- name: Set release version in poms
# Pinned: an unqualified `versions:set` resolves the latest plugin
# from Central at run time, which would execute unreviewed code in
# the job that holds the signing key.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: >-
mvn -B -ntp versions:set
-DnewVersion=${{ steps.ver.outputs.version }}
mvn -B -ntp org.codehaus.mojo:versions-maven-plugin:2.21.0:set
-DnewVersion="$VERSION"
-DprocessAllModules=true
-DgenerateBackupPoms=false

- name: Build agent jar with sources and javadoc
# `release` profile attaches the source jar; the inner pom always
# attaches the javadoc jar. -Dgpg.skip=true keeps the signing step
# in the release profile inert (signing happens on Woodpecker
# where the GPG key is wired up).
run: mvn -B -ntp -P release -DskipTests -Dgpg.skip=true clean verify
- name: Build and sign
# The `release` profile attaches sources + javadoc and signs every
# artifact with maven-gpg-plugin.
env:
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: >-
mvn -B -ntp -P release -DskipTests clean verify
-Dgpg.keyname="$GPG_KEY_ID"

- name: Smoke test java agent
- name: Smoke test the release jar
# Runs against the artifact built above, which — because the deploy
# step below reuses this same target/ without cleaning — is the exact
# jar that gets published: on the second lifecycle pass `jar:jar` and
# `source:jar-no-fork` skip as up-to-date, so the agent jar and the
# sources jar are byte-identical to the ones tested here. (The javadoc
# jar is regenerated and differs by its embedded generation timestamp;
# it is content-equivalent and carries its own signature.)
run: ./scripts/smoke-javaagent.sh

- name: Stage release assets
# Deliberately ahead of the Central deploy: this is fiddly,
# failure-prone file shuffling, and it must not be able to fail
# after an irreversible publish.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euxo pipefail
version="${{ steps.ver.outputs.version }}"
set -euo pipefail
mkdir -p release-assets
cp "keploy-sdk/target/keploy-sdk-${version}.jar" "release-assets/keploy-sdk-${version}.jar"
cp "keploy-sdk/target/keploy-sdk-${version}.jar" "release-assets/keploy-sdk.jar"
if [[ -f "keploy-sdk/target/keploy-sdk-${version}-sources.jar" ]]; then
cp "keploy-sdk/target/keploy-sdk-${version}-sources.jar" "release-assets/"
fi
if [[ -f "keploy-sdk/target/keploy-sdk-${version}-javadoc.jar" ]]; then
cp "keploy-sdk/target/keploy-sdk-${version}-javadoc.jar" "release-assets/"
fi
cp "keploy-sdk/pom.xml" "release-assets/keploy-sdk-${version}.pom"
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar" release-assets/
cp "keploy-sdk/target/keploy-sdk-${VERSION}-sources.jar" release-assets/
cp "keploy-sdk/target/keploy-sdk-${VERSION}-javadoc.jar" release-assets/
# Unversioned copy so docs and scripts can link a stable filename,
# with a matching signature so it is verifiable under that name too.
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar" "release-assets/keploy-sdk.jar"
cp "keploy-sdk/target/keploy-sdk-${VERSION}.jar.asc" "release-assets/keploy-sdk.jar.asc"
# The deployed poms are the source poms verbatim
# (createDependencyReducedPom=false), and the module pom declares
# the parent, so both are needed to consume these assets offline.
cp "keploy-sdk/pom.xml" "release-assets/keploy-sdk-${VERSION}.pom"
cp "pom.xml" "release-assets/java-sdk-${VERSION}.pom"
# Signatures. No nullglob: an unmatched glob must fail the job
# rather than quietly publish an unverifiable release.
cp keploy-sdk/target/*.asc release-assets/
cp target/*.asc release-assets/
# Belt and braces — assert every artifact shipped is signed.
missing=0
for f in release-assets/*.jar release-assets/*.pom; do
if [[ ! -f "${f}.asc" ]]; then
echo "Unsigned release asset: ${f}" >&2
missing=1
fi
done
[[ "$missing" -eq 0 ]] || exit 1
ls -la release-assets/

- name: Upload release assets
# Persisted so the github-release job below — and any later re-run
# of it — works from the exact artifacts that were validated here.
uses: actions/upload-artifact@v4
with:
name: release-assets
path: release-assets/
retention-days: 7
if-no-files-found: error
# v4 artifact names are immutable within a run and a partial re-run
# does not clear them, so without this a re-run of a failed publish
# job would 409 here — before it ever reached the deploy.
overwrite: true

- name: Check whether this version is already published
id: central
# Makes the deploy idempotent, which is what makes recovery work at
# all. Central rejects a re-upload of an existing version, so without
# this any whole-run retrigger — a re-pushed tag, "Re-run all jobs",
# or the workflow_dispatch below — would dead-end here and skip the
# github-release job forever, permanently stranding the release it
# was supposed to rescue.
env:
VERSION: ${{ steps.ver.outputs.version }}
run: |
set -euo pipefail
url="https://repo.maven.apache.org/maven2/io/keploy/keploy-sdk/${VERSION}/keploy-sdk-${VERSION}.jar"
if curl -fsI --max-time 30 "$url" >/dev/null; then
echo "already=true" >> "$GITHUB_OUTPUT"
echo "${VERSION} is already on Maven Central — skipping the deploy."
else
echo "already=false" >> "$GITHUB_OUTPUT"
fi

- name: Publish to Maven Central
# Last step in the job, and the point of no return. No `clean`: this
# reuses and republishes the artifacts smoke tested above.
if: steps.central.outputs.already != 'true'
env:
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }}
run: >-
mvn -B -ntp -P release -DskipTests deploy
-Dgpg.keyname="$GPG_KEY_ID"

github-release:
name: Publish GitHub Release
needs: publish
runs-on: ubuntu-latest
# Bounded by the repo1 poll below. Kept in its own job so that a slow
# Central sync fails here, re-runnably, instead of stranding a release.
timeout-minutes: 45
steps:
- name: Download release assets
uses: actions/download-artifact@v4
with:
name: release-assets
path: release-assets

- name: Wait for artifact on Maven Central
env:
VERSION: ${{ needs.publish.outputs.version }}
run: |
set -euo pipefail
artifact_url="https://repo.maven.apache.org/maven2/io/keploy/keploy-sdk/${VERSION}/keploy-sdk-${VERSION}.jar"
echo "Waiting for $artifact_url"
# Bounded by elapsed time, not by an iteration count: with
# `--max-time 30` on the request, 60 iterations of request+sleep
# could run for a full hour and be killed by the job timeout
# before the actionable message below ever printed.
deadline=$(( SECONDS + 1800 ))
attempt=0
while (( SECONDS < deadline )); do
attempt=$(( attempt + 1 ))
if curl -fsI --max-time 30 "$artifact_url" >/dev/null; then
echo "Released artifact is available on Maven Central"
exit 0
fi
echo "Artifact not available yet, retrying in 30s (attempt ${attempt})"
sleep 30
done
echo "Released artifact is not available on Maven Central after 30 minutes"
echo "The Central publish itself succeeded — re-run this job once it syncs."
exit 1

- name: Publish GitHub Release
env:
GITHUB_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
SHA: ${{ github.sha }}
run: |
set -euxo pipefail
set -euo pipefail
# Pre-release flag: anything with a hyphen suffix (e.g. v2.0.6-rc1)
prerelease_flag=()
if [[ "$TAG" == *-* ]]; then
prerelease_flag=(--prerelease)
fi
# Idempotent publish: if a previous run already created the
# release record, --clobber overwrites partial assets so a
# rerun ends in the same state either way.
if gh release view "$TAG" --repo "${{ github.repository }}" >/dev/null 2>&1; then
if gh release view "$TAG" --repo "$REPO" >/dev/null 2>&1; then
# Idempotent re-run: --clobber overwrites partial assets so the
# job ends in the same state either way. A release left as a
# draft by an interrupted run is promoted, otherwise it would
# stay invisible forever.
echo "Release $TAG already exists — uploading assets with --clobber"
gh release upload "$TAG" \
--repo "${{ github.repository }}" \
--clobber \
release-assets/*
gh release upload "$TAG" --repo "$REPO" --clobber release-assets/*
if [[ "$(gh release view "$TAG" --repo "$REPO" --json isDraft -q .isDraft)" == "true" ]]; then
echo "Promoting draft release $TAG"
gh release edit "$TAG" --repo "$REPO" --draft=false
fi
else
gh release create "$TAG" \
--repo "${{ github.repository }}" \
--repo "$REPO" \
--title "Java SDK $TAG" \
--target "${{ github.sha }}" \
--target "$SHA" \
--generate-notes \
"${prerelease_flag[@]}" \
release-assets/*
Expand Down
33 changes: 0 additions & 33 deletions .woodpecker/build.yml

This file was deleted.

Loading
Loading