Nightly Release #436
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Nightly Release | |
| on: | |
| schedule: | |
| - cron: "0 0 * * *" # midnight GMT | |
| workflow_dispatch: | |
| permissions: | |
| contents: read # checkout repository | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false | |
| env: | |
| CARGO_TERM_COLOR: always | |
| jobs: | |
| # 0) Move/force the "nightly" tag to main and delete the prior GH release. | |
| prepare-nightly-release: | |
| name: Prepare nightly tag + delete prior release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # push nightly tag and delete prior release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set nightly tag to latest main | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| git fetch origin main | |
| MAIN_SHA="$(git rev-parse origin/main)" | |
| # Move the nightly tag via the API (git push is unavailable with persist-credentials: false) | |
| gh api "repos/${GITHUB_REPOSITORY}/git/refs/tags/nightly" \ | |
| --method PATCH \ | |
| --field sha="${MAIN_SHA}" \ | |
| --field force=true 2>/dev/null \ | |
| || gh api "repos/${GITHUB_REPOSITORY}/git/refs" \ | |
| --method POST \ | |
| --field ref="refs/tags/nightly" \ | |
| --field sha="${MAIN_SHA}" | |
| - name: Delete existing nightly release (if any) | |
| shell: bash | |
| run: gh release delete nightly --yes || true | |
| # 1) Build & push per-arch nightly images, sign each by digest, export digests for manifest job. | |
| build-oci-images: | |
| name: Build OCI nightly image (${{ matrix.arch }}) | |
| environment: staging | |
| needs: prepare-nightly-release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-24.04 | |
| arch: amd64 | |
| platform: linux/amd64 | |
| - os: ubuntu-24.04-arm | |
| arch: arm64 | |
| platform: linux/arm64 | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push container images to GHCR | |
| id-token: write # keyless cosign signing | |
| outputs: | |
| digest_amd64: ${{ steps.meta.outputs.digest_amd64 }} | |
| digest_arm64: ${{ steps.meta.outputs.digest_arm64 }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build & push (by platform) with BuildKit cache | |
| id: build | |
| uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 | |
| with: | |
| context: . | |
| push: true | |
| platforms: ${{ matrix.platform }} | |
| tags: | | |
| ghcr.io/${{ github.repository_owner }}/hyper-mcp:nightly-${{ matrix.arch }} | |
| cache-from: type=gha,scope=hyper-mcp-nightly-${{ matrix.arch }} | |
| cache-to: type=gha,mode=max,scope=hyper-mcp-nightly-${{ matrix.arch }} | |
| - name: Resolve and sign arch image by digest | |
| id: meta | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp" | |
| DIGEST="${STEPS_BUILD_OUTPUTS_DIGEST}" | |
| REF="${IMAGE}@${DIGEST}" | |
| echo "Signing arch nightly image by digest: ${REF}" | |
| cosign sign --yes "${REF}" | |
| if [[ "${ARCH}" == "amd64" ]]; then | |
| echo "digest_amd64=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| elif [[ "${ARCH}" == "arm64" ]]; then | |
| echo "digest_arm64=${DIGEST}" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Unknown arch: ${ARCH}" >&2 | |
| exit 1 | |
| fi | |
| echo "Built ${IMAGE}:nightly-${ARCH} -> ${DIGEST}" | |
| env: | |
| STEPS_BUILD_OUTPUTS_DIGEST: ${{ steps.build.outputs.digest }} | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| ARCH: ${{ matrix.arch }} | |
| # 2) Create multi-arch nightly manifest (tag=nightly), sign manifest by digest, export digest for release notes. | |
| create-multiarch-manifests: | |
| name: Create & sign multi-arch nightly manifest | |
| needs: build-oci-images | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| packages: write # push manifest to GHCR | |
| id-token: write # keyless cosign signing | |
| outputs: | |
| nightly_digest: ${{ steps.digest.outputs.nightly_digest }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Set up Docker Buildx | |
| uses: docker/setup-buildx-action@4d04d5d9486b7bd6fa91e7baf45bbb4f8b9deedd # v4.0.0 | |
| - name: Install cosign | |
| uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@4907a6ddec9925e35a0a9e82d7399ccc52663121 # v4.1.0 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Create, push, and sign nightly manifest by digest | |
| id: digest | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp" | |
| AMD64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64}" | |
| ARM64_DIGEST="${NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64}" | |
| if [[ -z "${AMD64_DIGEST}" || -z "${ARM64_DIGEST}" ]]; then | |
| echo "Missing per-arch digests from build job." >&2 | |
| exit 1 | |
| fi | |
| echo "Creating multi-arch manifest: ${IMAGE}:nightly" | |
| docker buildx imagetools create \ | |
| -t "${IMAGE}:nightly" \ | |
| "${IMAGE}@${AMD64_DIGEST}" \ | |
| "${IMAGE}@${ARM64_DIGEST}" | |
| NIGHTLY_DIGEST="$( | |
| docker buildx imagetools inspect "${IMAGE}:nightly" --format '{{json .Manifest}}' \ | |
| | python3 -c 'import json,sys; print(json.load(sys.stdin)["digest"])' | |
| )" | |
| if [[ ! "$NIGHTLY_DIGEST" =~ ^sha256:[0-9a-f]{64}$ ]]; then | |
| echo "ERROR: Invalid digest: '$NIGHTLY_DIGEST'" >&2 | |
| exit 1 | |
| fi | |
| echo "nightly_digest=${NIGHTLY_DIGEST}" >> "$GITHUB_OUTPUT" | |
| echo "Resolved ${IMAGE}:nightly -> ${NIGHTLY_DIGEST}" | |
| echo "Signing nightly multi-arch manifest by digest: ${IMAGE}@${NIGHTLY_DIGEST}" | |
| cosign sign --yes "${IMAGE}@${NIGHTLY_DIGEST}" | |
| env: | |
| NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_AMD64: ${{ needs.build-oci-images.outputs.digest_amd64 }} | |
| NEEDS_BUILD_OCI_IMAGES_OUTPUTS_DIGEST_ARM64: ${{ needs.build-oci-images.outputs.digest_arm64 }} | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| # 3) Build binaries (matrix), upload as artifacts. | |
| build-nightly-binaries: | |
| name: Build nightly binaries (${{ matrix.target }}) | |
| environment: staging | |
| needs: prepare-nightly-release | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-24.04 | |
| target: x86_64-unknown-linux-gnu | |
| ext: "" | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| ext: "" | |
| - os: macos-latest | |
| target: aarch64-apple-darwin | |
| ext: "" | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| ext: ".exe" | |
| runs-on: ${{ matrix.os }} | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Install Rust toolchain | |
| run: rustup show | |
| - name: Install cargo-auditable | |
| run: cargo install cargo-auditable | |
| - name: Install compilation target | |
| shell: bash | |
| run: rustup target add "${TARGET}" | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Build | |
| shell: bash | |
| run: cargo auditable build --target "${TARGET}" --release --locked | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| - name: Package | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist | |
| bin="hyper-mcp${EXT}" | |
| src="target/${TARGET}/release/${bin}" | |
| if [[ "${RUNNER_OS}" == "Windows" ]]; then | |
| # Use PowerShell's Compress-Archive? We'll keep it simple with tar if available; | |
| # but Windows runners have bsdtar via Git. Use zip for compatibility. | |
| 7z a "dist/hyper-mcp-${TARGET}.zip" "${src}" | |
| pkg="dist/hyper-mcp-${TARGET}.zip" | |
| else | |
| tar -czf "dist/hyper-mcp-${TARGET}.tar.gz" -C "target/${TARGET}/release" "${bin}" | |
| pkg="dist/hyper-mcp-${TARGET}.tar.gz" | |
| fi | |
| # checksums | |
| if command -v sha256sum >/dev/null 2>&1; then | |
| sha256sum "${pkg}" > "dist/checksums-${TARGET}.txt" | |
| elif command -v shasum >/dev/null 2>&1; then | |
| shasum -a 256 "${pkg}" > "dist/checksums-${TARGET}.txt" | |
| elif [[ "$RUNNER_OS" == "Windows" ]]; then | |
| powershell.exe -NoProfile -Command \ | |
| "\$h=(Get-FileHash -Algorithm SHA256 '${pkg}').Hash.ToLower(); \"\$h ${pkg}\" | Out-File -Encoding ascii 'dist/checksums-${TARGET}.txt'" | |
| else | |
| echo "No SHA256 tool found" >&2 | |
| exit 1 | |
| fi | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| EXT: ${{ matrix.ext }} | |
| RUNNER_OS: ${{ runner.os }} | |
| - name: Upload artifacts | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: nightly-${{ matrix.target }} | |
| path: | | |
| dist/hyper-mcp-${{ matrix.target }}.tar.gz | |
| dist/hyper-mcp-${{ matrix.target }}.zip | |
| dist/checksums-${{ matrix.target }}.txt | |
| if-no-files-found: error | |
| sbom: | |
| name: SBOM | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read # checkout repository | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| submodules: true | |
| persist-credentials: false | |
| - name: Install cargo-cyclonedx | |
| run: cargo install cargo-cyclonedx --locked | |
| - name: Generate CycloneDX SBOM | |
| run: cargo cyclonedx -f json --all-features --override-filename temp | |
| - name: Clean SBOM | |
| run: jq ' | |
| del(.metadata.component."bom-ref") | |
| | del(.metadata.component.purl) | |
| | del(.metadata.component.components[0]."bom-ref") | |
| | del(.metadata.component.components[0].purl) | |
| ' temp.json > sbom.cdx.json | |
| - name: Upload sbom artifact | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: sbom-cdx-json | |
| path: sbom.cdx.json | |
| if-no-files-found: error | |
| # 4) Create the GitHub nightly release once, attach all artifacts, and include the immutable nightly digest. | |
| publish-nightly-release: | |
| name: Publish nightly GitHub Release | |
| needs: | |
| - build-nightly-binaries | |
| - create-multiarch-manifests | |
| - sbom | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write # create GitHub release and upload assets | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - name: Download build artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| pattern: nightly-* | |
| path: dist | |
| - name: Flatten artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| mkdir -p out | |
| find dist -type f -maxdepth 3 -print -exec cp {} out/ \; | |
| - name: Download sbom artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: sbom-cdx-json | |
| path: . | |
| - name: Write release notes (with immutable digest) | |
| id: notes | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| IMAGE="ghcr.io/${REPOSITORY_OWNER}/hyper-mcp" | |
| NIGHTLY_DIGEST="${NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_NIGHTLY_DIGEST}" | |
| SHA="${GITHUB_SHA}" | |
| cat > release-body.md <<EOF | |
| Nightly build from \`main\`. | |
| Commit: | |
| - \`${SHA}\` | |
| Container image (tag): | |
| - \`${IMAGE}:nightly\` | |
| ✅ Multi-arch digest (immutable, recommended for pinning): | |
| - \`${IMAGE}@${NIGHTLY_DIGEST}\` | |
| All container images are signed with Cosign. Verify the **immutable digest** with: | |
| \`\`\`bash | |
| cosign verify \ | |
| --certificate-identity "https://github.com/hyper-mcp-rs/hyper-mcp/.github/workflows/nightly.yml@refs/heads/main" \ | |
| --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ | |
| ${IMAGE}@${NIGHTLY_DIGEST} | |
| \`\`\` | |
| EOF | |
| env: | |
| NEEDS_CREATE_MULTIARCH_MANIFESTS_OUTPUTS_NIGHTLY_DIGEST: ${{ needs.create-multiarch-manifests.outputs.nightly_digest }} | |
| REPOSITORY_OWNER: ${{ github.repository_owner }} | |
| - name: Create nightly release | |
| shell: bash | |
| run: | | |
| gh release create nightly \ | |
| --title "Nightly build" \ | |
| --prerelease \ | |
| --generate-notes \ | |
| --notes-file release-body.md \ | |
| out/* \ | |
| sbom.cdx.json |