diff --git a/.cargo/config.toml b/.cargo/config.toml index 5c6eb941..39ceb4d8 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,13 +1,6 @@ [alias] xtask = "run --locked --package xtask --manifest-path xtask/Cargo.toml --" -# Cross-compilation configuration for Linux targets only -[target.x86_64-unknown-linux-gnu] -linker = "x86_64-linux-gnu-gcc" - -[target.aarch64-unknown-linux-gnu] -linker = "aarch64-linux-gnu-gcc" - # Build configuration for release [profile.release] lto = true diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 24548a76..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,137 +0,0 @@ -name: Build - -on: - push: - branches: - - main - tags: - - 'v*' - workflow_dispatch: - inputs: - tag: - description: 'Release tag (e.g., v1.0.0)' - required: true - type: string - -jobs: - build: - runs-on: ubuntu-latest - strategy: - matrix: - include: - # Linux targets - - target: x86_64-unknown-linux-gnu - os: ubuntu-latest - artifact-name: apollo-mcp-server-linux-x86_64 - - target: aarch64-unknown-linux-gnu - os: ubuntu-latest - artifact-name: apollo-mcp-server-linux-aarch64 - - # macOS targets (native builds only) - - target: x86_64-apple-darwin - os: macos-latest - artifact-name: apollo-mcp-server-macos-x86_64 - - target: aarch64-apple-darwin - os: macos-latest - artifact-name: apollo-mcp-server-macos-aarch64 - - # Windows targets - - target: x86_64-pc-windows-msvc - os: windows-latest - artifact-name: apollo-mcp-server-windows-x86_64.exe - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Install cross (for Linux cross-compilation) - if: runner.os == 'Linux' && contains(matrix.target, 'linux') - run: | - cargo install cross --git https://github.com/cross-rs/cross - - - name: Add target (for native builds) - if: runner.os != 'Linux' || !contains(matrix.target, 'linux') - run: rustup target add ${{ matrix.target }} - - - name: Build - run: | - if [[ "${{ matrix.target }}" == *"linux"* && "${{ runner.os }}" == "Linux" ]]; then - cross build --release --target ${{ matrix.target }} --package apollo-mcp-server - else - cargo build --release --target ${{ matrix.target }} --package apollo-mcp-server - fi - - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: ${{ matrix.artifact-name }} - path: target/${{ matrix.target }}/release/apollo-mcp-server* - - release: - needs: build - runs-on: ubuntu-latest - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Download all artifacts - uses: actions/download-artifact@v4 - with: - path: ./artifacts - - - name: Create release archive - run: | - mkdir -p release - for artifact in artifacts/*/; do - artifact_name=$(basename "$artifact") - if [[ "$artifact_name" == *".exe" ]]; then - # Windows executable - cp "$artifact"/*.exe "release/$artifact_name" - else - # Unix executable - cp "$artifact"/* "release/$artifact_name" - chmod +x "release/$artifact_name" - fi - done - - - name: Create checksums - run: | - cd release - sha256sum * > checksums.txt - - - name: Create release - uses: softprops/action-gh-release@v2 - with: - tag_name: ${{ github.event.inputs.tag || github.ref_name }} - name: Release ${{ github.event.inputs.tag || github.ref_name }} - files: | - release/* - draft: false - prerelease: false - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/canary-release.yml b/.github/workflows/canary-release.yml deleted file mode 100644 index f9c1c877..00000000 --- a/.github/workflows/canary-release.yml +++ /dev/null @@ -1,52 +0,0 @@ -name: Canary Release -on: - push: - # don't run on tags, run on commits - # https://github.com/orgs/community/discussions/25615 - tags-ignore: - - "**" - paths-ignore: - - '.github/**' - - '.cargo/**' - - '.direnv/**' - - '.vscode/**' - - 'docs/**' - - 'Cargo.*' - - 'crates/**/Cargo.*' - - '*.md' - branches: - - develop - workflow_dispatch: - -permissions: - contents: read - packages: write - -concurrency: - group: canary-${{ github.ref }} - cancel-in-progress: true - -jobs: - compute_canary_version: - runs-on: ubuntu-24.04 - outputs: - version: ${{ steps.canary_version.outputs.version }} - steps: - - name: Compute canary version - id: canary_version - run: | - SHORT_SHA=${GITHUB_SHA::7} - DATE=$(date -u +%Y%m%dT%H%M%SZ) - echo "version=canary-${DATE}-${SHORT_SHA}" >> "$GITHUB_OUTPUT" - - release_canary_container: - needs: compute_canary_version - permissions: - contents: read - packages: write - attestations: write - id-token: write - uses: ./.github/workflows/release-container.yml - with: - version: ${{ needs.compute_canary_version.outputs.version }} - secrets: inherit \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb42f641..947ad89e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,7 @@ -name: Nix CI +name: CI + on: push: - # don't run on tags, run on commits - # https://github.com/orgs/community/discussions/25615 tags-ignore: - "**" branches: @@ -11,135 +10,107 @@ on: pull_request: workflow_dispatch: -env: - # We want the cache to be as full as possible, so we instruct nix to keep derivations - # and other related outputs around in its cache - nix_conf: | - keep-env-derivations = true - keep-outputs = true - jobs: - # Cache the nix store so that subsequent runs are almost instantaneous - # See https://github.com/marketplace/actions/restore-and-save-nix-store#inputs - cache: - name: Cache nix store - runs-on: ubuntu-24.04 - permissions: - actions: write - contents: read + test: + name: Test + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: nixbuild/nix-quick-install-action@v30 + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 with: - primary-key: build-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - restore-prefixes-first-match: build-${{ runner.os }}- - purge: true - purge-prefixes: build-${{ runner.os }}- - purge-created: 0 - purge-primary-key: never - gc-max-store-size: 5G - - name: Save flake attributes from garbage collection - run: nix profile install .#saveFromGC + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Run tests + run: cargo test --workspace check: - name: Run checks - runs-on: ubuntu-24.04 - needs: cache - permissions: - actions: write - contents: read + name: Check and Lint + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: nixbuild/nix-quick-install-action@v30 + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 with: - primary-key: build-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - purge: true - purge-prefixes: build-${{ runner.os }}- - purge-created: 0 - purge-primary-key: never - gc-max-store-size: 5G - - name: Run checks - run: nix flake check + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Check formatting + run: cargo fmt --all -- --check + + - name: Run clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Check documentation + run: cargo doc --no-deps --workspace build: name: Build - runs-on: ubuntu-24.04 - needs: cache - permissions: - actions: write - contents: read + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: nixbuild/nix-quick-install-action@v30 - with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 - with: - primary-key: build-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - purge: true - purge-prefixes: build-${{ runner.os }}- - purge-created: 0 - purge-primary-key: never - gc-max-store-size: 5G - - name: Build - run: nix build .# + - uses: actions/checkout@v4 - test: - name: Run Tests - runs-on: ubuntu-24.04 - needs: cache - permissions: - actions: write - contents: read - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: nixbuild/nix-quick-install-action@v30 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache cargo registry + uses: actions/cache@v4 with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 with: - primary-key: build-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - purge: true - purge-prefixes: build-${{ runner.os }}- - purge-created: 0 - purge-primary-key: never - gc-max-store-size: 5G - - name: Run Tests - run: 'nix develop --command bash -c "cargo test"' - - coverage: - name: Run Coverage - runs-on: ubuntu-24.04 - permissions: - contents: read - steps: - - uses: actions/checkout@v5 + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 with: - ref: ${{ github.event.pull_request.head.sha }} - - uses: taiki-e/install-action@cargo-llvm-cov - - name: Generate code coverage - run: cargo llvm-cov --all-features --workspace --codecov --output-path codecov.json - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v5 + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build release binary + run: cargo build --release --package apollo-mcp-server + + - name: Upload binary artifact + uses: actions/upload-artifact@v4 with: - token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos - files: codecov.json - fail_ci_if_error: true + name: apollo-mcp-server-linux-x86_64 + path: target/release/apollo-mcp-server diff --git a/.github/workflows/prep-release.yml b/.github/workflows/prep-release.yml deleted file mode 100644 index 04dc979c..00000000 --- a/.github/workflows/prep-release.yml +++ /dev/null @@ -1,256 +0,0 @@ -name: Prep release - -on: - workflow_dispatch: - inputs: - version_bump: - type: choice - description: "Type of version bump" - default: patch - required: true - options: - - major - - minor - - patch - - custom - custom_version: - type: string - required: false - description: "Custom version (ignored for other bump types)" - -permissions: - contents: write - pull-requests: write - -concurrency: - group: pre-release - cancel-in-progress: false - -jobs: - validate: - runs-on: ubuntu-latest - steps: - - name: Enforce custom_version when bump=custom - run: | - if [[ "${{ inputs.version_bump }}" == "custom" ]]; then - if [[ -z "${{ inputs.custom_version }}" ]]; then - echo "::error title=Missing input::Set 'custom_version' when version_bump=custom"; exit 1 - fi - if [[ ! "${{ inputs.custom_version }}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?$ ]]; then - echo "::error title=Invalid SemVer::Use x.y.z (can use optional pre-release/build identifiers)"; exit 1 - fi - fi - prep-release: - runs-on: ubuntu-latest - - env: - GH_TOKEN: ${{ secrets.GH_PAT }} - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - - - name: Configure git author - run: | - git config --local user.name "Apollo Bot" - git config --local user.email "svc-apollo-bot-2@apollographql.com" - - - name: Retrieve current version from Cargo.toml - id: meta - run: | - set -eu - VERSION=$(cargo metadata --no-deps --format-version=1 | jq -er --arg NAME "apollo-mcp-server" '.packages[] | select(.name == $NAME) | .version') - [ -n "$VERSION" ] || { echo "::error::Could not determine version"; exit 1; } - echo "current_version=$VERSION" >> "$GITHUB_OUTPUT" - - - name: Bump the version - id: bump - shell: bash - env: - CURR: ${{ steps.meta.outputs.current_version }} - CUSTOM: ${{ inputs.custom_version }} - BUMP: ${{ inputs.version_bump }} - run: | - set -euo pipefail - - if [[ -n "${CUSTOM:-}" ]]; then - echo "new_version=$CUSTOM" >> "$GITHUB_OUTPUT" - echo "Custom Bumped: $CURR -> $CUSTOM" - else - # strip any pre-release / build metadata for arithmetic (e.g., -rc.1, +build.5) - BASE="${CURR%%[-+]*}" - - IFS=. read -r MA MI PA <<< "$BASE" - - case "$BUMP" in - major) MA=$((MA+1)); MI=0; PA=0 ;; - minor) MI=$((MI+1)); PA=0 ;; - patch) PA=$((PA+1)) ;; - *) echo "::error::Unknown bump '$BUMP'"; exit 1 ;; - esac - - NEW_VERSION="$MA.$MI.$PA" - echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" - echo "Bumped: $CURR -> $NEW_VERSION" - fi - - - name: Prepare release branch - id: prep_branch - run: | - set -e - git fetch origin develop - git switch -c "release/${{ steps.bump.outputs.new_version }}" "origin/develop" - echo "release_branch=release/${{ steps.bump.outputs.new_version }}" >> "$GITHUB_OUTPUT" - - - name: Update Cargo version - run: | - cargo install cargo-edit --locked - cargo set-version --workspace "${{ steps.bump.outputs.new_version }}" - - - name: Replace versions in scripts and docs - env: - CURR_VERSION: ${{ steps.meta.outputs.current_version }} - NEW_VERSION: ${{ steps.bump.outputs.new_version }} - run: | - python3 - <<'PY' - try: - import os, re, sys, glob, pathlib - - current_version = os.environ["CURR_VERSION"] - new_version = os.environ["NEW_VERSION"] - - print(f"current={current_version} new={new_version}") - - # negative lookbehind (word,., or -) + optional 'v' + the escaped current version + negative lookahead (word or .) - # e.g. current version of 1.0.1 will match 1.0.1, v1.0.1, v1.0.1-rc.1 - # e.g. current version of 1.0.1 will not match ver1.0.1, 1.0.1x, 1.0.11, 1.0.1.beta - pat = re.compile(rf'(? CHANGELOG_SECTION.md - try: - import os, re, sys, pathlib - new = os.environ["NEW"] - old = os.environ["OLD"] - - p = pathlib.Path("CHANGELOG.md") - if not p.exists(): - raise FileNotFoundError("CHANGELOG.md not found at repo root") - text = p.read_text(encoding="utf-8") - - # Find header for the new version - start = re.search(rf'(?m)^# \[{re.escape(new)}\]', text) - if not start: - print(f"::error::Could not find changelog entry for {new}", file=sys.stderr) - sys.exit(1) - - # Prefer the *specific* previous version header if present; otherwise, next '# ['; else, EOF - segment = text[start.start():] - end_old = re.search(rf'(?m)^# \[{re.escape(old)}\]', segment) - if end_old: - segment = segment[:end_old.start()] - else: - nxt = re.search(r'(?m)^# \[', segment[len('# [' + new + ']'):]) - if nxt: - # adjust to absolute end - segment = segment[: (len('# [' + new + ']') + nxt.start())] - - segment = segment.rstrip() + "\n" - print(segment) - except Exception: - import traceback - traceback.print_exc() - sys.exit(1) - PY - - { - echo 'body<> "$GITHUB_OUTPUT" - - - name: Commit and push changelog updates - shell: bash - run: | - set -euo pipefail - git add -A || true - git commit -m "chore(release): changelog for ${{ steps.bump.outputs.new_version }}" || echo "No changelog updates to commit" - git push origin HEAD - - - name: Open/Update draft PR to main - env: - HEAD: release/${{ steps.bump.outputs.new_version }} - TITLE: Releasing ${{ steps.bump.outputs.new_version }} - shell: bash - run: | - set -euo pipefail - # Try to create; if it already exists, update it - if ! gh pr create \ - --base main \ - --head "$HEAD" \ - --title "$TITLE" \ - --draft \ - --body-file CHANGELOG_SECTION.md \ - --label release - then - num=$(gh pr list --head "$HEAD" --base main --state open --json number -q '.[0].number' || true) - if [[ -n "$num" ]]; then - gh pr edit "$num" --title "$TITLE" --body-file CHANGELOG_SECTION.md --add-label release - else - echo "::error::Failed to create or find PR from $HEAD to main" - exit 1 - fi - fi \ No newline at end of file diff --git a/.github/workflows/release-bins.yml b/.github/workflows/release-bins.yml deleted file mode 100644 index c73063b1..00000000 --- a/.github/workflows/release-bins.yml +++ /dev/null @@ -1,214 +0,0 @@ -name: Build Release Binaries -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" - workflow_dispatch: - inputs: - version: - description: Version to publish - required: true - type: string - -env: - VERSION: ${{ inputs.version || github.ref_name }} - -jobs: - build: - name: Release binaries - strategy: - matrix: - include: - # Linux compiles itself - - os: ubuntu-24.04 - bundle: linux - targets: cross-aarch64-unknown-linux-gnu cross-aarch64-unknown-linux-musl cross-x86_64-unknown-linux-gnu cross-x86_64-unknown-linux-musl - - # We can compile the windows target from linux - - os: ubuntu-24.04 - bundle: windows - targets: cross-aarch64-pc-windows-gnullvm cross-x86_64-pc-windows-gnullvm - - # Apple SDK does not allow us to cross compile from non-apple-branded - # machines, so we run that bundle on a macOS runner - - os: macos-latest - bundle: darwin - targets: cross-aarch64-apple-darwin cross-x86_64-apple-darwin - runs-on: ${{ matrix.os }} - permissions: - contents: write - packages: write - attestations: write - id-token: write - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.ref }} - - uses: nixbuild/nix-quick-install-action@v30 - with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 - with: - primary-key: release-${{ matrix.bundle }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - restore-prefixes-first-match: | - release-${{ matrix.bundle }}- - build-${{ runner.os }}- - purge: true - purge-prefixes: release-${{ matrix.bundle }}- - purge-created: 0 - purge-primary-key: never - gc-max-store-size: 5G - - - name: Build binaries - run: | - mkdir release - for BUILD_TARGET in ${{ matrix.targets }}; do - TARGET=${BUILD_TARGET#"cross-"} - - echo "Scaffolding release for $TARGET..." - mkdir -p "release/$TARGET/dist" - cp README.md LICENSE "release/$TARGET/dist" - - echo "Building release for $TARGET..." - nix build .#$BUILD_TARGET - cp result/bin/* "release/$TARGET/dist/" - done - - - name: Sign Apple Binary - if: ${{ runner.os == 'macOS' }} - env: - MACOS_CERT_BUNDLE_PASSWORD: ${{ secrets.MACOS_CERT_BUNDLE_PASSWORD }} - MACOS_CERT_BUNDLE_BASE64: ${{ secrets.MACOS_CERT_BUNDLE_BASE64 }} - MACOS_KEYCHAIN_PASSWORD: ${{ secrets.MACOS_KEYCHAIN_PASSWORD }} - - APPLE_NOTARIZATION_PASSWORD: ${{ secrets.APPLE_NOTARIZATION_PASSWORD }} - APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} - APPLE_USERNAME: ${{ secrets.APPLE_USERNAME }} - - KEYCHAIN_NAME: "apollo-mcp-server-keychain" - ENTITLEMENTS_PATH: "macos-entitlements.plist" - run: | - echo "Pre-check: Valid Codesigning Identify" - security find-identity -v -p codesigning - echo "Pre-check: Codesigning Identify" - security find-identity -p codesigning - echo "Pre-check: Any Identify" - security find-identity - - echo "|||||||||||||||||||||||||||||||||||||||||||||" - - # Create a temporary keychain - EPHEMERAL_KEYCHAIN=`mktemp -d` - - echo "Creating keychain..." - security create-keychain -p "${MACOS_KEYCHAIN_PASSWORD}" $KEYCHAIN_NAME - echo "Removing relock timeout on keychain..." - security set-keychain-settings $KEYCHAIN_NAME - - echo "Decoding certificate bundle..." - echo "${MACOS_CERT_BUNDLE_BASE64}" | base64 --decode > $EPHEMERAL_KEYCHAIN/certificate.p12 - - echo "Importing codesigning certificate to build keychain..." - security import $EPHEMERAL_KEYCHAIN/certificate.p12 -k $KEYCHAIN_NAME -P "${MACOS_CERT_BUNDLE_PASSWORD}" -T /usr/bin/codesign - - echo "Adding the codesign tool to the security partition-list..." - security set-key-partition-list -S "apple-tool:,apple:,codesign:" -s -k "${MACOS_KEYCHAIN_PASSWORD}" $KEYCHAIN_NAME - - echo "Setting default keychain..." - security default-keychain -d user -s $KEYCHAIN_NAME - - echo "Unlocking keychain..." - security unlock-keychain -p "${MACOS_KEYCHAIN_PASSWORD}" $KEYCHAIN_NAME - - echo "Verifying keychain is set up correctly..." - security find-identity -v -p codesigning - - echo "|||||||||||||||||||||||||||||||||||||||||||||" - - echo "Post-check: Valid Codesigning Identify" - security find-identity -v -p codesigning - echo "Post-check: Codesigning Identify" - security find-identity -p codesigning - echo "Post-check: Any Identify" - security find-identity - - echo "|||||||||||||||||||||||||||||||||||||||||||||" - # Sign each binary - for RELEASE in release/*/; do - RELEASE=${RELEASE%/} - RELEASE=${RELEASE#"release/"} - - BINARY_PATH="release/$RELEASE/dist/apollo-mcp-server" - echo "Starting code signing for $RELEASE..." - - echo "> Signing code (step 1)..." - codesign --sign "$APPLE_TEAM_ID" --options runtime --entitlements $ENTITLEMENTS_PATH --force --timestamp "$BINARY_PATH" -v - - echo "> Signing code (step 2)..." - codesign -vvv --deep --strict "$BINARY_PATH" - - echo "> Zipping dist..." - TMP_DIST=`mktemp -d` - mkdir $TMP_DIST/dist - cp "$BINARY_PATH" "$TMP_DIST/dist/" - zip -r "$TMP_DIST/apollo-mcp-server-$VERSION.zip" "$TMP_DIST/dist" - - echo "> Beginning notarization process (might take up to 20m)..." - xcrun notarytool submit "$TMP_DIST/apollo-mcp-server-$VERSION.zip" \ - --apple-id "$APPLE_USERNAME" \ - --password "$APPLE_NOTARIZATION_PASSWORD" \ - --team-id "$APPLE_TEAM_ID" \ - --wait \ - --timeout 20m - - echo "> Cleaning up release..." - rm -rf $TMP_DIST - done - - echo "Cleaning up ephemeral keychain..." - rm -rf $EPHEMERAL_KEYCHAIN/ - - - name: Create release bundles - run: | - mkdir artifacts - for RELEASE in release/*/; do - # Remove trailing slash and leading parent - RELEASE=${RELEASE%/} - RELEASE=${RELEASE#"release/"} - RENAMED=${RELEASE/x86_64-pc-windows-gnullvm/x86_64-pc-windows-msvc} - RENAMED=${RENAMED/aarch64-pc-windows-gnullvm/aarch64-pc-windows-msvc} - - echo "Creating an artifact for $RELEASE" - tar -C release/$RELEASE -cf - dist/ | gzip -9 > artifacts/apollo-mcp-server-$VERSION-$RENAMED.tar.gz - done - - # We only need to generate the config schema for a release once, so we do it - # on the linux host since it is the cheapest. - - name: Generate config schema - if: ${{ matrix.bundle == 'linux' }} - run: | - ./release/x86_64-unknown-linux-musl/dist/config-schema > artifacts/config.schema.json - - - name: Upload release artifacts - uses: softprops/action-gh-release@v2 - with: - files: artifacts/* - prerelease: ${{ contains(env.VERSION, '-rc.') }} - make_latest: false # this runs for each combination in the matrix - don't mark as latest until all are done - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-path: "artifacts/*" - - publish: - name: Publish the release - needs: build - runs-on: ubuntu-24.04 - steps: - - name: Make latest - uses: softprops/action-gh-release@v2 - with: - prerelease: ${{ contains(env.VERSION, '-rc.') }} diff --git a/.github/workflows/release-container.yml b/.github/workflows/release-container.yml deleted file mode 100644 index 64bde965..00000000 --- a/.github/workflows/release-container.yml +++ /dev/null @@ -1,110 +0,0 @@ -name: Build Release Container -on: - push: - tags: - - "v[0-9]+.[0-9]+.[0-9]+" - - "v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+" - workflow_dispatch: - inputs: &release_inputs - version: - description: Version to publish - required: true - type: string - workflow_call: - inputs: *release_inputs - -env: - REGISTRY: ghcr.io - FQDN: ghcr.io/${{ github.repository }} - VERSION: ${{ inputs.version || github.ref_name }} - -jobs: - # Build a container for x86_64 and aarch64 linux - build: - name: Release Container - strategy: - matrix: - os: ["ubuntu-24.04", "ubuntu-24.04-arm"] - runs-on: ${{ matrix.os }} - permissions: - contents: read - packages: write - attestations: write - id-token: write - steps: - - uses: actions/checkout@v5 - with: - ref: ${{ github.ref }} - - - uses: nixbuild/nix-quick-install-action@v30 - with: - nix_conf: ${{ env.nix_conf }} - - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 - with: - primary-key: build-${{ runner.os }}-${{ hashFiles('Cargo.lock', '**/Cargo.toml', 'flake.nix', 'flake.lock', 'rust-toolchain.toml') }} - restore-prefixes-first-match: build-${{ runner.os }}- - # We don't want to affect the cache when building the container - purge: false - save: false - - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - id: build - name: Build Container - shell: bash - run: | - nix run .#streamImage | docker image load - echo "id=`docker image ls -q | head -n1`" >> $GITHUB_OUTPUT - echo "arch=`docker image ls --format '{{ .Tag }}' | head -n1`" >> $GITHUB_OUTPUT - - - id: deploy - name: Tag and push the container - env: - TAG: ${{ env.VERSION }}-${{ steps.build.outputs.arch }} - run: | - docker image tag "${{ steps.build.outputs.id }}" "$FQDN:$TAG" - docker image push "$FQDN:$TAG" - echo "digest=`docker manifest inspect $FQDN:$TAG --verbose | nix run --inputs-from .# nixpkgs#jq -- -r .Descriptor.digest`" >> $GITHUB_OUTPUT - - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ${{ env.FQDN }} - subject-digest: ${{ steps.deploy.outputs.digest }} - push-to-registry: true - - bundle: - name: Bundle into multiarch container - needs: build - runs-on: ubuntu-24.04 - steps: - - name: Log in to the Container registry - uses: docker/login-action@65b78e6e13532edd9afa3aa52ac7964289d1a9c1 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - name: Create multiarch manifest - run: | - docker manifest create $FQDN:$VERSION $FQDN:$VERSION-amd64 $FQDN:$VERSION-arm64 - docker manifest annotate $FQDN:$VERSION $FQDN:$VERSION-amd64 --arch amd64 - docker manifest annotate $FQDN:$VERSION $FQDN:$VERSION-arm64 --arch arm64 - - docker manifest create $FQDN:latest $FQDN:$VERSION-amd64 $FQDN:$VERSION-arm64 - docker manifest annotate $FQDN:latest $FQDN:$VERSION-amd64 --arch amd64 - docker manifest annotate $FQDN:latest $FQDN:$VERSION-arm64 --arch arm64 - - name: Push the multiarch manifests - shell: bash - run: | - docker manifest push $FQDN:$VERSION - - # push :latest only if version DOES NOT start with canary OR end with -rc. - if [[ ! "$VERSION" =~ (^canary|-rc\.[0-9]+$) ]]; then - docker manifest push $FQDN:latest - fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..ce0c32a1 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,119 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + inputs: + tag: + description: 'Release tag (e.g., v1.0.0)' + required: true + type: string + +jobs: + build: + name: Build (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + matrix: + include: + - os: ubuntu-latest + artifact_name: apollo-mcp-server-linux-x86_64 + binary_path: target/release/apollo-mcp-server + - os: macos-latest + artifact_name: apollo-mcp-server-macos-aarch64 + binary_path: target/release/apollo-mcp-server + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Build and Test + run: | + cargo build --release --package apollo-mcp-server + cargo test --workspace + cargo clippy --all-targets --all-features -- -D warnings + cargo fmt --all -- --check + + - name: Create release package + shell: bash + run: | + RELEASE_DIR="release" + BIN_NAME="apollo-mcp-server" + TARGET_DIR="target/release" + ARTIFACT_NAME="${{ matrix.artifact_name }}" + VERSION=${{ github.event.inputs.tag || github.ref_name }} + + mkdir -p "$RELEASE_DIR/$ARTIFACT_NAME" + cp "$TARGET_DIR/$BIN_NAME" "$RELEASE_DIR/$ARTIFACT_NAME/" + cp README.md LICENSE "$RELEASE_DIR/$ARTIFACT_NAME/" 2>/dev/null || true + + # Create checksums + cd "$RELEASE_DIR/$ARTIFACT_NAME" + if command -v sha256sum &> /dev/null; then + sha256sum "$BIN_NAME" > "${BIN_NAME}.sha256" + else + shasum -a 256 "$BIN_NAME" > "${BIN_NAME}.sha256" + fi + cd - + + # Create tar.gz archive + tar -czvf "${ARTIFACT_NAME}.tar.gz" -C "$RELEASE_DIR" "$ARTIFACT_NAME" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ matrix.artifact_name }} + path: ${{ matrix.artifact_name }}.tar.gz + + release: + name: Create Release + needs: build + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + + - name: Display structure of downloaded files + run: ls -R artifacts + + - name: Move artifacts to root + run: | + mkdir -p release-assets + find artifacts -name "*.tar.gz" -exec cp {} release-assets/ \; + + - name: Upload release assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ github.event.inputs.tag || github.ref_name }} + name: Release ${{ github.event.inputs.tag || github.ref_name }} + files: release-assets/*.tar.gz + draft: false + prerelease: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sync-develop.yml b/.github/workflows/sync-develop.yml deleted file mode 100644 index 79cbc72f..00000000 --- a/.github/workflows/sync-develop.yml +++ /dev/null @@ -1,197 +0,0 @@ -name: Sync main → develop - -on: - pull_request: - types: [closed] - branches: [main] - workflow_dispatch: - inputs: - head_branch: - description: "Branch to merge FROM (default: main)." - required: false - default: "main" - base_branch: - description: "Branch to merge INTO (default: develop)." - required: false - default: "develop" - source_pr_number: - description: "If testing, the PR number to comment on (optional)." - required: false - test_mode: - description: "Bypass PR/push guards for manual testing" - required: false - default: "true" - -permissions: - contents: write - pull-requests: write - issues: write - -concurrency: - group: sync-main-into-develop - cancel-in-progress: false - -jobs: - open-sync-pr: - if: | - github.actor != 'github-actions[bot]' && ( - ( - github.event_name == 'pull_request' && github.event.pull_request.merged == true - ) || ( - github.event_name == 'workflow_dispatch' && (inputs.test_mode == 'true') - ) - ) - runs-on: ubuntu-latest - - env: - # Use inputs for dispatch (testing), defaults for normal triggers - HEAD_BRANCH: ${{ (github.event_name == 'workflow_dispatch' && inputs.head_branch) || 'main' }} - BASE_BRANCH: ${{ (github.event_name == 'workflow_dispatch' && inputs.base_branch) || 'develop' }} - SOURCE_PR: ${{ (github.event_name == 'pull_request' && github.event.pull_request.number) || inputs.source_pr_number || '' }} - GH_TOKEN: ${{ secrets.GH_PAT }} - - steps: - - uses: actions/checkout@v5 - with: - fetch-depth: 0 - token: ${{ secrets.GH_PAT }} - - - name: Configure git author - run: | - git config --local user.name "Apollo Bot" - git config --local user.email "svc-apollo-bot-2@apollographql.com" - - # Generate branch name from PR# when available, otherwise use first 7 commit SHA characters - - name: Compute branch/name metadata - id: meta - run: | - pr=${{ github.event.pull_request.number }} - echo "sync_branch=sync/main-into-develop-pr-${pr}" >> $GITHUB_OUTPUT - echo "sync_title=Sync main → develop (PR #${pr})" >> $GITHUB_OUTPUT - echo "sync_body=Auto-opened after merging \`${{ github.event.pull_request.head.ref }}\` into \`main\`. Source PR: #${pr}." >> $GITHUB_OUTPUT - echo "conflict_branch=conflict/main-into-develop-pr-${pr}" >> $GITHUB_OUTPUT - echo "conflict_title=Sync main → develop (resolve conflicts)" >> $GITHUB_OUTPUT - echo "conflict_body=Opened from a copy of \`main\` so conflicts can be resolved without pushing to a protected branch." >> $GITHUB_OUTPUT - - # Short-lived sync branch from develop and merge main into it (do NOT rebase) - # use +e to stop errors from short-circuiting the script - - name: Prepare sync branch - id: prep - run: | - set -e - git fetch origin "${BASE_BRANCH}" "${HEAD_BRANCH}" - git switch -c "${{ steps.meta.outputs.sync_branch }}" "origin/${BASE_BRANCH}" - set +e - git merge --no-ff "origin/${HEAD_BRANCH}" - rc=$? - set -e - git add -A || true - git commit -m "WIP: merge ${HEAD_BRANCH} into ${BASE_BRANCH} via ${{ steps.meta.outputs.branch }}" || true - git push origin HEAD - - right=$(git rev-list --count --right-only "origin/${BASE_BRANCH}...HEAD") - - echo "merge_status=$rc" >> "$GITHUB_OUTPUT" - echo "sync_right=$right" >> "$GITHUB_OUTPUT" - echo "Merge exit=$rc, sync branch ahead-by=$right" - - # If no merge conflicts and there are changes, open the PR targeting develop - - name: Open clean PR to develop - id: sync_pr - if: ${{ steps.prep.outputs.merge_status == '0' && steps.prep.outputs.sync_right != '0' }} - run: | - # Avoid duplicate PRs - existing=$(gh pr list --base "${BASE_BRANCH}" --head "${{ steps.meta.outputs.sync_branch }}" --state open --json number --jq '.[0].number' || true) - if [ -n "$existing" ] && [ "$existing" != "null" ]; then - echo "pr_number=$existing" >> "$GITHUB_OUTPUT" - url=$(gh pr view "$existing" --json url --jq .url) - echo "pr_url=$url" >> "$GITHUB_OUTPUT" - exit 0 - fi - - gh pr create \ - --base "${BASE_BRANCH}" \ - --head "${{ steps.meta.outputs.sync_branch }}" \ - --title "${{ steps.meta.outputs.sync_title }}" \ - --body "${{ steps.meta.outputs.sync_body }} (created via gh CLI)" \ - --label back-merge \ - --label skip-changeset \ - --label automation - - # Fetch the newly created PR number, then its URL so that we display in a PR comment - num=$(gh pr list --base "${BASE_BRANCH}" --head "${{ steps.meta.outputs.sync_branch }}" --state open --json number --jq '.[0].number') - url=$(gh pr view "$num" --json url --jq .url) - echo "pr_number=$num" >> "$GITHUB_OUTPUT" - echo "pr_url=$url" >> "$GITHUB_OUTPUT" - - # If the merge hit conflicts, open a DIRECT PR: HEAD_BRANCH -> BASE_BRANCH so conflicts can be resolved prior to merge - - name: Open conflict PR - id: conflict_pr - if: ${{ steps.prep.outputs.merge_status != '0' }} - run: | - set -e - git fetch origin "${HEAD_BRANCH}" "${BASE_BRANCH}" - - git switch -c "${{ steps.meta.outputs.conflict_branch }}" "origin/${HEAD_BRANCH}" - git push -u origin HEAD - - # Skip if no diff between conflict branch and base (should be unlikely) - right=$(git rev-list --right-only --count "origin/${BASE_BRANCH}...origin/${{ steps.meta.outputs.conflict_branch }}") - if [ "$right" -eq 0 ]; then - echo "No diff between ${HEAD_BRANCH} and ${BASE_BRANCH}; nothing to open." - exit 0 - fi - - # Reuse existing open PR if present - existing=$(gh pr list --base "${BASE_BRANCH}" --head "${{ steps.meta.outputs.conflict_branch }}" --state open --json number --jq '.[0].number' || true) - if [ -n "$existing" ] && [ "$existing" != "null" ]; then - echo "pr_number=$existing" >> "$GITHUB_OUTPUT" - url=$(gh pr view "$existing" --json url --jq .url) - echo "pr_url=$url" >> "$GITHUB_OUTPUT" - exit 0 - fi - - gh pr create \ - --base "${BASE_BRANCH}" \ - --head "${{ steps.meta.outputs.conflict_branch }}" \ - --title "${{ steps.meta.outputs.conflict_title }}" \ - --body "${{ steps.meta.outputs.conflict_body }}" \ - --label back-merge \ - --label automation \ - --label skip-changeset \ - --label conflicts - - # Fetch the newly created conflict PR number, then its URL so that we display in a PR comment - num=$(gh pr list --base "${BASE_BRANCH}" --head "${{ steps.meta.outputs.conflict_branch }}" --state open --json number --jq '.[0].number') - url=$(gh pr view "$num" --json url --jq .url) - echo "pr_number=$num" >> "$GITHUB_OUTPUT" - echo "pr_url=$url" >> "$GITHUB_OUTPUT" - - # Comment back on the ORIGINAL merged PR with a link to the sync PR - - name: Comment on source PR with sync PR link - if: ${{ env.SOURCE_PR != '' && (steps.sync_pr.outputs.pr_number != '' || steps.conflict_pr.outputs.pr_number != '') }} - uses: actions/github-script@v7 - with: - script: | - const owner = context.repo.owner; - const repo = context.repo.repo; - const issue_number = Number(process.env.SOURCE_PR); - - const hadConflicts = '${{ steps.prep.outputs.merge_status }}' !== '0'; - const syncUrl = '${{ steps.sync_pr.outputs.pr_url || steps.conflict_pr.outputs.pr_url }}'; - const head = process.env.HEAD_BRANCH; - const base = process.env.BASE_BRANCH; - - const status = hadConflicts ? 'conflicts ❗' : 'clean ✅'; - const note = hadConflicts - ? 'Opened from a copy of main so conflicts can be resolved safely.' - : 'Opened from a sync branch created off develop.'; - - const body = [ - `Opened sync PR **${head} → ${base}**: ${syncUrl}`, - ``, - `Merge status: **${status}**`, - note - ].join('\n'); - - await github.rest.issues.createComment({ owner, repo, issue_number, body }); \ No newline at end of file diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 172c6fc0..00000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,46 +0,0 @@ -name: Test - -on: - push: - branches: [ main, develop ] - pull_request: - branches: [ main ] - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - - name: Install Rust - uses: dtolnay/rust-toolchain@stable - with: - components: rustfmt, clippy - - - name: Cache cargo registry - uses: actions/cache@v4 - with: - path: ~/.cargo/registry - key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo index - uses: actions/cache@v4 - with: - path: ~/.cargo/git - key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - - name: Cache cargo build - uses: actions/cache@v4 - with: - path: target - key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} - - - name: Run tests - run: cargo test --verbose - - - name: Run clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - - name: Check formatting - run: cargo fmt --all -- --check diff --git a/.github/workflows/verify-changeset.yml b/.github/workflows/verify-changeset.yml deleted file mode 100644 index 2bac53f0..00000000 --- a/.github/workflows/verify-changeset.yml +++ /dev/null @@ -1,66 +0,0 @@ -name: Verify Changeset -on: - pull_request: - types: [opened, reopened, synchronize, ready_for_review] - branches-ignore: - - main - - release/** - - conflict/* - - sync/* - paths-ignore: - - '.github/**' - - '.cargo/**' - - '.direnv/**' - - '.vscode/**' - - 'docs/**' - - 'Cargo.*' - - 'crates/**/Cargo.*' - - '*.md' - workflow_dispatch: - -jobs: - verify-changeset: - if: ${{ !contains(github.event.pull_request.labels.*.name, 'skip-changeset') && !startsWith(github.head_ref, 'sync/') && !startsWith(github.head_ref, 'conflict/') && !github.event.pull_request.draft }} - name: Verify - runs-on: ubuntu-24.04 - permissions: - pull-requests: write - contents: read - steps: - - name: Verify changeset included - uses: actions/github-script@v7 - with: - script: | - const dir = '.changesets/'; - const pr = context.payload.pull_request; - const files = await github.paginate( - github.rest.pulls.listFiles, - { owner: context.repo.owner, repo: context.repo.repo, pull_number: pr.number, per_page: 100 } - ); - const ok = files.some(f => - f.filename.startsWith(dir) && - ['added','modified','renamed'].includes(f.status) - ); - if (!ok) { - core.setFailed(`No changeset added to ${dir}.`); - } else { - core.info(`Changeset found under ${dir}.`); - } - core.setOutput('ok', ok ? 'true' : 'false'); - - name: Add changeset missing comment on failure - uses: actions/github-script@v7 - if: failure() - with: - script: | - const pr = context.payload.pull_request; - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: pr.number, - body: [ - "❌ **Changeset file missing for PR**", - "", - "All changes should include an associated changeset file.", - "Please refer to [README](https://github.com/apollographql/apollo-mcp-server/blob/main/.changesets/README.md) for more information on generating changesets." - ].join("\n") - }); diff --git a/crates/apollo-mcp-server/src/config_manager.rs b/crates/apollo-mcp-server/src/config_manager.rs index 362d21b2..9cf763cd 100644 --- a/crates/apollo-mcp-server/src/config_manager.rs +++ b/crates/apollo-mcp-server/src/config_manager.rs @@ -112,7 +112,6 @@ impl ConfigManager { } } - #[cfg(test)] mod tests { use super::*; @@ -124,7 +123,7 @@ mod tests { fn test_config_file_operations() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("test_config.yaml"); - + // Create initial config let initial_config = r#" endpoint: "https://api.example.com/graphql" @@ -157,7 +156,7 @@ headers: fn test_config_backup_creation() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("test_config.yaml"); - + let initial_config = r#" endpoint: "https://api.example.com/graphql" headers: @@ -166,7 +165,7 @@ headers: fs::write(&config_path, initial_config).unwrap(); let config_manager = ConfigManager::new(config_path.to_string_lossy().to_string()); - + // Count files before update let files_before: Vec<_> = fs::read_dir(temp_dir.path()).unwrap().collect(); let count_before = files_before.len(); @@ -182,12 +181,10 @@ headers: assert_eq!(count_after, count_before + 1); // Verify backup file exists - let backup_exists = fs::read_dir(temp_dir.path()) - .unwrap() - .any(|entry| { - let entry = entry.unwrap(); - entry.path().to_string_lossy().contains(".backup.") - }); + let backup_exists = fs::read_dir(temp_dir.path()).unwrap().any(|entry| { + let entry = entry.unwrap(); + entry.path().to_string_lossy().contains(".backup.") + }); assert!(backup_exists, "Backup file should exist"); } @@ -196,7 +193,7 @@ headers: fn test_config_verification() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("test_config.yaml"); - + let config_manager = ConfigManager::new(config_path.to_string_lossy().to_string()); // Test with non-existent file diff --git a/crates/apollo-mcp-server/src/startup.rs b/crates/apollo-mcp-server/src/startup.rs index 32f9e77d..70701dae 100644 --- a/crates/apollo-mcp-server/src/startup.rs +++ b/crates/apollo-mcp-server/src/startup.rs @@ -119,7 +119,6 @@ pub fn get_graphql_endpoint() -> Option { env::var("APOLLO_GRAPHQL_ENDPOINT").ok() } - #[cfg(test)] mod tests { use super::*; @@ -133,7 +132,7 @@ mod tests { async fn test_complete_initialization_flow() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("test_config.yaml"); - + // Create initial config let initial_config = r#" endpoint: "https://api.example.com/graphql" @@ -143,26 +142,32 @@ headers: fs::write(&config_path, initial_config).unwrap(); let _config_manager = ConfigManager::new(config_path.to_string_lossy().to_string()); - + // Test environment variable setup unsafe { std::env::set_var("APOLLO_REFRESH_TOKEN", "test_refresh_token"); std::env::set_var("APOLLO_REFRESH_URL", "https://api.example.com/refresh"); std::env::set_var("APOLLO_GRAPHQL_ENDPOINT", "https://api.example.com/graphql"); } - + // Test getting refresh token from environment let refresh_token = get_refresh_token(); assert_eq!(refresh_token, Some("test_refresh_token".to_string())); - + // Test getting refresh URL from environment let refresh_url = get_refresh_url(); - assert_eq!(refresh_url, Some("https://api.example.com/refresh".to_string())); - + assert_eq!( + refresh_url, + Some("https://api.example.com/refresh".to_string()) + ); + // Test getting GraphQL endpoint from environment let endpoint = get_graphql_endpoint(); - assert_eq!(endpoint, Some("https://api.example.com/graphql".to_string())); - + assert_eq!( + endpoint, + Some("https://api.example.com/graphql".to_string()) + ); + // Clean up environment variables unsafe { std::env::remove_var("APOLLO_REFRESH_TOKEN"); @@ -179,11 +184,11 @@ headers: std::env::remove_var("APOLLO_REFRESH_TOKEN"); std::env::remove_var("APOLLO_REFRESH_URL"); } - + // Test getting missing refresh token let refresh_token = get_refresh_token(); assert_eq!(refresh_token, None); - + // Test getting missing refresh URL let refresh_url = get_refresh_url(); assert_eq!(refresh_url, None); @@ -194,11 +199,11 @@ headers: async fn test_token_manager_integration() { let refresh_token = "test_refresh_token"; let refresh_url = "https://api.example.com/refresh"; - + // Test creating token manager let token_manager = TokenManager::new(refresh_token.to_string(), refresh_url.to_string()); assert!(token_manager.is_ok()); - + let token_manager = token_manager.unwrap(); assert_eq!(token_manager.refresh_token(), refresh_token); assert_eq!(token_manager.refresh_url(), refresh_url); diff --git a/crates/apollo-mcp-server/src/token_manager.rs b/crates/apollo-mcp-server/src/token_manager.rs index 39008e97..e688bf7e 100644 --- a/crates/apollo-mcp-server/src/token_manager.rs +++ b/crates/apollo-mcp-server/src/token_manager.rs @@ -37,7 +37,7 @@ impl TokenManager { None, )); } - + if refresh_url.trim().is_empty() { return Err(McpError::new( ErrorCode::INVALID_PARAMS, @@ -246,7 +246,6 @@ impl Clone for TokenManager { } } - #[cfg(test)] mod tests { use super::*; @@ -261,7 +260,7 @@ mod tests { async fn test_token_refresh_stores_in_memory() { let temp_dir = TempDir::new().unwrap(); let config_path = temp_dir.path().join("test_config.yaml"); - + // Create initial config let initial_config = r#" endpoint: "https://api.example.com/graphql" @@ -271,17 +270,18 @@ headers: fs::write(&config_path, initial_config).unwrap(); let _config_manager = ConfigManager::new(config_path.to_string_lossy().to_string()); - + // Mock refresh URL (this would normally be a real endpoint) let refresh_url = "https://api.example.com/refresh"; let refresh_token = "refresh_token_123"; - - let token_manager = TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); - + + let token_manager = + TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); + // Initially no token in memory assert!(token_manager.access_token.is_none()); assert!(token_manager.token_expires_at.is_none()); - + // Note: This test would need a mock server to actually test token refresh // For now, we test the structure and that it can be created assert_eq!(token_manager.refresh_token, refresh_token); @@ -292,9 +292,12 @@ headers: #[test] fn test_token_manager_creation_error() { // Test with empty refresh token - let result = TokenManager::new("".to_string(), "https://api.example.com/refresh".to_string()); + let result = TokenManager::new( + "".to_string(), + "https://api.example.com/refresh".to_string(), + ); assert!(result.is_err()); - + // Test with empty refresh URL let result = TokenManager::new("refresh_token".to_string(), "".to_string()); assert!(result.is_err()); @@ -305,13 +308,14 @@ headers: async fn test_token_expiry_logic() { let refresh_url = "https://api.example.com/refresh"; let refresh_token = "refresh_token_123"; - - let mut token_manager = TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); - + + let mut token_manager = + TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); + // Set a token that expires in the past token_manager.access_token = Some("test_token".to_string()); token_manager.token_expires_at = Some(Instant::now() - Duration::from_secs(3600)); - + // Token should be considered expired let now = Instant::now(); if let Some(expires_at) = token_manager.token_expires_at { @@ -324,40 +328,46 @@ headers: fn test_token_manager_clone() { let refresh_url = "https://api.example.com/refresh"; let refresh_token = "refresh_token_123"; - - let mut token_manager = TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); + + let mut token_manager = + TokenManager::new(refresh_token.to_string(), refresh_url.to_string()).unwrap(); token_manager.access_token = Some("test_token".to_string()); token_manager.token_expires_at = Some(Instant::now() + Duration::from_secs(3600)); - + let cloned_manager = token_manager.clone(); - - assert_eq!(cloned_manager.refresh_token(), token_manager.refresh_token()); + + assert_eq!( + cloned_manager.refresh_token(), + token_manager.refresh_token() + ); assert_eq!(cloned_manager.refresh_url(), token_manager.refresh_url()); assert_eq!(cloned_manager.access_token(), token_manager.access_token()); - assert_eq!(cloned_manager.token_expires_at(), token_manager.token_expires_at()); + assert_eq!( + cloned_manager.token_expires_at(), + token_manager.token_expires_at() + ); } -} + // Test helper methods for TokenManager + impl TokenManager { + /// Get the refresh token (for testing) + pub fn refresh_token(&self) -> &str { + &self.refresh_token + } -#[cfg(test)] -impl TokenManager { - /// Get the refresh token (for testing) - pub fn refresh_token(&self) -> &str { - &self.refresh_token - } - - /// Get the refresh URL (for testing) - pub fn refresh_url(&self) -> &str { - &self.refresh_url - } + /// Get the refresh URL (for testing) + pub fn refresh_url(&self) -> &str { + &self.refresh_url + } - /// Get the current access token (for testing) - pub fn access_token(&self) -> &Option { - &self.access_token - } + /// Get the current access token (for testing) + pub fn access_token(&self) -> &Option { + &self.access_token + } - /// Get the token expiry time (for testing) - pub fn token_expires_at(&self) -> &Option { - &self.token_expires_at + /// Get the token expiry time (for testing) + pub fn token_expires_at(&self) -> &Option { + &self.token_expires_at + } } } diff --git a/nix/apollo-mcp.nix b/nix/apollo-mcp.nix index 0b63cf9a..ef44d0c1 100644 --- a/nix/apollo-mcp.nix +++ b/nix/apollo-mcp.nix @@ -15,7 +15,9 @@ graphqlFilter = path: _type: builtins.match ".*graphql$" path != null; testFilter = path: _type: builtins.match ".*snap$" path != null; srcFilter = path: type: - (graphqlFilter path type) || (testFilter path type) || (craneLib.filterCargoSources path type); + (graphqlFilter path type) + || (testFilter path type) + || (craneLib.filterCargoSources path type); # Crane options src = pkgs.lib.cleanSourceWith { @@ -24,7 +26,8 @@ name = "source"; # Be reproducible, regardless of the directory name }; - craneLib = (crane.mkLib pkgs).overrideToolchain toolchain; + # Use default system toolchain to avoid compatibility issues + craneLib = crane.mkLib pkgs; craneCommonArgs = { inherit src; pname = "apollo-mcp"; @@ -33,6 +36,10 @@ nativeBuildInputs = [perl pkg-config]; buildInputs = []; + # Force native builds only to prevent cross-compilation + CARGO_BUILD_TARGET = pkgs.stdenv.hostPlatform.config; + CARGO_TARGET_DIR = "target"; + # Meta information about the packages meta = { description = "Apollo MCP Server"; @@ -46,32 +53,45 @@ # Generate a derivation for just the dependencies of the project so that they # can be cached across all of the various checks and builders. - cargoArtifacts = craneLib.buildDepsOnly craneCommonArgs; + # Use buildDepsOnly with target override to avoid cross-compilation + cargoArtifacts = craneLib.buildDepsOnly ( + craneCommonArgs + // { + # Force native target only + CARGO_BUILD_TARGET = pkgs.stdenv.hostPlatform.config; + # Override cargo check to specify target (crane already adds --release --locked) + cargoCheckExtraArgs = "--target ${pkgs.stdenv.hostPlatform.config}"; + # Override linker to use native gcc instead of cross-compilation linker + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER = "gcc"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_CC = "gcc"; + CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_CXX = "g++"; + } + ); in { # Expose the list of build dependencies for inheriting in dev shells nativeDependencies = craneCommonArgs.nativeBuildInputs; dependencies = craneCommonArgs.buildInputs; # Expose derivations that should be cached in CI - cache = [ - cargoArtifacts - ]; + cache = [cargoArtifacts]; # Expose checks for the project used by the root nix flake checks = { - clippy = craneLib.cargoClippy (craneCommonArgs + clippy = craneLib.cargoClippy ( + craneCommonArgs // { inherit cargoArtifacts; - cargoClippyExtraArgs = "--all-targets -- --deny warnings"; - }); - docs = craneLib.cargoDoc (craneCommonArgs + cargoClippyExtraArgs = "-- --deny warnings"; + } + ); + docs = craneLib.cargoDoc ( + craneCommonArgs // { inherit cargoArtifacts; - }); + } + ); - rustfmt = craneLib.cargoFmt { - inherit src; - }; + rustfmt = craneLib.cargoFmt {inherit src;}; toml-fmt = craneLib.taploFmt { src = pkgs.lib.sources.sourceFilesBySuffices src [".toml"]; }; @@ -98,16 +118,19 @@ in { # Helper for generating a command using cargo-zigbuild and other shell-expanded # env vars. mkCmd = cmd: - builtins.concatStringsSep " " ((lib.optionals stdenv.isDarwin ["SDKROOT=${apple-sdk.sdkroot}"]) + builtins.concatStringsSep " " ( + (lib.optionals stdenv.isDarwin ["SDKROOT=${apple-sdk.sdkroot}"]) ++ [ "CARGO_ZIGBUILD_CACHE_DIR=$TMP/.cache/cargo-zigbuild" "ZIG_LOCAL_CACHE_DIR=$TMP/.cache/zig-local" "ZIG_GLOBAL_CACHE_DIR=$TMP/.cache/zig-global" "${cargo-zigbuild-patched}/bin/cargo-zigbuild ${cmd}" - ]); + ] + ); in - craneLib.buildPackage (craneCommonArgs + craneLib.buildPackage ( + craneCommonArgs // { pname = craneCommonArgs.pname + "-${target}"; nativeBuildInputs = [ @@ -123,19 +146,20 @@ in { # Use zig for both CC and linker since it actually supports cross-compilation # nicely. - cargoExtraArgs = lib.strings.concatStringsSep " " ([ - "--target ${zig-target}" - ] + cargoExtraArgs = lib.strings.concatStringsSep " " ( + ["--target ${zig-target}"] # x86_64-apple-darwin compilation has a bug that causes release builds to # fail with "bad relocation", so we build debug targets for it instead. # See: https://github.com/rust-cross/cargo-zigbuild/issues/338 - ++ (lib.optionals (target != "x86_64-apple-darwin") ["--release"])); + ++ (lib.optionals (target != "x86_64-apple-darwin") ["--release"]) + ); cargoCheckCommand = mkCmd "check"; cargoBuildCommand = mkCmd "zigbuild"; # Make sure to compile it for the specified target CARGO_BUILD_TARGET = target; - }); + } + ); }; }