Skip to content

Commit 3c20387

Browse files
committed
feat: drive releases from a single GitHub Actions workflow
Replaces the previous two-workflow setup (push a tag, separate publish workflow listens for it) with a single end-to-end `Release` workflow at `.github/workflows/release.yml`. The old `publish-release-from-tag.yml` is removed. Flow: 1. Actions → Release → Run workflow with `version=vX.Y.Z` (optional `ref`, defaults to `main`). 2. The job runs in the protected `release` GitHub Environment, which holds the Sonatype / GPG secrets and requires reviewer approval before anything happens. 3. After approval, the workflow validates semver, runs `./gradlew check` on the chosen ref, creates and pushes the annotated tag `vX.Y.Z`, checks out the tag, re-runs `./gradlew check`, builds artifacts, creates the GitHub Release with the SDK / agent / OTel extension jars attached, publishes to Maven Central via Sonatype, and polls Maven Central until the version is visible. Because publishing happens in the same workflow as the tag push, the tag push no longer needs to retrigger anything. The default `GITHUB_TOKEN` is sufficient — no GitHub App / PAT is required. Re-publish path: re-run `Release` with the same version. The workflow detects an existing tag, skips the tag-creation step, and resumes from build/publish. GitHub Release asset uploads use `--clobber` so partial uploads from a prior failed run are replaced. The environment name is intentionally generic (`release`) rather than maven-central-specific because the gated job covers the full release flow — tag creation, GitHub Release, Sonatype publish, and Maven Central sync wait — not just the Sonatype step. `CONTRIBUTING.md` gains a Releasing section documenting the end-to-end flow, the approval gate, the re-publish behavior, the scoped environment secrets, and a note that there is no version constant to bump in source because the version is derived from git tags at build time by `generateVersion()` in `build.gradle`. `scripts/release.sh` is retained as a local fallback.
1 parent e60ade0 commit 3c20387

2 files changed

Lines changed: 134 additions & 76 deletions

File tree

Lines changed: 103 additions & 76 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,85 @@
1-
# This workflow is triggered when a new tag is pushed to main.
2-
# It can also be run manually to re-publish a release in case it failed for some reason.
3-
name: Publish Release From Tag
1+
# Drives a release end-to-end from GitHub Actions in a single workflow.
2+
#
3+
# Click "Run workflow", enter a version like v1.2.3, and this will:
4+
# 1. Validate the version (semver, no -SNAPSHOT).
5+
# 2. Run ./gradlew check on the chosen ref as a final gate.
6+
# 3. Create and push the annotated tag vX.Y.Z (using GITHUB_TOKEN).
7+
# 4. Build release artifacts at that tag.
8+
# 5. Create the GitHub Release and upload the SDK / agent / OTel
9+
# extension jars.
10+
# 6. Publish to Maven Central via Sonatype, signed with the project
11+
# GPG key.
12+
# 7. Poll Maven Central until the new version is visible.
13+
#
14+
# Re-publishing a failed release: re-run this workflow with the same
15+
# version. If the tag already exists, the tag-creation step is skipped
16+
# and the rest of the pipeline runs against the existing tag.
17+
#
18+
# The entire job runs in the protected `release` GitHub Environment,
19+
# which holds the Sonatype / GPG secrets and requires reviewer approval
20+
# before any tag is pushed or any artifact is published.
21+
name: Release
422

523
on:
6-
push:
7-
tags:
8-
- 'v*'
924
workflow_dispatch:
1025
inputs:
11-
tag:
12-
description: 'Tag to publish (e.g., v1.0.0)'
26+
version:
27+
description: 'Version to release (e.g., v1.2.3)'
1328
required: true
1429
type: string
30+
ref:
31+
description: 'Branch or commit to tag (default: main). Ignored if the tag already exists.'
32+
required: false
33+
type: string
34+
default: main
1535

1636
permissions:
1737
contents: write
1838

