From 1591e8901bfa925970e6f769d82aa546a91760cb Mon Sep 17 00:00:00 2001 From: Aditya Parikh Date: Sun, 2 Aug 2026 17:00:39 -0400 Subject: [PATCH] fix(ci): repair release workflows that silently skipped or never ran Verified findings from a CodeRabbit review, rebased onto current main. Jobs that never run: - release-publish.yml: publish-mcp-registry gates on needs.validate-release.outputs.proceed but does not list validate-release in needs:. A needs. reference to a job absent from needs: resolves to empty rather than erroring, so the condition is always falsy and MCP Registry publishing never happens. - atr-release.yml / atr-release-test.yml: finish-release has no needs: at all, only `if: always() && inputs.skip_vote`, so it announces the release in parallel with the jobs that produce it. Steps that fail outright: - atr-release.yml uses actions/upload-artifact@v3, retired by GitHub. Pinned to the SHA already used elsewhere in this repo. - The MCP Publisher download 404s today: the asset is mcp-publisher_linux_amd64.tar.gz, not mcp-publisher-linux-amd64.tar.gz. Because `curl -L` has no --fail and is piped straight into tar, the error page is streamed to tar instead of failing the step. Pinned to v1.8.0 and verified against the release's published checksum. Other correctness and hardening: - apache/tooling-actions upload-to-atr / release-on-atr were referenced at @main; pinned to a commit SHA. ASF's Actions allow-list matches by exact SHA, and a mutable ref is not reproducible. - Pass workflow_dispatch inputs through step env instead of interpolating them into shell text. - The Docker Hub publish step warned and exited 0 when credentials were absent, so a release could report success while publishing nothing; it now fails. Credentials move to JIB_TO_AUTH_* env so the token is not in the process argument list. - The build.gradle.kts version sed was unanchored; restrict it to the top-level `version = ` declaration. - Drop an unresolved ${VOTE_THREAD_NOTE} from the announcement heredoc; the vote thread URL is already appended by the block below it. - The release step rewrote only .packages[0].version in server.json, leaving the second package on -SNAPSHOT so the registry entry advertised an image tag that is never published. Rewrite every package and fail if a SNAPSHOT survives. Co-Authored-By: Claude Opus 5 (1M context) Signed-off-by: Aditya Parikh --- .github/workflows/atr-release-test.yml | 19 ++++-- .github/workflows/atr-release.yml | 26 +++++--- .github/workflows/release-publish.yml | 92 +++++++++++++++++--------- server.json | 4 +- 4 files changed, 93 insertions(+), 48 deletions(-) diff --git a/.github/workflows/atr-release-test.yml b/.github/workflows/atr-release-test.yml index cfe48825..a37d51c5 100644 --- a/.github/workflows/atr-release-test.yml +++ b/.github/workflows/atr-release-test.yml @@ -115,8 +115,11 @@ jobs: fetch-depth: 0 # Full history for proper tagging - name: Create test tag if not exists + env: + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_CANDIDATE: ${{ inputs.release_candidate }} run: | - TEST_TAG="v${{ inputs.release_version }}-${{ inputs.release_candidate }}" + TEST_TAG="v${RELEASE_VERSION}-${RELEASE_CANDIDATE}" if ! git rev-parse "${TEST_TAG}" >/dev/null 2>&1; then echo "Creating test tag: ${TEST_TAG}" git config user.email "test@example.com" @@ -221,7 +224,7 @@ jobs: - name: Upload artifacts to ATR (Real) if: ${{ !inputs.dry_run }} - uses: apache/tooling-actions/upload-to-atr@main + uses: apache/tooling-actions/upload-to-atr@81f0a6cf2202b362ee6434b82c271b1849133581 # main @ 2026-07-27 with: project: ${{ env.ATR_PROJECT_NAME }} version: ${{ inputs.release_version }}-${{ inputs.release_candidate }} @@ -261,7 +264,9 @@ jobs: name: Vote Instructions (TEST) runs-on: ubuntu-latest needs: [ compose-release ] - if: ${{ !inputs.skip_vote && (success() || inputs.skip_compose) }} + # A skipped `needs` job skips its dependents unless the if: uses always(). + # Without it the skip_compose=true path could never reach this job. + if: ${{ always() && !inputs.skip_vote && (needs.compose-release.result == 'success' || inputs.skip_compose) }} steps: - name: Generate test vote email @@ -307,6 +312,10 @@ jobs: finish-release: name: Finalize Release (TEST) runs-on: ubuntu-latest + # Must wait for the compose and vote jobs even though both may be skipped + # (skip_compose / skip_vote). always() keeps this reachable in that case, + # but without needs: it would otherwise start in parallel with them. + needs: [ compose-release, vote-instructions ] if: ${{ always() && inputs.skip_vote }} steps: @@ -343,7 +352,7 @@ jobs: - name: Resolve vote and announce on ATR (Real) if: ${{ !inputs.dry_run }} - uses: apache/tooling-actions/release-on-atr@main + uses: apache/tooling-actions/release-on-atr@81f0a6cf2202b362ee6434b82c271b1849133581 # main @ 2026-07-27 with: version: ${{ inputs.release_version }} atr-host: release-test.apache.org @@ -382,7 +391,7 @@ jobs: # Test main platform echo "1. Testing main platform (https://release-test.apache.org):" - if curl -s -o /dev/null -w " HTTP Status: %{http_code}\n" https://release-test.apache.org; then + if curl -fsS --max-time 15 -o /dev/null -w " HTTP Status: %{http_code}\n" https://release-test.apache.org; then echo " ✅ Platform is reachable" else echo " ❌ Platform unreachable" diff --git a/.github/workflows/atr-release.yml b/.github/workflows/atr-release.yml index 9053e1cd..9f901f95 100644 --- a/.github/workflows/atr-release.yml +++ b/.github/workflows/atr-release.yml @@ -158,9 +158,13 @@ jobs: fetch-depth: 0 # Full history for proper tagging - name: Verify release tag + env: + RELEASE_VERSION: ${{ inputs.release_version }} + RELEASE_CANDIDATE: ${{ inputs.release_candidate }} run: | - if ! git rev-parse "v${{ inputs.release_version }}-${{ inputs.release_candidate }}" >/dev/null 2>&1; then - echo "ERROR: Release tag not found: v${{ inputs.release_version }}-${{ inputs.release_candidate }}" + RELEASE_TAG="v${RELEASE_VERSION}-${RELEASE_CANDIDATE}" + if ! git rev-parse "${RELEASE_TAG}" >/dev/null 2>&1; then + echo "ERROR: Release tag not found: ${RELEASE_TAG}" exit 1 fi echo "✓ Release tag verified" @@ -236,7 +240,7 @@ jobs: # gpg --armor --detach-sign build/distributions/*.jar - name: Upload artifacts to ATR - uses: apache/tooling-actions/upload-to-atr@main + uses: apache/tooling-actions/upload-to-atr@81f0a6cf2202b362ee6434b82c271b1849133581 # main @ 2026-07-27 with: asf-uid: ${{ secrets.ASF_USERNAME }} project: ${{ env.ATR_PROJECT_NAME }} @@ -246,7 +250,7 @@ jobs: ssh-port: 2222 - name: Upload artifacts for review - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0 with: name: release-artifacts-${{ inputs.release_version }}-${{ inputs.release_candidate }} path: build/distributions/ @@ -274,7 +278,9 @@ jobs: name: Vote Instructions runs-on: ubuntu-latest needs: [ compose-release ] - if: ${{ !inputs.skip_vote && (success() || inputs.skip_compose) }} + # A skipped `needs` job skips its dependents unless the if: uses always(). + # Without it the skip_compose=true path could never reach this job. + if: ${{ always() && !inputs.skip_vote && (needs.compose-release.result == 'success' || inputs.skip_compose) }} steps: - name: Generate vote email template @@ -329,7 +335,7 @@ jobs: echo "**Note**: ATR voting automation is not yet available. Manual process required." >> $GITHUB_STEP_SUMMARY - name: Upload vote email template - uses: actions/upload-artifact@v3 + uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # 6.0.0 with: name: vote-email-template path: vote-email.txt @@ -338,6 +344,10 @@ jobs: finish-release: name: Finalize and Announce Release runs-on: ubuntu-latest + # Must wait for the compose and vote jobs even though both may be skipped + # (skip_compose / skip_vote). always() keeps this reachable in that case, + # but without needs: it would otherwise start in parallel with them. + needs: [ compose-release, vote-instructions ] if: ${{ always() && inputs.skip_vote }} # Manual trigger after vote passes steps: @@ -347,7 +357,7 @@ jobs: ref: "v${{ inputs.release_version }}-${{ inputs.release_candidate }}" - name: Resolve vote and announce on ATR - uses: apache/tooling-actions/release-on-atr@main + uses: apache/tooling-actions/release-on-atr@81f0a6cf2202b362ee6434b82c271b1849133581 # main @ 2026-07-27 with: version: ${{ inputs.release_version }} atr-host: release-test.apache.org @@ -428,7 +438,7 @@ jobs: echo "" >> $GITHUB_STEP_SUMMARY echo "#### Distribution:" >> $GITHUB_STEP_SUMMARY echo "- Apache Mirrors: https://www.apache.org/dyn/closer.lua/solr/mcp/${{ inputs.release_version }}/" >> $GITHUB_STEP_SUMMARY - echo "- Docker Hub: `apache/solr-mcp:${{ inputs.release_version }}`" >> $GITHUB_STEP_SUMMARY + echo "- Docker Hub: \`apache/solr-mcp:${{ inputs.release_version }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "#### Next Steps:" >> $GITHUB_STEP_SUMMARY echo "1. Send announcement to announce@apache.org" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 4c6c6c76..f8352746 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -191,7 +191,7 @@ jobs: run: | # Ensure the Gradle project version matches the GA version (removes any -SNAPSHOT) # This keeps image tags and any generated artifacts consistent with the voted release - sed -i "s/version = \".*\"/version = \"${RELEASE_VERSION}\"/" build.gradle.kts + sed -i "0,/^version = \".*\"/s//version = \"${RELEASE_VERSION}\"/" build.gradle.kts - name: Build project run: ./gradlew build @@ -206,34 +206,34 @@ jobs: echo "Contact INFRA for setup requirements" - name: Build and publish to Docker Hub (apache/solr-mcp) + env: + # Jib reads these directly; passing them as -Djib.to.auth.* would put + # the token in the process argument list, visible to anything on the runner. + JIB_TO_AUTH_USERNAME: ${{ secrets.DOCKERHUB_APACHE_USERNAME }} + JIB_TO_AUTH_PASSWORD: ${{ secrets.DOCKERHUB_APACHE_TOKEN }} + RELEASE_VERSION: ${{ inputs.release_version }} run: | - # Publish official release to apache/solr-mcp - # This requires Apache PMC credentials - if [[ -n "${{ secrets.DOCKERHUB_APACHE_USERNAME }}" ]]; then - # Build and push with multiple tags - ./gradlew jib \ - -Djib.to.image=apache/solr-mcp:${{ inputs.release_version }} \ - -Djib.to.auth.username=${{ secrets.DOCKERHUB_APACHE_USERNAME }} \ - -Djib.to.auth.password=${{ secrets.DOCKERHUB_APACHE_TOKEN }} \ - -Djib.to.tags=${{ inputs.release_version }},latest - - # Also tag with major and minor versions - MAJOR_VERSION=$(echo "${{ inputs.release_version }}" | cut -d. -f1) - MINOR_VERSION=$(echo "${{ inputs.release_version }}" | cut -d. -f1-2) - - ./gradlew jib \ - -Djib.to.image=apache/solr-mcp:${MAJOR_VERSION} \ - -Djib.to.auth.username=${{ secrets.DOCKERHUB_APACHE_USERNAME }} \ - -Djib.to.auth.password=${{ secrets.DOCKERHUB_APACHE_TOKEN }} - - ./gradlew jib \ - -Djib.to.image=apache/solr-mcp:${MINOR_VERSION} \ - -Djib.to.auth.username=${{ secrets.DOCKERHUB_APACHE_USERNAME }} \ - -Djib.to.auth.password=${{ secrets.DOCKERHUB_APACHE_TOKEN }} - else - echo "WARNING: Apache Docker Hub credentials not configured" + set -euo pipefail + # Publish official release to apache/solr-mcp. This requires Apache PMC + # credentials; a release that cannot publish must fail loudly rather than + # reporting success while shipping nothing. + if [[ -z "${JIB_TO_AUTH_USERNAME}" || -z "${JIB_TO_AUTH_PASSWORD}" ]]; then + echo "ERROR: Apache Docker Hub credentials (DOCKERHUB_APACHE_USERNAME / DOCKERHUB_APACHE_TOKEN) are not configured" >&2 + exit 1 fi + # Build and push with multiple tags + ./gradlew jib \ + -Djib.to.image="apache/solr-mcp:${RELEASE_VERSION}" \ + -Djib.to.tags="${RELEASE_VERSION},latest" + + # Also tag with major and minor versions + MAJOR_VERSION="${RELEASE_VERSION%%.*}" + MINOR_VERSION="$(echo "${RELEASE_VERSION}" | cut -d. -f1-2)" + + ./gradlew jib -Djib.to.image="apache/solr-mcp:${MAJOR_VERSION}" + ./gradlew jib -Djib.to.image="apache/solr-mcp:${MINOR_VERSION}" + - name: Build and publish to GitHub Container Registry run: | # Also publish to GitHub Container Registry @@ -332,7 +332,6 @@ jobs: ## Vote Thread This release was approved by the Apache Solr PMC. - ${VOTE_THREAD_NOTE} EOF if [[ -n "${{ inputs.vote_thread_url }}" ]]; then @@ -400,7 +399,7 @@ jobs: env: RELEASE_VERSION: ${{ inputs.release_version }} run: | - sed -i "s/version = \".*\"/version = \"${RELEASE_VERSION}\"/" build.gradle.kts + sed -i "0,/^version = \".*\"/s//version = \"${RELEASE_VERSION}\"/" build.gradle.kts # Inline `docker login` rather than docker/login-action: that third-party # action is not on the ASF GitHub Actions allow-list, which fails the whole @@ -515,7 +514,10 @@ jobs: runs-on: ubuntu-latest # Wait for both JVM (publish-docker) and native (publish-native-manifests) # images so the MCP Registry entry references a fully-published release. - needs: [publish-docker, publish-native-manifests] + # validate-release must be listed here too: the if: below reads its output, + # and a needs. reference to a job not in needs: resolves to empty, + # which would silently make this job never run. + needs: [validate-release, publish-docker, publish-native-manifests] if: ${{ needs.validate-release.outputs.proceed == 'true' }} # Permissions required for OIDC-based auth to the MCP Registry and read access @@ -543,16 +545,40 @@ jobs: VERSION="${{ steps.mcp_version.outputs.version }}" # Update the top-level server version (e.g., 1.2.3) jq --arg v "$VERSION" '.version = $v' server.json > server.json.tmp - # Update package version to match GA release (no -SNAPSHOT suffix) - jq --arg v "$VERSION" '.packages[0].version = $v' server.json.tmp > server.json + # Update EVERY package version to match the GA release. Rewriting + # only packages[0] left the other package on -SNAPSHOT, so the + # registry entry advertised an image tag that is never published. + jq --arg v "$VERSION" '.packages[].version = $v' server.json.tmp > server.json rm server.json.tmp + # Fail if any -SNAPSHOT version survived the rewrite + if grep -q 'SNAPSHOT' server.json; then + echo "ERROR: server.json still contains a SNAPSHOT version after rewrite" >&2 + cat server.json >&2 + exit 1 + fi # Show the final server.json for auditing cat server.json - # Download the MCP Publisher CLI from its latest GitHub release + # Download the MCP Publisher CLI, pinned to an explicit release and + # verified against that release's published checksum file. Piping an + # unverified `curl -L` straight into tar would silently extract a + # GitHub error page on a 404, so download, verify, then extract. - name: Download MCP Publisher + env: + MCP_PUBLISHER_VERSION: v1.8.0 run: | - curl -L https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher-linux-amd64.tar.gz | tar xz + set -euo pipefail + base="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}" + asset="mcp-publisher_linux_amd64.tar.gz" + checksums="registry_${MCP_PUBLISHER_VERSION#v}_checksums.txt" + + curl -fsSL -o "${asset}" "${base}/${asset}" + curl -fsSL -o "${checksums}" "${base}/${checksums}" + + # Abort if the archive does not match the release's published digest + grep " ${asset}\$" "${checksums}" | sha256sum -c - + + tar xzf "${asset}" chmod +x mcp-publisher ./mcp-publisher --version diff --git a/server.json b/server.json index 5c3f5b4b..b3b9e70b 100644 --- a/server.json +++ b/server.json @@ -6,7 +6,7 @@ "url": "https://github.com/apache/solr-mcp", "source": "github" }, - "version": "1.0.0", + "version": "1.0.0-SNAPSHOT", "packages": [ { "registryType": "docker", @@ -51,4 +51,4 @@ ] } ] -} \ No newline at end of file +}