diff --git a/.github/scripts/resolve-capi-version.sh b/.github/scripts/resolve-capi-version.sh new file mode 100755 index 0000000000..5ff2dfec33 --- /dev/null +++ b/.github/scripts/resolve-capi-version.sh @@ -0,0 +1,48 @@ +#!/usr/bin/env bash +# Prints the newest capi-release version whose CC API (v2) version equals SUPPORTED_API_VERSION +# from CloudFoundryClient.java. Needs only git and curl, no GitHub API and so no token. +# +# A capi-release tag pins cloud_controller_ng as a submodule, and that repo records its v2 API +# version in config/version_v2. The release notes aren't reliable for this. The v2 version only +# grows with the tags, so a binary search over the sorted tags is enough. +set -euo pipefail + +source_file="$(dirname "$0")/../../cloudfoundry-client/src/main/java/org/cloudfoundry/client/CloudFoundryClient.java" +target="$(sed -n 's/.*String SUPPORTED_API_VERSION = "\([0-9.]*\)";.*/\1/p' "$source_file")" +[ -n "$target" ] || { echo "SUPPORTED_API_VERSION not found in $source_file" >&2; exit 1; } + +workdir="$(mktemp -d)" +trap 'rm -rf "$workdir"' EXIT +# commits and trees only, which is all it takes to read the submodule commit of a tag +git clone --quiet --bare --filter=blob:none https://github.com/cloudfoundry/capi-release.git "$workdir/capi-release" + +mapfile -t tags < <(git -C "$workdir/capi-release" tag -l | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' | sort -V) + +v2_version() { + local sha + sha="$(git -C "$workdir/capi-release" ls-tree "$1" src/cloud_controller_ng | awk '{print $3}')" + curl -fsSL --retry 3 "https://raw.githubusercontent.com/cloudfoundry/cloud_controller_ng/$sha/config/version_v2" | tr -d '[:space:]' +} + +# highest index whose v2 version is <= target +lo=0 +hi=$((${#tags[@]} - 1)) +found=-1 +while [ "$lo" -le "$hi" ]; do + mid=$(((lo + hi) / 2)) + v="$(v2_version "${tags[$mid]}")" + if [ "$(printf '%s\n%s\n' "$v" "$target" | sort -V | tail -1)" = "$target" ]; then + found=$mid + lo=$((mid + 1)) + else + hi=$((mid - 1)) + fi +done + +if [ "$found" -lt 0 ] || [ "$(v2_version "${tags[$found]}")" != "$target" ]; then + echo "No capi-release found for CC API version $target" >&2 + exit 1 +fi + +echo "CC API $target -> capi-release ${tags[$found]}" >&2 +echo "${tags[$found]}" diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml index b1cf12b2a4..3d05984149 100644 --- a/.github/workflows/ci-java.yml +++ b/.github/workflows/ci-java.yml @@ -41,3 +41,9 @@ jobs: name: Check style with Spotless run: ./mvnw spotless:check -Pintegration-test + integration-test: + needs: build + uses: ./.github/workflows/integration-test.yml + with: + java-versions: '[21]' + diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000000..d7ca2141fa --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,124 @@ +name: Integration Tests + +on: + workflow_dispatch: + inputs: + java-versions: + description: 'JSON array of Java versions to test' + type: string + required: false + default: '[8, 11, 17, 21]' + preview: + description: 'Deploy the latest cf-deployment versions instead of the default ones' + type: boolean + required: false + default: false + schedule: + # preview run: latest versions (including CAPI) on the latest JDK only + - cron: '0 3 * * 1-5' + workflow_call: + inputs: + java-versions: + description: 'JSON array of Java versions to test' + type: string + required: false + default: '[8, 11, 17, 21]' + preview: + description: 'Deploy the latest cf-deployment versions instead of the default ones' + type: boolean + required: false + default: false + +permissions: + contents: read + +# A new push supersedes the running test. The group name differs from the caller's workflow name on +# purpose: a called workflow shares github.workflow with its caller, which would cancel the caller. +concurrency: + group: integration-test-${{ github.event_name == 'schedule' && 'schedule' || github.ref }} + cancel-in-progress: true + +env: + CF_API_HOST: api.cf.127-0-0-1.nip.io + CF_UAA_HOST: uaa.cf.127-0-0-1.nip.io + +jobs: + integration-test: + # the cron trigger also fires on forks, where nobody wants a nightly KinD run + if: ${{ github.event_name != 'schedule' || github.repository == 'cloudfoundry/cf-java-client' }} + runs-on: ubuntu-latest + timeout-minutes: 120 + strategy: + fail-fast: false + matrix: + java: ${{ fromJSON(inputs.java-versions || (github.event_name == 'schedule' && '[21]' || '[8, 11, 17, 21]')) }} + name: Java ${{ matrix.java }} integration test${{ (inputs.preview || github.event_name == 'schedule') && ' (preview)' || '' }} + steps: + - uses: actions/checkout@v7 + with: + submodules: recursive + # Validation runs deploy the capi-release that matches SUPPORTED_API_VERSION. Preview runs + # (scheduled, or started by hand with `preview`) keep the latest versions. + - name: Resolve CAPI version + id: capi + if: ${{ !(inputs.preview || github.event_name == 'schedule') }} + run: | + # separate assignment, so that a failing resolver fails the step + version="$(.github/scripts/resolve-capi-version.sh)" + echo "version=$version" >> "$GITHUB_OUTPUT" + # Overrides the capi chart version of kind-deployment (merged last by Helmfile). + # kind-deployment's helmfile passes cloudController.appDomains, which the capi chart only + # accepts from 1.241.0 on (1.240.0 fails its values schema), so older matches are raised to it. + - name: Write CAPI values override + id: values + if: ${{ steps.capi.outputs.version != '' }} + env: + MIN_CAPI_VERSION: 1.241.0 + MATCHED_CAPI_VERSION: ${{ steps.capi.outputs.version }} + run: | + version="$(printf '%s\n%s\n' "$MIN_CAPI_VERSION" "$MATCHED_CAPI_VERSION" | sort -V | tail -1)" + if [ "$version" != "$MATCHED_CAPI_VERSION" ]; then + echo "::warning::capi-release $MATCHED_CAPI_VERSION matches SUPPORTED_API_VERSION but is too old for kind-deployment, deploying $version" + fi + cat > "$RUNNER_TEMP/capi-values.yaml" <> "$GITHUB_OUTPUT" + # Every job gets its own Cloud Foundry on KinD, so the matrix can run in parallel. + # The action deploys the kind-deployment ref given in `ref`, so keep both on the same commit. + # TEMPORARY: the fork contains cloudfoundry/kind-deployment#516 (postgres-max-connections). + - name: Set up Cloud Foundry + uses: ZPascal/kind-deployment/.github/actions/setup-cf@4d1dc903ddd9438bc46f0830f96295c0f4544648 + with: + ref: 0ed311315c9852fef89690cf0174a760dbb1e5b9 + use-latest-versions: ${{ (inputs.preview || github.event_name == 'schedule') }} + github-token: ${{ github.token }} + # all components share one Postgres, which runs out of its default 100 connections + postgres-max-connections: '500' + additional-values-files: ${{ steps.values.outputs.file }} + - name: Set up Java + uses: actions/setup-java@v5 + with: + distribution: liberica + java-version: ${{ matrix.java }} + - name: Cache Maven packages + uses: actions/cache@v6 + with: + path: ~/.m2 + key: ${{ runner.os }}-m2-${{ hashFiles('**/pom.xml') }} + restore-keys: ${{ runner.os }}-m2 + - name: Run integration tests + # CC_ADMIN_PASSWORD and UAA_ADMIN_SECRET are exported by setup-cf + env: + TEST_APIHOST: ${{ env.CF_API_HOST }} + TEST_ADMIN_USERNAME: ccadmin + TEST_ADMIN_PASSWORD: ${{ env.CC_ADMIN_PASSWORD }} + TEST_ADMIN_CLIENTID: admin + TEST_ADMIN_CLIENTSECRET: ${{ env.UAA_ADMIN_SECRET }} + TEST_SKIPSSLVALIDATION: 'true' + TEST_QUOTAS_ROUTES_RESERVEDPORTS: '50' + # kind-deployment has no metric registrar, which the log-cache metric tests need + TEST_SKIP_METRIC_REGISTRAR: 'true' + run: ./mvnw -B -Pintegration-test test -Dgpg.skip