1939
jobs:
20-
validate-and-publish:
21-
name: Validate Tag and Publish Release
22-
# we want to run ubuntu-latest but we'll pin to a specific version so workflow is reproducable
40+
release:
41+
name: Release
2342
runs-on: ubuntu-24.04
43+
# Gate the entire release behind a protected GitHub Environment.
44+
# Required reviewers, deployment branch/tag rules, and the Sonatype /
45+
# GPG secrets are configured on the environment itself in repo
46+
# settings (Settings → Environments → release).
47+
environment: release
2448
steps:
25-
- name: Checkout code
26-
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
27-
with:
28-
fetch-depth: 0
29-
30-
- name: Determine tag
31-
id: determine-tag
49+
- name: Validate version format
3250
run: |
33-
if [[ "${{ github.event_name }}" == "push" ]]; then
34-
TAG_NAME="${{ github.ref_name }}"
35-
else
36-
TAG_NAME="${{ inputs.tag }}"
37-
fi
38-
echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT
39-
echo "Using tag: $TAG_NAME"
40-
41-
- name: Validate tag format
42-
run: |
43-
TAG="${{ steps.determine-tag.outputs.tag }}"
44-
45-
# Check if tag starts with 'v'
46-
if [[ ! "$TAG" =~ ^v ]]; then
47-
echo "Error: Tag '$TAG' must start with 'v'"
51+
V="${{ inputs.version }}"
52+
if [[ ! "$V" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
53+
echo "Error: version must be semver (e.g. v1.2.3)" >&2
4854
exit 1
4955
fi
50-
51-
# Extract version without 'v' prefix
52-
VERSION="${TAG#v}"
53-
54-
# Check if version is valid semver (x.y.z)
55-
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
56-
echo "Error: Tag '$TAG' is not valid semver format (vx.y.z)"
56+
if [[ "$V" == *-SNAPSHOT ]]; then
57+
echo "Error: version cannot end with -SNAPSHOT" >&2
5758
exit 1
5859
fi
5960
60-
# Check that version does not end with -SNAPSHOT
61-
if [[ "$VERSION" =~ -SNAPSHOT$ ]]; then
62-
echo "Error: Tag '$TAG' cannot end with '-SNAPSHOT'"
63-
exit 1
64-
fi
65-
66-
echo "Tag '$TAG' is valid"
61+
- name: Checkout
62+
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
63+
with:
64+
ref: ${{ inputs.ref }}
65+
fetch-depth: 0
6766

68-
- name: Verify tag exists
67+
- name: Determine whether tag already exists
68+
id: tag-state
6969
run: |
70-
TAG="${{ steps.determine-tag.outputs.tag }}"
71-
if ! git tag -l | grep -q "^$TAG$"; then
72-
echo "Error: Tag '$TAG' does not exist"
73-
exit 1
70+
TAG="${{ inputs.version }}"
71+
git fetch --tags --quiet
72+
if git rev-parse -q --verify "refs/tags/$TAG" >/dev/null; then
73+
echo "exists=true" >> "$GITHUB_OUTPUT"
74+
echo "Tag '$TAG' already exists; will publish from the existing tag."
75+
elif git ls-remote --tags origin | grep -q "refs/tags/${TAG}$"; then
76+
echo "exists=true" >> "$GITHUB_OUTPUT"
77+
echo "Tag '$TAG' exists on origin but not locally; fetching."
78+
git fetch origin "refs/tags/$TAG:refs/tags/$TAG"
79+
else
80+
echo "exists=false" >> "$GITHUB_OUTPUT"
81+
echo "Tag '$TAG' does not exist yet; will create from ref '${{ inputs.ref }}'."
7482
fi
75-
echo "Tag '$TAG' exists"
76-
77-
- name: Checkout tag
78-
run: |
79-
git checkout ${{ steps.determine-tag.outputs.tag }}
8083
8184
- name: Set up JDK 17
8285
uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4.8.0
@@ -87,7 +90,27 @@ jobs:
8790
- name: Setup Gradle
8891
uses: gradle/gradle-build-action@a8f75513eafdebd8141bd1cd4e30fcd194af8dfa # v2.12.0
8992

90-
- name: Run CI
93+
- name: Run CI (pre-tag, on chosen ref)
94+
if: steps.tag-state.outputs.exists == 'false'
95+
run: ./gradlew check
96+
97+
- name: Configure git identity
98+
if: steps.tag-state.outputs.exists == 'false'
99+
run: |
100+
git config user.name "github-actions[bot]"
101+
git config user.email "github-actions[bot]@users.noreply.github.com"
102+
103+
- name: Create and push tag
104+
if: steps.tag-state.outputs.exists == 'false'
105+
run: |
106+
TAG="${{ inputs.version }}"
107+
git tag -a "$TAG" -m "Release $TAG"
108+
git push origin "$TAG"
109+
110+
- name: Checkout tag
111+
run: git checkout "${{ inputs.version }}"
112+
113+
- name: Run CI (at tag)
91114
run: ./gradlew check
92115

93116
- name: Build release artifacts
@@ -96,7 +119,7 @@ jobs:
96119
- name: Find built artifacts
97120
id: find-artifacts
98121
run: |
99-
TAG="${{ steps.determine-tag.outputs.tag }}"
122+
TAG="${{ inputs.version }}"
100123
# Strip 'v' prefix to get the actual version used by Gradle
101124
VERSION="${TAG#v}"
102125
@@ -124,29 +147,39 @@ jobs:
124147
echo " Agent JAR: $AGENT_JAR"
125148
echo " OTel Extension JAR: $OTL_EXT_JAR"
126149
127-
- name: Create GitHub Release
150+
- name: Create or update GitHub Release
151+
env:
152+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
128153
run: |
129-
TAG="${{ steps.determine-tag.outputs.tag }}"
154+
TAG="${{ inputs.version }}"
130155
131-
# Create the release
132-
gh release create "$TAG" \
133-
--generate-notes \
134-
--title "Release $TAG"
156+
# Create the release if it doesn't already exist (re-publish path).
157+
if ! gh release view "$TAG" >/dev/null 2>&1; then
158+
gh release create "$TAG" \
159+
--generate-notes \
160+
--title "Release $TAG"
161+
else
162+
echo "Release '$TAG' already exists; will upload (clobber) assets."
163+
fi
135164
136-
# Upload SDK artifacts
165+
# Upload artifacts, clobbering any partial uploads from a prior run.
137166
for jar in \
138167
"${{ steps.find-artifacts.outputs.sdk-main-jar }}" \
139168
"${{ steps.find-artifacts.outputs.sdk-sources-jar }}" \
140169
"${{ steps.find-artifacts.outputs.sdk-javadoc-jar }}" \
141170
"${{ steps.find-artifacts.outputs.agent-jar }}" \
142171
"${{ steps.find-artifacts.outputs.otel-ext-jar }}"; do
143172
if [[ -n "$jar" && -f "$jar" ]]; then
144-
gh release upload "$TAG" "$jar"
173+
gh release upload "$TAG" "$jar" --clobber
145174
fi
146175
done
147-
env:
148-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
176+
149177
- name: Publish to Sonatype
178+
env:
179+
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
180+
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
181+
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
182+
GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }}
150183
run: |-
151184
if [ -z "$SONATYPE_USERNAME" ]; then
152185
echo "Error: SONATYPE_USERNAME is not set"
@@ -169,16 +202,10 @@ jobs:
169202
printenv -- GPG_SIGNING_KEY | gpg --batch --passphrase-fd 3 --import 3<<< "$GPG_SIGNING_PASSWORD"
170203
GPG_SIGNING_KEY_ID="$(gpg --with-colons --list-keys | awk -F : -- '/^pub:/ { getline; print "0x" substr($10, length($10) - 7) }')"
171204
./gradlew publishAndReleaseToMavenCentral --stacktrace -PmavenCentralUsername="$SONATYPE_USERNAME" -PmavenCentralPassword="$SONATYPE_PASSWORD" --no-configuration-cache
172-
env:
173-
SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }}
174-
SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }}
175-
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
176-
GPG_SIGNING_PASSWORD: ${{ secrets.GPG_SIGNING_PASSWORD }}
177205
178206
- name: Wait for Maven Central sync
179207
run: |
180-
TAG="${{ steps.determine-tag.outputs.tag }}"
181-
# Strip 'v' prefix to get the Maven version
208+
TAG="${{ inputs.version }}"
182209
VERSION="${TAG#v}"
183210
echo "Waiting for version $VERSION to sync to Maven Central. THIS CAN TAKE MANY HOURS! Godspeed"
184211
./scripts/wait-for-maven.sh "$VERSION"

