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
169 changes: 110 additions & 59 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,29 +1,30 @@
# Drives a release end-to-end from GitHub Actions in a single workflow.
# Drives a release end-to-end from GitHub Actions.
#
# Click "Run workflow", enter a version like v1.2.3, and this will:
# 1. Validate the version (semver, no -SNAPSHOT) and the SHA.
# 2. Run ./gradlew check on the pinned SHA as a final gate.
# 3. Create and push the annotated tag vX.Y.Z pointing at the SHA
# 1. Validate the version and resolve the exact commit to release.
# 2. Create the candidate tag locally and run ./gradlew check against it.
# 3. Ask for approval through the protected `release` environment.
# 4. Create and push the annotated tag vX.Y.Z pointing at the tested commit
# (using GITHUB_TOKEN).
# 5. Build release artifacts at that tag.
# 6. Create the GitHub Release and upload the SDK / agent / OTel
# extension jars.
# 7. Publish to Maven Central via Sonatype, signed with the project
# GPG key.
# 8. Poll Maven Central until the new version is visible.
# 9. Ask javadoc.io to ingest the new Javadocs.
#
# The releaser must supply an explicit commit SHA (not a branch name) so
# that commits which land on main during the environment approval gate
# are NOT silently included in the release.
# 4. Build release artifacts at that tag.
# 5. Create the GitHub Release and upload the SDK / agent / OTel
# extension jars.
# 6. Publish to Maven Central via Sonatype, signed with the project
# GPG key.
# 7. Poll Maven Central until the new version is visible.
# 8. Ask javadoc.io to ingest the new Javadocs.
#
# Re-publishing a failed release: re-run this workflow with the same
# version. If the tag already exists, the tag-creation step is skipped
# and the rest of the pipeline runs against the existing tag.
# version. If the tag already exists, preflight tests that tag and the
# release job skips tag creation.
#
# The entire job runs in the protected `release` GitHub Environment,
# which holds the Sonatype / GPG secrets and requires reviewer approval
# before any tag is pushed or any artifact is published.
# Only the publishing job uses the protected `release` GitHub Environment,
# which holds the Sonatype / GPG secrets and requires reviewer approval.
# Input validation and the full CI gate finish before approval is requested.
name: Release

on:
Expand All @@ -39,30 +40,25 @@ on:
type: string

permissions:
contents: write
contents: read

jobs:
release:
name: Release
preflight:
name: Validate and test
runs-on: ubuntu-24.04
# Gate the entire release behind a protected GitHub Environment.
# Required reviewers, deployment branch/tag rules, and the Sonatype /
# GPG secrets are configured on the environment itself in repo
# settings (Settings → Environments → release).
environment: release
outputs:
release_sha: ${{ steps.release-ref.outputs.release_sha }}
tag_existed: ${{ steps.release-ref.outputs.tag_existed }}
steps:
- name: Validate inputs
env:
VERSION: ${{ inputs.version }}
SHA: ${{ inputs.sha }}
run: |
V="${{ inputs.version }}"
if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: version must be semver (e.g. v1.2.3)" >&2
exit 1
fi
if [[ "$V" == *-SNAPSHOT ]]; then
echo "Error: version cannot end with -SNAPSHOT" >&2
exit 1
fi
SHA="${{ inputs.sha }}"
if [[ ! "$SHA" =~ ^[0-9a-f]{40}$ ]]; then
echo "Error: sha must be a full 40-character lowercase commit SHA. Got: '$SHA'" >&2
echo "Tip: copy the SHA from the commit page on GitHub (use the 'Copy full SHA' button)." >&2
Expand All @@ -72,35 +68,94 @@ jobs:
- name: Checkout
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ inputs.sha }}
fetch-depth: 0

- name: Verify SHA is reachable from main
- name: Resolve and check out release ref
id: release-ref
env:
TAG: ${{ inputs.version }}
SHA: ${{ inputs.sha }}
run: |
SHA="${{ inputs.sha }}"
git fetch origin main --quiet
if ! git merge-base --is-ancestor "$SHA" origin/main; then
echo "Error: commit $SHA is not an ancestor of origin/main." >&2
git fetch origin "+refs/heads/main:refs/remotes/origin/main" --tags --force --quiet

if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
TAG_EXISTED=true
RELEASE_SHA="$(git rev-parse "${TAG}^{commit}")"
echo "Tag '$TAG' already exists; testing the existing tag at $RELEASE_SHA."
else
TAG_EXISTED=false
if ! git cat-file -e "${SHA}^{commit}" 2>/dev/null; then
echo "Error: commit $SHA does not exist in the repository." >&2
exit 1
fi
RELEASE_SHA="$SHA"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git tag -a "$TAG" -m "Release $TAG" "$RELEASE_SHA"
echo "Created candidate tag '$TAG' locally at $RELEASE_SHA."
fi

if ! git merge-base --is-ancestor "$RELEASE_SHA" origin/main; then
echo "Error: release commit $RELEASE_SHA is not an ancestor of origin/main." >&2
echo "Releases must be cut from commits that have landed on main." >&2
exit 1
fi
echo "Commit $SHA is reachable from origin/main."

