This is an automated email from the ASF dual-hosted git repository.

uranusjr pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new b0300009a42 Verify Java SDK artifact integrity continuously (#69643)
b0300009a42 is described below

commit b0300009a427e147c998ef736d7c6688d14f2495
Author: Tzu-ping Chung <[email protected]>
AuthorDate: Thu Jul 9 20:38:00 2026 +0800

    Verify Java SDK artifact integrity continuously (#69643)
---
 .github/workflows/java-sdk-release-verify.yml     | 177 ++++++++++++++++++++++
 java-sdk/.gitattributes                           |   4 +
 java-sdk/README.md                                |   9 +-
 java-sdk/scripts/ci/smoke-test-staged-binaries.sh |  92 +++++++++++
 java-sdk/scripts/ci/verify-source-release.sh      | 123 +++++++++++++++
 5 files changed, 401 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/java-sdk-release-verify.yml 
b/.github/workflows/java-sdk-release-verify.yml
new file mode 100644
index 00000000000..9384f8746b1
--- /dev/null
+++ b/.github/workflows/java-sdk-release-verify.yml
@@ -0,0 +1,177 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# Runs the Java SDK reviewer checklist ("Verifying a release" in
+# java-sdk/README.md) automatically, minus the GPG signature (only the release
+# manager's key can satisfy that).
+#
+#   * on a pull request touching java-sdk/ -> verify HEAD can produce a source
+#     release that is self-contained and builds from scratch.
+#   * on a `java-sdk/**` tag push -> the same check against the tagged commit
+#     (release readiness gate).
+#   * on manual dispatch -> verify the ACTUAL staged artifacts: the source
+#     package in dist/dev and the closed Nexus staging repo. Run this after
+#     staging, before sending the [VOTE] email.
+#
+# The environment setup intentionally mirror what a human verifying the release
+# would do. An (unpinned) latest Gradle is downloaded to create the Gradle
+# wrapper from, and a more modern JDK is used to run things.
+
+---
+name: Java SDK release verification
+
+on:  # yamllint disable-line rule:truthy
+  pull_request:
+    paths:
+      - "java-sdk/**"
+      - ".github/workflows/java-sdk-release-verify.yml"
+  push:
+    tags:
+      - "java-sdk/**"
+  workflow_dispatch:
+    inputs:
+      tag:
+        description: "Java SDK release tag (starts with 'java-sdk/') to verify"
+        required: true
+        type: string
+      nexus_repo_id:
+        description: "Nexus staging repo id (the NNNN in 
orgapacheairflow-NNNN)"
+        required: true
+        type: string
+
+permissions:
+  contents: read
+
+concurrency:
+  group: java-sdk-release-verify-${{ github.event_name }}-${{ github.ref 
}}-${{ inputs.tag }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+  JAVA_VERSION: "21"
+
+jobs:
+  source-release:
+    name: Source release builds cleanly
+    if: github.event_name != 'workflow_dispatch'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout
+        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # 
v7.0.0
+        with:
+          ref: ${{ github.event_name == 'push' && github.ref || github.sha }}
+          fetch-depth: 0
+          persist-credentials: false
+      - name: Set up JDK ${{ env.JAVA_VERSION }}
+        uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520  # 
v5.4.0
+        with:
+          distribution: temurin
+          java-version: ${{ env.JAVA_VERSION }}
+      - name: Use an isolated Gradle home
+        run: echo "GRADLE_USER_HOME=${RUNNER_TEMP}/gradle-home" >> 
"$GITHUB_ENV"
+      - name: Resolve the ref to build the source release from
+        run: |
+          set -euo pipefail
+          if [ "${{ github.event_name }}" = "push" ]; then
+            echo "RELEASE_REF=${GITHUB_REF_NAME}" >> "$GITHUB_ENV"
+          else  # A PR HEAD.
+            echo "RELEASE_REF=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
+          fi
+      - name: Build the source tarball from the ref (project's wrapper)
+        working-directory: java-sdk
+        run: ./gradlew --no-daemon sourceTarball checksumSourceTarball 
-PgitRef="${RELEASE_REF}"
+      - name: Install the latest Gradle to bootstrap a new wrapper for the 
source extracted from the tarball
+        run: |
+          set -euo pipefail
+          meta=$(curl -fsSL https://services.gradle.org/versions/current)
+          ver=$(echo "$meta" | jq -r .version)
+          url=$(echo "$meta" | jq -r .downloadUrl)
+          sum=$(curl -fsSL "$(echo "$meta" | jq -r .checksumUrl)")
+          curl -fsSL -o /tmp/gradle.zip "$url"
+          echo "${sum}  /tmp/gradle.zip" | sha256sum -c -
+          unzip -q -d "$HOME/gradle" /tmp/gradle.zip
+          echo "$HOME/gradle/gradle-${ver}/bin" >> "$GITHUB_PATH"
+      - name: Verify the source tarball
+        run: |
+          set -euo pipefail
+          tarball=$(ls 
java-sdk/build/distributions/apache-airflow-java-sdk-*-src.tar.gz)
+          java-sdk/scripts/ci/verify-source-release.sh \
+            --tarball "${tarball}" \
+            --sha512 "${tarball}.sha512" \
+            --tag "${RELEASE_REF}"
+
+  artifact:
+    name: Staged artifacts pass the reviewer checklist
+    if: github.event_name == 'workflow_dispatch'
+    runs-on: ubuntu-latest
+    steps:
+      - name: Checkout tag
+        uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0  # 
v7.0.0
+        with:
+          ref: ${{ inputs.tag }}
+          fetch-depth: 0
+          persist-credentials: false
+      - name: Set up JDK ${{ env.JAVA_VERSION }}
+        uses: actions/setup-java@1bcf9fb12cf4aa7d266a90ae39939e61372fe520  # 
v5.4.0
+        with:
+          distribution: temurin
+          java-version: ${{ env.JAVA_VERSION }}
+      - name: Use an isolated Gradle home
+        run: echo "GRADLE_USER_HOME=${RUNNER_TEMP}/gradle-home" >> 
"$GITHUB_ENV"
+      - name: Download the staged source package from dist/dev
+        env:
+          TAG: ${{ inputs.tag }}
+        run: |
+          set -euo pipefail
+          rcdir="${TAG#java-sdk/}"          # <version>-rc<N>
+          version="${rcdir%-rc*}"           # <version>
+          
base="https://dist.apache.org/repos/dist/dev/airflow/java-sdk/${rcdir}";
+          file="apache-airflow-java-sdk-${version}-src.tar.gz"
+          mkdir -p staged
+          curl -fsSL -o "staged/${file}" "${base}/${file}"
+          curl -fsSL -o "staged/${file}.sha512" "${base}/${file}.sha512"
+      - name: Install the latest Gradle to bootstrap a new wrapper for the 
source extracted from the tarball
+        run: |
+          set -euo pipefail
+          meta=$(curl -fsSL https://services.gradle.org/versions/current)
+          ver=$(echo "$meta" | jq -r .version)
+          url=$(echo "$meta" | jq -r .downloadUrl)
+          sum=$(curl -fsSL "$(echo "$meta" | jq -r .checksumUrl)")
+          curl -fsSL -o /tmp/gradle.zip "$url"
+          echo "${sum}  /tmp/gradle.zip" | sha256sum -c -
+          unzip -q -d "$HOME/gradle" /tmp/gradle.zip
+          echo "$HOME/gradle/gradle-${ver}/bin" >> "$GITHUB_PATH"
+      - name: Verify the staged source package
+        env:
+          TAG: ${{ inputs.tag }}
+        run: |
+          set -euo pipefail
+          rcdir="${TAG#java-sdk/}"; version="${rcdir%-rc*}"
+          file="apache-airflow-java-sdk-${version}-src.tar.gz"
+          java-sdk/scripts/ci/verify-source-release.sh \
+            --tarball "staged/${file}" \
+            --sha512 "staged/${file}.sha512" \
+            --tag "${TAG}"
+      - name: Smoke-test the staged convenience binaries
+        env:
+          TAG: ${{ inputs.tag }}
+          NEXUS_REPO_ID: ${{ inputs.nexus_repo_id }}
+        run: |
+          set -euo pipefail
+          rcdir="${TAG#java-sdk/}"; version="${rcdir%-rc*}"
+          java-sdk/scripts/ci/smoke-test-staged-binaries.sh \
+            --nexus-repo-id "${NEXUS_REPO_ID}" \
+            --version "${version}"
diff --git a/java-sdk/.gitattributes b/java-sdk/.gitattributes
index 6cd69336377..04a9c526a5e 100644
--- a/java-sdk/.gitattributes
+++ b/java-sdk/.gitattributes
@@ -18,3 +18,7 @@
 /gradlew export-ignore
 /gradlew.bat export-ignore
 /gradle/wrapper/gradle-wrapper.jar export-ignore
+
+# ktlint reads .editorconfig for its formatting rules at build time, and the
+# build task requires the lint to pass, so the source release must ship it.
+/.editorconfig -export-ignore
diff --git a/java-sdk/README.md b/java-sdk/README.md
index 2c57c9be143..621266ee267 100644
--- a/java-sdk/README.md
+++ b/java-sdk/README.md
@@ -389,16 +389,17 @@ manager, before sending the vote — should run against the 
source package in
 
 3. **Diff against the git tag.** Extract the tarball and compare it with a
    clean checkout of the tag it claims to be built from. They should be
-   identical except `gradlew`, `gradlew.bat`, and `gradle-wrapper.jar`. The
-   extracted top-level directory should be `apache-airflow-java-sdk-<version>`
-   without the `-src` suffix that only appears in the tarball's own filename:
+   identical except for the files kept out of the source release via
+   `.gitattributes` `export-ignore`. The extracted top-level directory should 
be
+   `apache-airflow-java-sdk-<version>` without the `-src` suffix that only
+   appears in the tarball's own filename:
 
    ```bash
    tar xzf apache-airflow-java-sdk-<VERSION>-src.tar.gz
    git clone --branch java-sdk/<VERSION>-rc<N> \
      https://github.com/apache/airflow.git tag-checkout
    diff -rq apache-airflow-java-sdk-<VERSION>/ tag-checkout/java-sdk/ \
-     | grep -vE ': (gradlew|gradlew\.bat|gradle-wrapper\.jar)$'
+     | grep -vE ': (gradlew|gradlew\.bat|gradle-wrapper\.jar|scripts)$'
    ```
 
    Any remaining diff output is unexpected and should block the vote.
diff --git a/java-sdk/scripts/ci/smoke-test-staged-binaries.sh 
b/java-sdk/scripts/ci/smoke-test-staged-binaries.sh
new file mode 100755
index 00000000000..b6198f06628
--- /dev/null
+++ b/java-sdk/scripts/ci/smoke-test-staged-binaries.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Smoke-test the STAGED convenience binaries: resolve the airflow-sdk-bom and
+# every artifact it manages from the ASF Nexus staging repository. This is only
+# meaningful for a staged release candidate, so it lives apart from
+# verify-source-release.sh (which checks the source package and also runs on 
PRs
+# and tag pushes, where nothing is staged).
+#
+# Usage:
+#   smoke-test-staged-binaries.sh --nexus-repo-id <NNNN> --version <VERSION>
+#
+# Requirements:
+#   Bash with common shell tools, Gradle, a JDK, and network access to
+#   repository.apache.org and Maven Central.
+
+set -euo pipefail
+
+nexus_repo_id="" version=""
+while [ "$#" -gt 0 ]; do
+  case "$1" in
+    --nexus-repo-id) nexus_repo_id="$2"; shift 2 ;;
+    --version) version="$2"; shift 2 ;;
+    *) echo "Unknown argument: $1" >&2; exit 2 ;;
+  esac
+done
+if [ -z "$nexus_repo_id" ] || [ -z "$version" ]; then
+  echo "ERROR: --nexus-repo-id and --version are required" >&2
+  exit 2
+fi
+
+nexus_url="https://repository.apache.org/content/repositories/orgapacheairflow-$nexus_repo_id";
+work="$(mktemp -d)"
+trap 'rm -rf "$work"' EXIT
+
+echo "==> Staged-binary smoke test (Nexus repo 
orgapacheairflow-$nexus_repo_id)"
+
+# Derive the artifact list from the published BOM rather than hard-coding it. 
The
+# BOM is the authoritative set (kept honest against the published modules by
+# :bom:verifyBomCoverage), so this never drifts as modules are added/removed.
+# Fetching the BOM pom also confirms the BOM itself is staged.
+bom_pom="$work/airflow-sdk-bom.pom"
+curl -fsSL -o "$bom_pom" \
+  
"$nexus_url/org/apache/airflow/airflow-sdk-bom/$version/airflow-sdk-bom-$version.pom"
+artifacts="$(sed -n '/<dependencyManagement>/,/<\/dependencyManagement>/p' 
"$bom_pom" \
+  | grep -oE '<artifactId>[^<]+</artifactId>' | sed -E 's#</?artifactId>##g' | 
sort -u)"
+if [ -z "$artifacts" ]; then
+  echo "ERROR: no managed artifacts found in the BOM ($bom_pom)" >&2
+  exit 1
+fi
+echo "    BOM manages:"
+while IFS= read -r a; do echo "      $a"; done <<< "$artifacts"
+
+smoke="$work/smoke"
+mkdir -p "$smoke"
+echo 'rootProject.name = "airflow-sdk-nexus-smoke"' > 
"$smoke/settings.gradle.kts"
+{
+  echo 'plugins { id("java-library") }'
+  echo 'repositories {'
+  echo "    maven { url = uri(\"$nexus_url/\") }"
+  echo '    mavenCentral()'
+  echo '}'
+  echo 'dependencies {'
+  echo "    
implementation(platform(\"org.apache.airflow:airflow-sdk-bom:$version\"))"
+  for a in $artifacts; do
+    echo "    implementation(\"org.apache.airflow:$a\")"
+  done
+  echo '}'
+} > "$smoke/build.gradle.kts"
+
+(
+  cd "$smoke"
+  # Forces resolution of every BOM-managed artifact from the staging repo.
+  gradle --no-daemon dependencies --configuration runtimeClasspath
+)
+echo "    OK"
+echo "Staged-binary smoke test passed."
diff --git a/java-sdk/scripts/ci/verify-source-release.sh 
b/java-sdk/scripts/ci/verify-source-release.sh
new file mode 100755
index 00000000000..496c8e6a4ae
--- /dev/null
+++ b/java-sdk/scripts/ci/verify-source-release.sh
@@ -0,0 +1,123 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Run the reviewer checklist from java-sdk/README.md against an Apache Airflow
+# Java SDK source-release tarball (except the GPG signature).
+#
+# Usage:
+#   verify-source-release.sh --tarball <path> --tag java-sdk/<version>-rc<N> \
+#       [--sha512 <path>]
+#
+# Requirements:
+#   Bash with common shell tools, Git, Gradle, JDK 11.
+#
+# Staged convenience binaries are checked separately by
+# smoke-test-staged-binaries.sh (only meaningful for a staged release).
+
+set -euo pipefail
+
+REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)"
+
+tarball="" tag="" sha512=""
+while [ "$#" -gt 0 ]; do
+  case "$1" in
+    --tarball) tarball="$2"; shift 2 ;;
+    --tag) tag="$2"; shift 2 ;;
+    --sha512) sha512="$2"; shift 2 ;;
+    *) echo "Unknown argument: $1" >&2; exit 2 ;;
+  esac
+done
+if [ -z "$tarball" ] || [ -z "$tag" ]; then
+  echo "ERROR: --tarball and --tag are required" >&2
+  exit 2
+fi
+tarball="$(cd "$(dirname "$tarball")" && pwd)/$(basename "$tarball")"
+[ -z "$sha512" ] || sha512="$(cd "$(dirname "$sha512")" && pwd)/$(basename 
"$sha512")"
+
+work="$(mktemp -d)"
+tagco="$work/tagco"
+cleanup() {
+  git -C "$REPO_ROOT" worktree remove --force "$tagco" >/dev/null 2>&1 || true
+  rm -rf "$work"
+}
+trap cleanup EXIT
+
+echo "==> 1. Checksum"
+if [ -n "$sha512" ]; then
+  ( cd "$(dirname "$tarball")" && sha512sum -c "$sha512" )
+else
+  echo "    no --sha512 given; skipping"
+fi
+
+echo "==> 2. Extract to a clean directory outside the repo"
+tar -xzf "$tarball" -C "$work"
+extracted="$(find "$work" -maxdepth 1 -type d -name 
'apache-airflow-java-sdk-*' | head -n1)"
+[ -n "$extracted" ] || { echo "ERROR: no apache-airflow-java-sdk-* directory 
inside the tarball" >&2; exit 1; }
+echo "    $extracted"
+
+echo "==> 3. No compiled/binary files (ASF source releases must be 
source-only)"
+bins="$(find "$extracted" -type f -exec sh -c 'file -b "$1" | grep -qviE 
"text|json|xml|empty" && echo "$1"' _ {} \; || true)"
+if [ -n "$bins" ]; then
+  echo "ERROR: non-source files found in the tarball:" >&2
+  echo "$bins" >&2
+  exit 1
+fi
+echo "    OK"
+
+echo "==> 4. Diff against the tagged source (only export-ignored files may 
differ)"
+git -C "$REPO_ROOT" worktree add --detach --quiet "$tagco" "$tag"
+diffout="$(diff -rq "$extracted" "$tagco/java-sdk" \
+  | grep -vE ': (gradlew|gradlew\.bat|gradle-wrapper\.jar|scripts)$' || true)"
+if [ -n "$diffout" ]; then
+  echo "ERROR: tarball differs from the tag beyond the export-ignored files:" 
>&2
+  echo "$diffout" >&2
+  exit 1
+fi
+echo "    OK"
+
+echo "==> 5. Build from scratch (regenerate the wrapper, then ./gradlew build)"
+props="$extracted/gradle/wrapper/gradle-wrapper.properties"
+gv="$(sed -n 's#^distributionUrl=.*/gradle-\(.*\)-bin\.zip#\1#p' "$props")"
+gsum="$(sed -n 's#^distributionSha256Sum=##p' "$props")"
+if [ -z "$gv" ] || [ -z "$gsum" ]; then
+  echo "ERROR: could not read Gradle version/sha256 from $props" >&2
+  exit 1
+fi
+(
+  cd "$extracted"
+  gradle --no-daemon wrapper --gradle-version "$gv" 
--gradle-distribution-sha256-sum "$gsum"
+  ./gradlew --no-daemon build   # `build` runs `check`, which runs 
:bom:verifyBomCoverage
+)
+echo "    OK"
+
+echo "==> 6. LICENSE and NOTICE present in every published jar"
+repo="$work/maven-repo"
+( cd "$extracted" && ./gradlew --no-daemon publish -PmavenUrl="file://$repo" 
-PskipSigning=true )
+missing=0
+while IFS= read -r jar; do
+  for entry in META-INF/LICENSE META-INF/NOTICE; do
+    if ! unzip -l "$jar" | grep -qE "${entry}$"; then
+      echo "ERROR: ${entry} missing from ${jar##*/}" >&2
+      missing=1
+    fi
+  done
+done < <(find "$repo" -name '*.jar')
+[ "$missing" -eq 0 ] || exit 1
+echo "    OK"
+
+echo "All release-verification checks passed."

Reply via email to