diff --git a/.github/workflows/publish-docker.yaml b/.github/workflows/publish-docker.yaml index 13dd95e4ae..ba10b71356 100644 --- a/.github/workflows/publish-docker.yaml +++ b/.github/workflows/publish-docker.yaml @@ -16,37 +16,90 @@ 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: + # 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: Build Agent + 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: @@ -55,7 +108,7 @@ jobs: build-docker: if: github.repository == 'apache/skywalking-java' - needs: [ build-tar ] + needs: [ agent-package ] name: Build and Push Docker runs-on: ubuntu-latest permissions: @@ -64,9 +117,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 +129,35 @@ jobs: with: name: skywalking-agent path: skywalking-agent + - 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. + 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 +176,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 5b941a822c..3107827cff 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 @@ -92,8 +100,49 @@ 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) +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 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. + +#### 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. diff --git a/tools/releasing/release.sh b/tools/releasing/release.sh index 22c200dc46..7848138489 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 # ============================================================ @@ -160,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 @@ -208,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). @@ -249,12 +328,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 +398,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 +444,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 +573,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" @@ -517,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 # ============================================================ @@ -524,7 +645,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..." @@ -537,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}" } # ============================================================ @@ -578,11 +699,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 +712,38 @@ 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 " 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 + 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_docker + + cmd_promote "$version" + echo "" + cmd_github_release "$version" echo "" - cmd_email announce + cmd_email announce "$version" if [ -n "$old_version" ]; then echo "" @@ -618,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 "$@" ;; @@ -634,17 +783,24 @@ 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 " 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 }