From 6a5c2a994a73f8c9b3d1de3eb843871ef4b5eae4 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 17:31:31 +0800 Subject: [PATCH 1/6] Identify the release by its tag, not the checked-out branch Every step after `prepare` derived the version from the working branch: version=$(git describe --tags --abbrev=0 | sed 's/^v//') Release tags are created on release/x.y.z and never become ancestors of main, so `git describe` cannot see them from main. By the time `vote-passed` runs - days later, after the release PR is merged and the branch auto-deleted - describe walks straight past every release tag to an unrelated ancient one, v3.2.6 in this repository. That is not a loud failure: `promote` would svn mv a 3.2.6 path, and `docker` would push apache/skywalking-java-agent:3.2.6-{alpine,java8,...} to Docker Hub under real version tags. The tag is the only thing that still pins a release once the branch is gone, and it is already what the rest of the flow acts on: release:perform builds from the tag, the source tar is cut from the tag, and the vote email quotes the tag's commit IDs. Resolve the version from the tag list too - branch-independent, and it outlives the release branch. resolve_version() takes an explicit argument, else $RELEASE_VERSION, else the highest vX.Y.Z tag. It validates the x.y.z shape and refuses to run when the tag does not exist, so a typo aborts instead of addressing a path that was never released. Wired into stage, upload, email, promote and docker, each of which now accepts an optional version argument; prepare-vote threads its version through explicitly rather than re-deriving it. vote-passed additionally prints what it is about to publish - tag, SVN move, Docker Hub tags, old version being removed - and asks for confirmation, since both destinations are public and awkward to walk back. Co-Authored-By: Claude Opus 5 (1M context) --- docs/en/contribution/release-java-agent.md | 7 ++ tools/releasing/release.sh | 117 +++++++++++++++++---- 2 files changed, 102 insertions(+), 22 deletions(-) diff --git a/docs/en/contribution/release-java-agent.md b/docs/en/contribution/release-java-agent.md index 5b941a822c..f7a7ccb107 100644 --- a/docs/en/contribution/release-java-agent.md +++ b/docs/en/contribution/release-java-agent.md @@ -92,6 +92,13 @@ are found in `https://dist.apache.org/repos/dist/dev/skywalking/java-agent/x.y.z 1. Check the Apache License Header. Run `docker run --rm -v $(pwd):/github/workspace apache/skywalking-eyes header check`. (No binaries in source codes) ## vote-passed +Every step after `prepare` identifies the release by its **tag** (`vx.y.z`), never by the +checked-out branch. By the time you run `vote-passed`, the release PR has normally been +merged and `release/x.y.z` deleted, and `main` has already moved on to the next +`-SNAPSHOT`; the tag is the only thing that still pins the release. The version defaults to +the highest `vx.y.z` tag in the repository, and can be overridden with a positional +argument (`./release.sh docker 9.7.0`) or `RELEASE_VERSION=9.7.0`. + After the vote passes, run `vote-passed` which executes: 1. **promote** — move packages from `dist/dev` to `dist/release` in Apache SVN (prompts for SVN credentials), then release the Nexus staging repository at https://repository.apache.org and update the website download page 2. **docker** — build and push all Docker image variants (alpine, java8, java11, java17, java21, java25) diff --git a/tools/releasing/release.sh b/tools/releasing/release.sh index 22c200dc46..39e2e96954 100755 --- a/tools/releasing/release.sh +++ b/tools/releasing/release.sh @@ -46,6 +46,51 @@ info() { echo -e "${GREEN}[INFO]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*"; exit 1; } +# ============================================================ +# resolve_version — identify the release from its tag +# ============================================================ +# Every step after `prepare` acts on the release tag, never on whatever branch +# happens to be checked out. `release:perform` builds from the tag, the source +# tar is cut from the tag, and the vote email quotes the tag's commit IDs. +# +# The version therefore has to be derived from the tag as well. `git describe` +# cannot do this: release tags are created on release/x.y.z branches and never +# become ancestors of main, so once the release PR is merged and the branch is +# deleted, describe walks past them to an unrelated ancient tag (v3.2.6 here). +# That would aim SVN moves and Docker pushes at the wrong version. Tags are +# branch-independent and outlive the release branch, so select from the tag list. +# +# Order of precedence: explicit argument, then $RELEASE_VERSION, then the highest +# vX.Y.Z tag in the repository. +resolve_version() { + local explicit="${1:-}" + [ -z "$explicit" ] && explicit="${RELEASE_VERSION:-}" + + local version + if [ -n "$explicit" ]; then + version="${explicit#v}" + else + local latest + latest=$(git tag -l 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -1) + if [ -z "$latest" ]; then + error "No vX.Y.Z release tag found. Pass the version explicitly, e.g. '$0 9.7.0'." + fi + version="${latest#v}" + fi + + case "$version" in + [0-9]*.[0-9]*.[0-9]*) ;; + *) error "Version must look like x.y.z, got: ${version}" ;; + esac + + # Refuse to act on a release that was never tagged. + if ! git rev-parse -q --verify "refs/tags/v${version}" >/dev/null 2>&1; then + error "Tag v${version} does not exist locally. Run '$0 prepare ${version}' first, or fetch it with 'git fetch origin --tags'." + fi + + echo "$version" +} + # ============================================================ # preflight — check tools and environment # ============================================================ @@ -249,12 +294,11 @@ EOF cmd_stage() { cd "$PROJECT_ROOT" - # Detect version from latest tag local version - version=$(git describe --tags --abbrev=0 | sed 's/^v//') + version=$(resolve_version "${1:-}") local tag_name="v${version}" - info "Staging release ${version}..." + info "Staging release ${version} (from tag ${tag_name})..." # Maven release:perform info "Running maven release:perform..." @@ -320,7 +364,7 @@ cmd_upload() { cd "$PROJECT_ROOT" local version - version=$(git describe --tags --abbrev=0 | sed 's/^v//') + version=$(resolve_version "${1:-}") local svn_dev="https://dist.apache.org/repos/dist/dev/skywalking/java-agent" info "Uploading release ${version} to Apache SVN (dist/dev)..." @@ -366,13 +410,13 @@ cmd_upload() { cmd_email() { local type="${1:-}" if [[ ! "$type" =~ ^(vote|announce)$ ]]; then - error "Usage: $0 email [vote|announce]" + error "Usage: $0 email [version]" fi cd "$PROJECT_ROOT" local version - version=$(git describe --tags --abbrev=0 | sed 's/^v//') + version=$(resolve_version "${2:-}") local tag="v${version}" local commit_id commit_id=$(git rev-list -n1 "$tag" 2>/dev/null || echo "") @@ -495,9 +539,9 @@ cmd_docker() { cd "$PROJECT_ROOT" local version - version=$(git describe --tags --abbrev=0 | sed 's/^v//') + version=$(resolve_version "${1:-}") - info "Building and pushing Docker images for ${version}..." + info "Building and pushing Docker images for ${version} (from tag v${version})..." local dist_tar="${SCRIPT_DIR}/${PRODUCT_NAME}-${version}/${PRODUCT_NAME}-${version}.tgz" @@ -524,7 +568,7 @@ cmd_promote() { cd "$PROJECT_ROOT" local version - version=$(git describe --tags --abbrev=0 | sed 's/^v//') + version=$(resolve_version "${1:-}") info "Promoting release ${version} from dist/dev to dist/release..." @@ -578,11 +622,11 @@ cmd_prepare_vote() { echo "" cmd_prepare "$version" "$next_version" echo "" - cmd_stage + cmd_stage "$version" echo "" - cmd_upload + cmd_upload "$version" echo "" - cmd_email vote + cmd_email vote "$version" } # ============================================================ @@ -591,11 +635,36 @@ cmd_prepare_vote() { cmd_vote_passed() { local old_version="${1:-}" - cmd_promote + cd "$PROJECT_ROOT" + + # Resolved from the release tag, so this works after the release branch has + # been merged and deleted. Show it before touching SVN or Docker Hub, both of + # which are public and awkward to undo. + local version + version=$(resolve_version "") + + info "Publishing release ${version}:" + echo " Release tag : v${version}" + echo " SVN promote : dist/dev/skywalking/java-agent/${version} -> dist/release/..." + echo " Docker Hub tags : apache/skywalking-java-agent:${version}-{alpine,java8,java11,java17,java21,java25}" + if [ -n "$old_version" ]; then + echo " Remove from SVN : dist/release/skywalking/java-agent/${old_version}" + else + echo " Remove from SVN : (nothing - no old version given)" + fi + echo "" + read -rp "Continue? [y/N] " confirm + if [[ ! "$confirm" =~ ^[Yy]$ ]]; then + info "Aborted." + exit 0 + fi + echo "" + + cmd_promote "$version" echo "" - cmd_docker + cmd_docker "$version" echo "" - cmd_email announce + cmd_email announce "$version" if [ -n "$old_version" ]; then echo "" @@ -634,17 +703,21 @@ main() { echo " (wait for 72h vote to pass)" echo " $0 vote-passed [old_version] # after vote" echo "" + echo "Every command after 'prepare' identifies the release by its tag (vX.Y.Z), not by the" + echo "checked-out branch, so they still work once release/x.y.z has been merged and deleted." + echo "The version defaults to the highest vX.Y.Z tag; override with an argument or RELEASE_VERSION." + echo "" echo "Individual commands:" echo " preflight Check tools and environment" echo " prepare [next_ver] Prepare release (branch, tag, PR)" - echo " stage Stage release (maven release:perform, build tars)" - echo " upload Upload to Apache SVN dist/dev" + echo " stage [ver] Stage release (maven release:perform, build tars)" + echo " upload [ver] Upload to Apache SVN dist/dev" echo " prepare-vote [next_ver] Run preflight + prepare + stage + upload + vote email" - echo " email [vote|announce] Generate email content" - echo " promote Move from dist/dev to dist/release in SVN" - echo " docker Build and push Docker images" - echo " vote-passed [old_ver] Run promote + docker + announce email [+ cleanup]" - echo " cleanup Remove old release from dist/release" + echo " email [ver] Generate email content" + echo " promote [ver] Move from dist/dev to dist/release in SVN" + echo " docker [ver] Build and push Docker images to Docker Hub" + echo " vote-passed [old_ver] Run promote + docker + announce email [+ cleanup]" + echo " cleanup Remove old release from dist/release" ;; esac } From 3b545b7cd1732edf7f9f92902be1ed96be39a3d2 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 15:32:55 +0800 Subject: [PATCH 2/6] Prompt for the next milestone ID when resetting CHANGES.md When cmd_prepare resets CHANGES.md for the next development version it wrote a literal placeholder: All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/xxx?closed=1) Nothing in the release flow ever filled that in, so the placeholder rode the release PR into main unless someone noticed and edited it by hand, leaving the next version's change log pointing at a dead milestone link. Ask for the milestone ID instead. The prompt is raised up front, next to the version confirmation, rather than after release:prepare, so the release does not stop for input in the middle of a multi-minute build. The answer is validated as numeric and cross-checked against the milestone's title on apache/skywalking, so a typo or a stale ID (for example last release's milestone) is reported before anything is committed. NEXT_MILESTONE= answers non-interactively for scripted runs, and a blank answer keeps the old placeholder behaviour but now warns. gh api writes its error body to stdout on failure, so the title lookup gates on gh's exit status; otherwise a 404 payload would be reported as the milestone name. Co-Authored-By: Claude Opus 5 (1M context) --- docs/en/contribution/release-java-agent.md | 8 +++++ tools/releasing/release.sh | 36 +++++++++++++++++++++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/docs/en/contribution/release-java-agent.md b/docs/en/contribution/release-java-agent.md index f7a7ccb107..0b61051e29 100644 --- a/docs/en/contribution/release-java-agent.md +++ b/docs/en/contribution/release-java-agent.md @@ -76,6 +76,14 @@ Then run `gpgconf --kill gpg-agent` and `gpg --sign /dev/null` to cache it. 4. **upload** — upload to Apache SVN `dist/dev` (prompts for SVN credentials) 5. **email vote** — print vote email template with pre-filled version, commit ID, submodule commit, and checksums +Before the long build starts, **prepare** asks for the GitHub milestone ID of the next +development version, which it writes into the reset `CHANGES.md`. Look up the +`Java - ` milestone at https://github.com/apache/skywalking/milestones and +enter its number. The ID is checked against that milestone's title, and you are warned if +they disagree. Set `NEXT_MILESTONE=` to answer non-interactively; leave the prompt +blank to keep the `milestone/xxx` placeholder and edit it by hand before merging the +release PR. + Copy the generated email and send it to `dev@skywalking.apache.org`. Voting remains open for at least 72 hours. At least 3 (+1 binding) PMC votes with more +1 than -1 are required. ## Vote Check diff --git a/tools/releasing/release.sh b/tools/releasing/release.sh index 39e2e96954..c1b1e5197d 100755 --- a/tools/releasing/release.sh +++ b/tools/releasing/release.sh @@ -205,6 +205,40 @@ cmd_prepare() { echo " Tag: v${version}" echo " Next dev version: ${next_version}-SNAPSHOT" echo " Branch: ${branch_name}" + echo "" + + # At the end of this step CHANGES.md is reset for the next development + # version, and its milestone link needs that version's GitHub milestone ID. + # Ask for it here, up front, so the release does not stop for input after + # the long release:prepare build. Set NEXT_MILESTONE= to skip the prompt. + local next_milestone="${NEXT_MILESTONE:-}" + if [ -z "$next_milestone" ]; then + echo " CHANGES.md will be reset for ${next_version}, and its milestone link needs an ID." + echo " Find 'Java - ${next_version}' at https://github.com/apache/skywalking/milestones" + read -rp " Milestone ID for ${next_version} (number, or blank to fill in manually later): " next_milestone + fi + if [ -n "$next_milestone" ]; then + case "$next_milestone" in + *[!0-9]*) error "Milestone ID must be a number, got: ${next_milestone}" ;; + esac + # gh api writes the error body to stdout on failure, so gate on its + # exit status rather than letting a 404 payload become the title. + local milestone_title + if ! milestone_title=$(gh api "repos/apache/skywalking/milestones/${next_milestone}" -q .title 2>/dev/null); then + milestone_title="" + fi + if [ -z "$milestone_title" ]; then + warn " Could not verify milestone ${next_milestone} on apache/skywalking; using it as given." + elif [ "$milestone_title" != "Java - ${next_version}" ]; then + warn " Milestone ${next_milestone} is '${milestone_title}', expected 'Java - ${next_version}'. Double-check it." + else + info " Next milestone: ${next_milestone} (${milestone_title})" + fi + else + next_milestone="xxx" + warn " No milestone ID given; CHANGES.md will keep 'milestone/xxx' - edit it before merging the release PR." + fi + echo "" read -rp "Continue? [y/N] " confirm if [[ ! "$confirm" =~ ^[Yy]$ ]]; then @@ -253,7 +287,7 @@ ${next_version} ------------------ -All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/xxx?closed=1) +All issues and pull requests are [here](https://github.com/apache/skywalking/milestone/${next_milestone}?closed=1) ------------------ Find change logs of all versions [here](changes). From 0343b08ac9e0dac57d1d515d991f2a0eaf9872d9 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 20:30:31 +0800 Subject: [PATCH 3/6] Publish release Docker images from GitHub Actions Official images were pushed by the release manager running `make docker.push.*` locally, from `vote-passed`. That means a multi-arch build of six variants on a laptop, over whatever network it happens to be on, authenticated with a personal Docker Hub session, with no record of what ran. publish-docker.yaml already builds these images for every push to main; it just sends them to ghcr.io tagged with the commit SHA. Give it a `release: released` trigger and point that path at Docker Hub instead, matching what apache/skywalking does. On a release it uses HUB=apache, NAME=skywalking-java-agent and the tag name with `v` stripped, so the published tags are unchanged: apache/skywalking-java-agent:x.y.z-{alpine,java8,java11,java17,java21,java25}. The trigger is `released`, not `published`, so cutting a pre-release does not ship official images. The matrix gains alpine on releases only, leaving the per-commit development images as they are. QEMU and buildx are set up explicitly because the Makefile builds linux/amd64 and linux/arm64. Creating the GitHub Release is what fires this, so the script now does that as a release step: `github-release` publishes the tag with changes/changes-x.y.z.md as its notes, and vote-passed runs it where it used to run `docker`. `docker` stays as a documented fallback for when the workflow fails. Requires DOCKERHUB_USER and DOCKERHUB_TOKEN repository secrets, which apache/skywalking already has. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish-docker.yaml | 58 ++++++++++++++++--- docs/en/contribution/release-java-agent.md | 17 +++++- tools/releasing/release.sh | 65 +++++++++++++++++++--- 3 files changed, 122 insertions(+), 18 deletions(-) diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index 13dd95e4ae..d7b68a7d42 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -16,14 +16,27 @@ name: publish-docker +# Two kinds of image come out of this workflow: +# +# push to main -> per-commit development images, tagged with the commit SHA, +# pushed to GitHub Container Registry. +# release -> the official versioned images for a passed release vote, +# tagged x.y.z-, pushed to Docker Hub as +# apache/skywalking-java-agent. +# +# The release trigger is `released` rather than `published`, so publishing a +# pre-release does not ship official images. Creating the GitHub Release is the +# last step of `tools/releasing/release.sh vote-passed`. on: push: branches: - main + release: + types: + - released env: SKIP_TEST: true - HUB: ghcr.io/apache/skywalking-java jobs: build-tar: @@ -64,9 +77,10 @@ jobs: timeout-minutes: 60 strategy: matrix: - java-version: [ 8, 11, 17, 21, 25 ] - env: - TAG: ${{ github.sha }} + # A release publishes the complete set the previous manual `make + # docker.push.*` produced, alpine included. Per-commit development + # images keep the existing JRE-only set. + base: ${{ github.event_name == 'release' && fromJSON('["alpine","java8","java11","java17","java21","java25"]') || fromJSON('["java8","java11","java17","java21","java25"]') }} steps: - uses: actions/checkout@v2 with: @@ -75,6 +89,26 @@ jobs: with: name: skywalking-agent path: skywalking-agent + - name: Set environment variables + run: | + if [[ "${{ github.event_name }}" == "release" ]]; then + # apache/skywalking-java-agent:x.y.z- on Docker Hub. + # NAME differs from the development images, which is why it is set + # here rather than left to the Makefile default. + echo "HUB=apache" >> $GITHUB_ENV + echo "NAME=skywalking-java-agent" >> $GITHUB_ENV + echo "DOCKER_REGISTRY=docker.io" >> $GITHUB_ENV + echo "DOCKER_USERNAME=${{ secrets.DOCKERHUB_USER }}" >> $GITHUB_ENV + echo "DOCKER_PASSWORD=${{ secrets.DOCKERHUB_TOKEN }}" >> $GITHUB_ENV + TAG=${{ github.event.release.tag_name }} + echo "TAG=${TAG#v}" >> $GITHUB_ENV + else + echo "HUB=ghcr.io/apache/skywalking-java" >> $GITHUB_ENV + echo "DOCKER_REGISTRY=ghcr.io" >> $GITHUB_ENV + echo "DOCKER_USERNAME=${{ github.actor }}" >> $GITHUB_ENV + echo "DOCKER_PASSWORD=${{ secrets.GITHUB_TOKEN }}" >> $GITHUB_ENV + echo "TAG=${{ github.sha }}" >> $GITHUB_ENV + fi - name: Disable containerd image store run: | DAEMON_JSON="/etc/docker/daemon.json" @@ -93,8 +127,14 @@ jobs: - name: Log in to the Container registry uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 with: - registry: ${{ env.HUB }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Build docker image - run: make docker.push.java${{ matrix.java-version }} || make docker.push.java${{ matrix.java-version }} + registry: ${{ env.DOCKER_REGISTRY }} + username: ${{ env.DOCKER_USERNAME }} + password: ${{ env.DOCKER_PASSWORD }} + # The Makefile builds linux/amd64 and linux/arm64, which needs emulation + # and the docker-container buildx driver. + - name: Set up QEMU + uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - name: Build and push docker image + run: make docker.push.${{ matrix.base }} || make docker.push.${{ matrix.base }} diff --git a/docs/en/contribution/release-java-agent.md b/docs/en/contribution/release-java-agent.md index 0b61051e29..ba29df506d 100644 --- a/docs/en/contribution/release-java-agent.md +++ b/docs/en/contribution/release-java-agent.md @@ -109,6 +109,21 @@ argument (`./release.sh docker 9.7.0`) or `RELEASE_VERSION=9.7.0`. After the vote passes, run `vote-passed` which executes: 1. **promote** — move packages from `dist/dev` to `dist/release` in Apache SVN (prompts for SVN credentials), then release the Nexus staging repository at https://repository.apache.org and update the website download page -2. **docker** — build and push all Docker image variants (alpine, java8, java11, java17, java21, java25) +2. **github-release** — publish the GitHub Release for the tag, using `changes/changes-x.y.z.md` as its notes 3. **email announce** — print announcement email template. Copy and send to `dev@skywalking.apache.org` and `announce@apache.org` 4. **cleanup** (optional) — if old version is provided, remove it from `dist/release`. Update download page links to point to `https://archive.apache.org/dist/skywalking` + +### Docker images +Docker images are published by GitHub Actions, not from your machine. Publishing the +GitHub Release fires the `release: released` trigger in +[`.github/workflows/publish-docker.yaml`](../../../.github/workflows/publish-docker.yaml), +which builds every base variant and pushes +`apache/skywalking-java-agent:x.y.z-{alpine,java8,java11,java17,java21,java25}` to Docker +Hub for `linux/amd64` and `linux/arm64`. Watch that workflow; if it fails you can fall back +to pushing from your machine with `./tools/releasing/release.sh docker x.y.z`, which needs +you to be logged in to Docker Hub with push access to the `apache` organisation. + +The same workflow keeps publishing per-commit development images to +`ghcr.io/apache/skywalking-java` on every push to `main`; only the `release` event +publishes official versioned images. It requires the `DOCKERHUB_USER` and +`DOCKERHUB_TOKEN` repository secrets, as `apache/skywalking` does. diff --git a/tools/releasing/release.sh b/tools/releasing/release.sh index c1b1e5197d..7848138489 100755 --- a/tools/releasing/release.sh +++ b/tools/releasing/release.sh @@ -595,6 +595,49 @@ cmd_docker() { info "Docker images pushed for ${version}." } +# ============================================================ +# github-release — publish the GitHub Release +# ============================================================ +# This is what ships the official Docker images. Publishing a non-prerelease +# fires the `release: released` trigger in .github/workflows/publish-docker.yaml, +# which builds every base variant and pushes them to Docker Hub. Running +# `$0 docker` by hand is only a fallback for when that workflow fails. +cmd_github_release() { + cd "$PROJECT_ROOT" + + local version + version=$(resolve_version "${1:-}") + local tag="v${version}" + local notes_file="changes/changes-${version}.md" + + info "Publishing GitHub Release ${tag}..." + + if ! git ls-remote --tags origin "refs/tags/${tag}" | grep -q .; then + error "Tag ${tag} is not on origin. Push it before publishing the release." + fi + + if gh release view "${tag}" >/dev/null 2>&1; then + warn "GitHub Release ${tag} already exists; leaving it alone." + warn "If the images were not pushed, re-run the workflow or use '$0 docker ${version}'." + return 0 + fi + + local -a notes_args + if [ -f "$notes_file" ]; then + notes_args=(--notes-file "$notes_file") + else + warn " ${notes_file} not found; using auto-generated notes." + notes_args=(--generate-notes) + fi + + gh release create "${tag}" --title "${version}" "${notes_args[@]}" + + info "GitHub Release ${tag} published." + info " publish-docker.yaml is now pushing to Docker Hub:" + info " apache/skywalking-java-agent:${version}-{alpine,java8,java11,java17,java21,java25}" + info " Watch: https://github.com/apache/skywalking-java/actions/workflows/publish-docker.yaml" +} + # ============================================================ # promote — move from dist/dev to dist/release # ============================================================ @@ -615,10 +658,10 @@ cmd_promote() { info "Release ${version} promoted." info "Next steps:" - info " 1. Release the Nexus staging repository" + info " 1. Release the Nexus staging repository at https://repository.apache.org" info " 2. Update website download page" - info " 3. Run: $0 email announce" - info " 4. Run: $0 docker" + info " 3. Run: $0 github-release ${version} (pushes the Docker images via GitHub Actions)" + info " 4. Run: $0 email announce ${version}" } # ============================================================ @@ -680,7 +723,9 @@ cmd_vote_passed() { info "Publishing release ${version}:" echo " Release tag : v${version}" echo " SVN promote : dist/dev/skywalking/java-agent/${version} -> dist/release/..." + echo " GitHub Release : v${version} (this is what triggers the Docker Hub push)" echo " Docker Hub tags : apache/skywalking-java-agent:${version}-{alpine,java8,java11,java17,java21,java25}" + echo " pushed by .github/workflows/publish-docker.yaml, not from here" if [ -n "$old_version" ]; then echo " Remove from SVN : dist/release/skywalking/java-agent/${old_version}" else @@ -696,7 +741,7 @@ cmd_vote_passed() { cmd_promote "$version" echo "" - cmd_docker "$version" + cmd_github_release "$version" echo "" cmd_email announce "$version" @@ -721,8 +766,9 @@ main() { prepare) cmd_prepare "$@" ;; stage) cmd_stage "$@" ;; upload) cmd_upload "$@" ;; - email) cmd_email "$@" ;; - docker) cmd_docker "$@" ;; + email) cmd_email "$@" ;; + github-release) cmd_github_release "$@" ;; + docker) cmd_docker "$@" ;; promote) cmd_promote "$@" ;; cleanup) cmd_cleanup "$@" ;; prepare-vote) cmd_prepare_vote "$@" ;; @@ -749,8 +795,11 @@ main() { echo " prepare-vote [next_ver] Run preflight + prepare + stage + upload + vote email" echo " email [ver] Generate email content" echo " promote [ver] Move from dist/dev to dist/release in SVN" - echo " docker [ver] Build and push Docker images to Docker Hub" - echo " vote-passed [old_ver] Run promote + docker + announce email [+ cleanup]" + echo " github-release [ver] Publish the GitHub Release; this is what pushes" + echo " the Docker images, via publish-docker.yaml" + echo " docker [ver] Push Docker images from this machine (fallback" + echo " for when the workflow fails)" + echo " vote-passed [old_ver] Run promote + github-release + announce [+ cleanup]" echo " cleanup Remove old release from dist/release" ;; esac From 02a563e92d70e787739fc4a1f2a6a7f9215481cb Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 21:50:27 +0800 Subject: [PATCH 4/6] Build release images from the voted tarball, not a rebuild The previous local `cmd_docker` extracted apache-skywalking-java-agent-x.y.z.tgz - the artifact that was signed, uploaded to dist and voted on - and fed that directory to the Dockerfile as ARG DIST. Moving the push into GitHub Actions quietly dropped that: build-tar ran `make build`, so the published image would have contained a recompile of the tag rather than the bits the PMC approved. Skip build-tar on release events and download the tarball from the Apache distribution area instead, then prove it is the right one before it goes into an image: sha512 rules out a truncated download, and verifying the detached signature against the project KEYS file rules out anything the release manager did not sign. `release.sh promote` does the svn mv from dist/dev to dist/release immediately before the GitHub Release that triggers this workflow, so the file is in place by the time the job runs. build-docker needs `always()` in its condition, since a skipped build-tar would otherwise skip it as well. Development images are unaffected and still come from the artifact build-tar uploads. Verified against the real 9.7.0 artifact: sha512 matches, gpg reports a good signature from the release manager's key, and the tarball unpacks to skywalking-agent/, which is what the Makefile passes as ARG DIST. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish-docker.yaml | 43 +++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index d7b68a7d42..3006202ef0 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -39,8 +39,11 @@ env: SKIP_TEST: true jobs: + # Only development images are compiled here. A release must not be rebuilt: its + # image has to contain the exact agent package that was signed, uploaded to + # dist/dev and voted on, so build-docker downloads that tarball instead. build-tar: - if: github.repository == 'apache/skywalking-java' + if: github.repository == 'apache/skywalking-java' && github.event_name != 'release' name: Build Agent runs-on: ubuntu-latest timeout-minutes: 30 @@ -67,7 +70,11 @@ jobs: path: skywalking-agent build-docker: - if: github.repository == 'apache/skywalking-java' + # build-tar is skipped on releases, and a skipped dependency would otherwise + # skip this job too. + if: | + always() && github.repository == 'apache/skywalking-java' && + (needs.build-tar.result == 'success' || needs.build-tar.result == 'skipped') needs: [ build-tar ] name: Build and Push Docker runs-on: ubuntu-latest @@ -85,10 +92,40 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - uses: actions/download-artifact@v4 + - name: Download development agent package + if: github.event_name != 'release' + uses: actions/download-artifact@v4 with: name: skywalking-agent path: skywalking-agent + # The published image must carry the artifact the PMC voted on, not a + # rebuild of it. Take the tarball straight from the Apache distribution + # area and prove it is that one: the sha512 rules out a truncated download, + # and verifying the detached signature against the project KEYS file rules + # out anything the release manager did not sign. `svn mv` from dist/dev to + # dist/release runs in `release.sh promote`, immediately before the GitHub + # Release that triggers this workflow, so the file is already in place. + - name: Download the released agent package + if: github.event_name == 'release' + run: | + set -euo pipefail + TAG=${{ github.event.release.tag_name }} + VERSION=${TAG#v} + BASE="https://dist.apache.org/repos/dist/release/skywalking/java-agent/${VERSION}" + TARBALL="apache-skywalking-java-agent-${VERSION}.tgz" + + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}" + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.asc" + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.sha512" + + sha512sum -c "${TARBALL}.sha512" + + curl -fsSL --retry 5 --retry-delay 10 https://downloads.apache.org/skywalking/KEYS | gpg --import + gpg --verify "${TARBALL}.asc" "${TARBALL}" + + tar -xzf "${TARBALL}" + # The Makefile feeds this directory to the Dockerfile as ARG DIST. + test -d skywalking-agent - name: Set environment variables run: | if [[ "${{ github.event_name }}" == "release" ]]; then From 6c84c6471ba3e1d27b6946cd046e99364f98737a Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 21:57:17 +0800 Subject: [PATCH 5/6] Fail with a clear message when Docker Hub secrets are missing DOCKERHUB_USER and DOCKERHUB_TOKEN are provisioned by ASF INFRA on request and are not set on this repository yet, so the first release to use this workflow would have died inside docker/login-action with nothing pointing at the cause. Check them at the top of the release path and say what is missing, where they come from, and how to publish in the meantime. Document the request process alongside, since .asf.yaml cannot set secrets and it has to go through an INFRA ticket. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish-docker.yaml | 9 +++++++++ docs/en/contribution/release-java-agent.md | 23 ++++++++++++++++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index 3006202ef0..ff8f57f2a3 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -129,6 +129,15 @@ jobs: - name: Set environment variables run: | if [[ "${{ github.event_name }}" == "release" ]]; then + # Provisioned by ASF INFRA on request, as for apache/skywalking. + # Without them docker/login-action fails with an opaque error, so say + # what is actually missing. + if [[ -z "${{ secrets.DOCKERHUB_USER }}" || -z "${{ secrets.DOCKERHUB_TOKEN }}" ]]; then + echo "::error::DOCKERHUB_USER / DOCKERHUB_TOKEN are not set on this repository." + echo "::error::Ask ASF INFRA to add them (see docs/en/contribution/release-java-agent.md)," + echo "::error::or publish from a workstation with './tools/releasing/release.sh docker '." + exit 1 + fi # apache/skywalking-java-agent:x.y.z- on Docker Hub. # NAME differs from the development images, which is why it is set # here rather than left to the Makefile default. diff --git a/docs/en/contribution/release-java-agent.md b/docs/en/contribution/release-java-agent.md index ba29df506d..3107827cff 100644 --- a/docs/en/contribution/release-java-agent.md +++ b/docs/en/contribution/release-java-agent.md @@ -123,7 +123,26 @@ Hub for `linux/amd64` and `linux/arm64`. Watch that workflow; if it fails you ca to pushing from your machine with `./tools/releasing/release.sh docker x.y.z`, which needs you to be logged in to Docker Hub with push access to the `apache` organisation. +The image contains the exact tarball that was voted on. The workflow downloads +`apache-skywalking-java-agent-x.y.z.tgz` from `dist/release`, checks it against the +published `.sha512`, and verifies the `.asc` signature against the project +[KEYS](https://downloads.apache.org/skywalking/KEYS) file before it goes into an image — it +does not rebuild the agent from source. + The same workflow keeps publishing per-commit development images to `ghcr.io/apache/skywalking-java` on every push to `main`; only the `release` event -publishes official versioned images. It requires the `DOCKERHUB_USER` and -`DOCKERHUB_TOKEN` repository secrets, as `apache/skywalking` does. +publishes official versioned images. + +#### Docker Hub credentials +The release path needs the `DOCKERHUB_USER` and `DOCKERHUB_TOKEN` repository secrets. These +are the names used across the other Apache SkyWalking repositories (`apache/skywalking`, +`skywalking-python`, `skywalking-mcp`, ...). They are **not** self-service: `.asf.yaml` +cannot set secrets. File an [ASF INFRA JIRA](https://issues.apache.org/jira/browse/INFRA) +ticket asking for them to be added to `apache/skywalking-java`, referencing that +`apache/skywalking` already has them; INFRA holds the Docker Hub account credentials. See +[GitHub Actions and Secrets](https://infra.apache.org/github-actions-secrets.html). + +Until they exist, the release run fails early with an explicit error and you should publish +with `./tools/releasing/release.sh docker x.y.z` instead. Because `github-release` is +idempotent, you can also add the secrets later and just re-run the failed workflow from the +Actions tab — there is no need to delete and recreate the GitHub Release. From aa5cef403c504d8bf1679729efdb733b068ebda3 Mon Sep 17 00:00:00 2001 From: Wu Sheng Date: Wed, 12 Aug 2026 22:03:50 +0800 Subject: [PATCH 6/6] Fetch the agent package once, not once per image variant The six image variants differ only in the JRE they sit on. The Dockerfile takes BASE_IMAGE and ADDs the same DIST directory, and the agent is Java 8 bytecode that runs on all of them, so one package serves every variant - which is what the old local `make docker.push.alpine docker.push.java8 ...` did from a single extracted tarball. The matrix I added ignored that and had each of the six jobs download and verify its own copy of the 46MB release tarball: 276MB per release pulled from dist.apache.org, which is SVN-backed rather than a CDN, plus six redundant signature checks. Fold the acquisition back into the single upstream job, which now either compiles the agent (development images) or downloads and verifies the voted tarball (releases), and hands the result to the matrix as an artifact. That also drops the `always()` condition the skipped-job arrangement needed. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/publish-docker.yaml | 89 ++++++++++++++------------- 1 file changed, 46 insertions(+), 43 deletions(-) diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index ff8f57f2a3..ba10b71356 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -39,30 +39,67 @@ env: SKIP_TEST: true jobs: - # Only development images are compiled here. A release must not be rebuilt: its - # image has to contain the exact agent package that was signed, uploaded to - # dist/dev and voted on, so build-docker downloads that tarball instead. - build-tar: - if: github.repository == 'apache/skywalking-java' && github.event_name != 'release' - name: Build Agent + # One agent package feeds every image. The variants differ only in the JRE they + # sit on: the Dockerfile takes BASE_IMAGE and ADDs the same DIST directory, and + # the agent itself is Java 8 bytecode that runs on all of them. So this is built + # (or downloaded) exactly once and handed to the matrix below as an artifact, + # rather than each variant fetching its own copy. + agent-package: + if: github.repository == 'apache/skywalking-java' + name: Prepare Agent Package runs-on: ubuntu-latest timeout-minutes: 30 steps: - uses: actions/checkout@v2 with: submodules: true + + # Development images are compiled from the branch. - name: Cache local Maven repository + if: github.event_name != 'release' uses: actions/cache@v4 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-publish-docker-${{ hashFiles('**/pom.xml') }} restore-keys: ${{ runner.os }}-maven-publish-docker- - uses: actions/setup-java@v2 + if: github.event_name != 'release' with: distribution: temurin java-version: 17 - name: Build Agent + if: github.event_name != 'release' run: make build + + # A release is never rebuilt. The published image has to carry the artifact + # the PMC voted on, so take it from the Apache distribution area and prove + # it is that one: the sha512 rules out a truncated download, and verifying + # the detached signature against the project KEYS file rules out anything + # the release manager did not sign. `release.sh promote` does the svn mv + # from dist/dev to dist/release immediately before the GitHub Release that + # triggers this workflow, so the file is in place by the time this runs. + - name: Download the released agent package + if: github.event_name == 'release' + run: | + set -euo pipefail + TAG=${{ github.event.release.tag_name }} + VERSION=${TAG#v} + BASE="https://dist.apache.org/repos/dist/release/skywalking/java-agent/${VERSION}" + TARBALL="apache-skywalking-java-agent-${VERSION}.tgz" + + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}" + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.asc" + curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.sha512" + + sha512sum -c "${TARBALL}.sha512" + + curl -fsSL --retry 5 --retry-delay 10 https://downloads.apache.org/skywalking/KEYS | gpg --import + gpg --verify "${TARBALL}.asc" "${TARBALL}" + + tar -xzf "${TARBALL}" + # The Makefile passes this directory to the Dockerfile as ARG DIST. + test -d skywalking-agent + - uses: actions/upload-artifact@v4 name: Upload Agent with: @@ -70,12 +107,8 @@ jobs: path: skywalking-agent build-docker: - # build-tar is skipped on releases, and a skipped dependency would otherwise - # skip this job too. - if: | - always() && github.repository == 'apache/skywalking-java' && - (needs.build-tar.result == 'success' || needs.build-tar.result == 'skipped') - needs: [ build-tar ] + if: github.repository == 'apache/skywalking-java' + needs: [ agent-package ] name: Build and Push Docker runs-on: ubuntu-latest permissions: @@ -92,40 +125,10 @@ jobs: - uses: actions/checkout@v2 with: submodules: true - - name: Download development agent package - if: github.event_name != 'release' - uses: actions/download-artifact@v4 + - uses: actions/download-artifact@v4 with: name: skywalking-agent path: skywalking-agent - # The published image must carry the artifact the PMC voted on, not a - # rebuild of it. Take the tarball straight from the Apache distribution - # area and prove it is that one: the sha512 rules out a truncated download, - # and verifying the detached signature against the project KEYS file rules - # out anything the release manager did not sign. `svn mv` from dist/dev to - # dist/release runs in `release.sh promote`, immediately before the GitHub - # Release that triggers this workflow, so the file is already in place. - - name: Download the released agent package - if: github.event_name == 'release' - run: | - set -euo pipefail - TAG=${{ github.event.release.tag_name }} - VERSION=${TAG#v} - BASE="https://dist.apache.org/repos/dist/release/skywalking/java-agent/${VERSION}" - TARBALL="apache-skywalking-java-agent-${VERSION}.tgz" - - curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}" - curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.asc" - curl -fsSL --retry 5 --retry-delay 10 -O "${BASE}/${TARBALL}.sha512" - - sha512sum -c "${TARBALL}.sha512" - - curl -fsSL --retry 5 --retry-delay 10 https://downloads.apache.org/skywalking/KEYS | gpg --import - gpg --verify "${TARBALL}.asc" "${TARBALL}" - - tar -xzf "${TARBALL}" - # The Makefile feeds this directory to the Dockerfile as ARG DIST. - test -d skywalking-agent - name: Set environment variables run: | if [[ "${{ github.event_name }}" == "release" ]]; then