CONTRIBUTING.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,37 @@ Because the SDK is new and under active development, third-party contribution be
1717
- These hooks automatically run common checks for you but CI also runs the same checks before merging to the main branch is allowed
1818
- NOTE: this will overwrite existing hooks. Take backups before running
1919

20+
## Releasing
21+
22+
Releases are driven end-to-end from a single GitHub Actions workflow. You do not need to tag locally or push tags from your machine.
23+
24+
To cut a release:
25+
26+
1. Make sure everything you want included is merged to `main` and CI is green.
27+
2. Go to **Actions → Release → Run workflow**.
28+
3. Enter the version as `vX.Y.Z` (semver, no `-SNAPSHOT`). Leave `ref` as `main` unless you have a specific reason to tag a different commit.
29+
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).
30+
5. Once approved, the `Release` workflow will, in one job:
31+
- Validate the version.
32+
- Check out the chosen ref and run `./gradlew check`.
33+
- Create and push the annotated tag `vX.Y.Z` (using the default `GITHUB_TOKEN` — no separate bot identity is needed since the publish steps are in the same workflow).
34+
- Check out the tag, re-run `./gradlew check`, and build release artifacts.
35+
- Create the GitHub Release with the SDK, agent, and OTel extension jars attached.
36+
- Publish to Maven Central via Sonatype, signed with the project GPG key.
37+
- Poll Maven Central until the new version is visible (this can take many hours).
38+
39+
The Sonatype and GPG signing secrets (`SONATYPE_USERNAME`, `SONATYPE_PASSWORD`, `GPG_SIGNING_KEY`, `GPG_SIGNING_PASSWORD`) are scoped to the `release` environment.
40+
41+
The SDK version is computed from git tags at build time (see `generateVersion()` in `build.gradle`) and embedded into `braintrust.properties`, so there are no version constants to bump in source.
42+
43+
### Re-publishing a failed release
44+
45+
If the workflow fails partway through, re-run **Release** with the same version. The workflow detects that the tag already exists, skips tag creation, and resumes from the build/publish steps against the existing tag. GitHub Release asset uploads use `--clobber` so partial uploads from a prior run are replaced.
46+
47+
### Local fallback
48+
49+
`scripts/release.sh` can still create and push a tag from a clean local checkout if the Actions-driven flow is unavailable. Prefer the workflow.
50+
2051
## Misc Tips
2152

2253
### Running a local OpenTelemetry collector

0 commit comments

Comments
 (0)