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 +}