- name: Determine whether tag already exists
echo "release_sha=$RELEASE_SHA" >> "$GITHUB_OUTPUT"
echo "tag_existed=$TAG_EXISTED" >> "$GITHUB_OUTPUT"
git checkout --detach "$TAG"

- name: Set up JDK 17
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
with:
java-version: '17'
distribution: 'temurin'

- name: Setup Gradle
uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0

- name: Run CI at candidate tag
run: ./gradlew check

release:
name: Release
needs: preflight
runs-on: ubuntu-24.04
permissions:
contents: write
# Only publishing is gated by the protected environment. Preflight input
# validation and tests must pass before GitHub requests reviewer approval.
environment: release
steps:
- name: Checkout tested commit
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
with:
ref: ${{ needs.preflight.outputs.release_sha }}
fetch-depth: 0

- name: Verify tag state
id: tag-state
env:
TAG: ${{ inputs.version }}
TESTED_SHA: ${{ needs.preflight.outputs.release_sha }}
TAG_EXISTED_AT_PREFLIGHT: ${{ needs.preflight.outputs.tag_existed }}
run: |
TAG="${{ inputs.version }}"
git fetch --tags --quiet
git fetch origin --tags --force --quiet
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
CURRENT_SHA="$(git rev-parse "${TAG}^{commit}")"
if [[ "$CURRENT_SHA" != "$TESTED_SHA" ]]; then
echo "Error: tag '$TAG' moved after preflight ($TESTED_SHA -> $CURRENT_SHA)." >&2
exit 1
fi
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag '$TAG' already exists; will publish from the existing tag."
elif git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then
echo "exists=true" >> "$GITHUB_OUTPUT"
echo "Tag '$TAG' exists on origin but not locally; fetching."
git fetch origin "refs/tags/$TAG:refs/tags/$TAG"
echo "Tag '$TAG' already exists at the tested commit."
else
if [[ "$TAG_EXISTED_AT_PREFLIGHT" == "true" ]]; then
echo "Error: tag '$TAG' was deleted after preflight." >&2
exit 1
fi
echo "exists=false" >> "$GITHUB_OUTPUT"
echo "Tag '$TAG' does not exist yet; will create at $SHA."
echo "Tag '$TAG' does not exist yet; will create it at $TESTED_SHA."
fi

- name: Set up JDK 17
Expand All @@ -112,29 +167,25 @@ jobs:
- name: Setup Gradle
uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0

- name: Run CI (pre-tag, on chosen ref)
if: steps.tag-state.outputs.exists == 'false'
run: ./gradlew check

- name: Configure git identity
if: steps.tag-state.outputs.exists == 'false'
run: |
git config user.name "github-actions[bot]"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"

- name: Create and push tag
if: steps.tag-state.outputs.exists == 'false'
env:
TAG: ${{ inputs.version }}
TESTED_SHA: ${{ needs.preflight.outputs.release_sha }}
run: |
TAG="${{ inputs.version }}"
SHA="${{ inputs.sha }}"
git tag -a "$TAG" -m "Release $TAG" "$SHA"
git tag -a "$TAG" -m "Release $TAG" "$TESTED_SHA"
git push origin "$TAG"

- name: Checkout tag
run: git checkout "${{ inputs.version }}"

- name: Run CI (at tag)
run: ./gradlew check
env:
TAG: ${{ inputs.version }}
run: git checkout "$TAG"

- name: Build release artifacts
run: ./gradlew build publishToMavenLocal
Expand Down
14 changes: 8 additions & 6 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,12 +180,14 @@ To cut a release:
3. Enter:
- `version`: the release version as `vX.Y.Z` (semver, no `-SNAPSHOT`).
- `sha`: the **full 40-character commit SHA** on `main` you want to release. Copy it from the commit page on GitHub using "Copy full SHA". A branch name is intentionally not accepted — pinning to a SHA prevents commits that land on `main` during the approval gate from sneaking into the release.
4. The job runs in the protected `release` GitHub Environment and will pause for **required-reviewer approval** before doing anything. Approve from the workflow run page (or the repo's Deployments tab).
5. Once approved, the `Release` workflow will, in one job:
- Validate the version and the SHA, and verify the SHA is reachable from `origin/main`.
- Check out the pinned SHA and run `./gradlew check`.
- Create and push the annotated tag `vX.Y.Z` pointing at the SHA (using the default `GITHUB_TOKEN` — no separate bot identity is needed since the publish steps are in the same workflow).
- Check out the tag, re-run `./gradlew check`, and build release artifacts.
4. The workflow first runs an ungated preflight job:
- Validate the version and SHA.
- Resolve the commit to release and verify it is reachable from `origin/main`.
- Create the candidate tag locally (or check out the existing tag when re-publishing) and run `./gradlew check` against that exact tag.
5. After preflight passes, the publishing job enters the protected `release` GitHub Environment and pauses for **required-reviewer approval**. Approve from the workflow run page (or the repo's Deployments tab).
6. Once approved, the publishing job will:
- Verify that the tag did not move while approval was pending, then create and push it if needed.
- Build release artifacts at the tested tag.
- Create the GitHub Release with the SDK, agent, and OTel extension jars attached.
- Publish to Maven Central via Sonatype, signed with the project GPG key.
- Poll Maven Central until the new version is visible (this can take many hours).
Expand Down
Loading