feat(xtest): otdf-local multi-instance refactor #5254
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: X-Test | |
| on: | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| platform-ref: | |
| required: false | |
| type: string | |
| default: main | |
| description: "platform ref: branch, tag (e.g. service/v0.12.0), commit SHA, 'latest', or 'lts'" | |
| otdfctl-ref: | |
| required: false | |
| type: string | |
| default: main | |
| description: "otdfctl ref: branch, tag (e.g. v0.29.0), commit SHA, 'latest', or 'lts'" | |
| js-ref: | |
| required: false | |
| type: string | |
| default: main | |
| description: "web-sdk ref: branch, tag (e.g. sdk/0.9.0 or 0.9.0-beta.84), commit SHA, 'latest', or 'lts'" | |
| java-ref: | |
| required: false | |
| type: string | |
| default: main | |
| description: "java-sdk ref: branch, tag (e.g. v0.12.0), commit SHA, 'latest', or 'lts'" | |
| focus-sdk: | |
| required: false | |
| type: string | |
| default: all | |
| description: "SDK to focus on (go, js, java, all)" | |
| workflow_call: | |
| inputs: | |
| platform-ref: | |
| required: false | |
| type: string | |
| default: main | |
| otdfctl-ref: | |
| required: false | |
| type: string | |
| default: main | |
| js-ref: | |
| required: false | |
| type: string | |
| default: main | |
| java-ref: | |
| required: false | |
| type: string | |
| default: main | |
| focus-sdk: | |
| required: false | |
| type: string | |
| default: all | |
| schedule: | |
| - cron: "30 6 * * *" # 0630 UTC | |
| - cron: "0 5 * * 1,3" # 500 UTC (Monday, Wednesday) | |
| - cron: "0 18 * * 0" # 1800 UTC (Sunday) | |
| concurrency: | |
| group: ${{ github.workflow }}-pr-${{ github.event.pull_request.number || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| resolve-versions: | |
| timeout-minutes: 10 | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| platform-tag-to-sha: ${{ steps.version-info.outputs.platform-tag-to-sha }} | |
| platform-tag-list: ${{ steps.version-info.outputs.platform-tag-list }} | |
| heads: ${{ steps.version-info.outputs.platform-heads }} | |
| default-tags: ${{ steps.version-info.outputs.default-tags }} | |
| go: ${{ steps.version-info.outputs.go-version-info }} | |
| java: ${{ steps.version-info.outputs.java-version-info }} | |
| js: ${{ steps.version-info.outputs.js-version-info }} | |
| sdk-version-list: ${{ steps.version-info.outputs.sdk-version-list }} | |
| env: | |
| PLATFORM_REF: "${{ inputs.platform-ref }}" | |
| JS_REF: "${{ inputs.js-ref }}" | |
| OTDFCTL_REF: "${{ inputs.otdfctl-ref }}" | |
| JAVA_REF: "${{ inputs.java-ref }}" | |
| steps: | |
| - name: Validate focus-sdk input | |
| if: ${{ inputs.focus-sdk != '' }} | |
| env: | |
| FOCUS_SDK_INPUT: ${{ inputs.focus-sdk }} | |
| run: |- | |
| if [[ ! "all go java js" =~ (^|[[:space:]])${FOCUS_SDK_INPUT}($|[[:space:]]) ]]; then | |
| echo "Invalid focus-sdk input: ${FOCUS_SDK_INPUT}. Must be one of: all, go, java, js." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| fi | |
| - name: Default Versions depend on context | |
| id: default-tags | |
| run: |- | |
| if [[ $CRON_NIGHTLY == 'true' ]]; then | |
| echo "Running nightly tests" | |
| echo "DEFAULT_TAGS=main latest" >> "$GITHUB_ENV" | |
| elif [[ $CRON_MONDAY_WEDNESDAY == 'true' ]]; then | |
| echo "Running Monday/Wednesday tests" | |
| echo "DEFAULT_TAGS=main" >> "$GITHUB_ENV" | |
| elif [[ $CRON_WEEKLY == 'true' ]]; then | |
| echo "Running weekly tests" | |
| echo "DEFAULT_TAGS=main latest" >> "$GITHUB_ENV" | |
| else | |
| echo "Running PR, Workflow Dispatch, or manual trigger" | |
| echo "DEFAULT_TAGS=main latest" >> "$GITHUB_ENV" | |
| fi | |
| env: | |
| CRON_NIGHTLY: ${{ github.event.schedule == '30 6 * * *' }} | |
| CRON_MONDAY_WEDNESDAY: ${{ github.event.schedule == '0 5 * * 1,3' }} | |
| CRON_WEEKLY: ${{ github.event.schedule == '0 18 * * 0' }} | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| path: otdf-sdk | |
| persist-credentials: false | |
| repository: opentdf/tests | |
| sparse-checkout: | | |
| xtest/sdk | |
| otdf-sdk-mgr | |
| - uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b | |
| with: | |
| python-version: "3.14" | |
| - uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 | |
| - id: version-info | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| with: | |
| script: | | |
| function htmlEscape(str) { | |
| return str.replace(/&/g, "&") | |
| .replace(/</g, "<") | |
| .replace(/>/g, ">") | |
| .replace(/"/g, """) | |
| .replace(/'/g, "'"); | |
| } | |
| const { spawnSync } = require('child_process'); | |
| const path = require('path'); | |
| const sdkMgrDir = path.join(process.env.GITHUB_WORKSPACE, 'otdf-sdk', 'otdf-sdk-mgr'); | |
| const defaultTags = process.env.DEFAULT_TAGS || 'main'; | |
| core.setOutput('default-tags', defaultTags); | |
| const refs = { | |
| platform: process.env.PLATFORM_REF || defaultTags, | |
| go: process.env.OTDFCTL_REF || defaultTags, | |
| js: process.env.JS_REF || defaultTags, | |
| java: process.env.JAVA_REF || defaultTags | |
| }; | |
| const versionData = {}; | |
| for (const [sdkType, ref] of Object.entries(refs)) { | |
| try { | |
| const refArgs = ref.trim().split(/\s+/).filter(Boolean); | |
| const result = spawnSync('uv', ['run', '--project', sdkMgrDir, 'otdf-sdk-mgr', 'versions', 'resolve', sdkType, ...refArgs], { cwd: sdkMgrDir, encoding: 'utf-8' }); | |
| if (result.status !== 0) { | |
| throw new Error(result.stderr || `Process exited with code ${result.status}`); | |
| } | |
| const output = result.stdout; | |
| const ojson = JSON.parse(output); | |
| if (!!ojson.err) { | |
| throw new Error(ojson.err); | |
| } | |
| versionData[sdkType] = ojson; | |
| } catch (error) { | |
| console.error(`Error resolving version for ${sdkType}:`, error); | |
| versionData[sdkType] = [{ tag: ref, err: error.message }]; | |
| } | |
| } | |
| core.setOutput('all', JSON.stringify(versionData)); | |
| const sdkVersionList = []; | |
| for (const [sdkType, refInfo] of Object.entries(versionData)) { | |
| if (sdkType === 'platform') continue; | |
| for (const { tag, err } of refInfo) { | |
| if (!err) sdkVersionList.push(`${sdkType}@${tag}`); | |
| } | |
| } | |
| core.setOutput('sdk-version-list', JSON.stringify(sdkVersionList)); | |
| core.summary.addHeading('Versions under Test', 3); | |
| function artifactLink(sdkType, tag, release, head, source) { | |
| if (head || !release) return ''; | |
| const v = tag.replace(/^v/, ''); | |
| if (sdkType === 'js') { | |
| const url = `https://www.npmjs.com/package/@opentdf/ctl/v/${encodeURIComponent(v)}`; | |
| return `<a href="${htmlEscape(url)}">npmjs</a>`; | |
| } | |
| if (sdkType === 'java') { | |
| const url = `https://central.sonatype.com/artifact/io.opentdf.platform/sdk/${encodeURIComponent(v)}`; | |
| return `<a href="${htmlEscape(url)}">Maven Central</a>`; | |
| } | |
| if (sdkType === 'go') { | |
| const modulePath = source === 'platform' | |
| ? `github.com/opentdf/platform/otdfctl` | |
| : `github.com/opentdf/otdfctl`; | |
| const url = `https://pkg.go.dev/${modulePath}@${encodeURIComponent(tag)}`; | |
| return `<a href="${htmlEscape(url)}">pkg.go.dev</a>`; | |
| } | |
| return ''; | |
| } | |
| let errorCount = 0; | |
| const table = []; | |
| const th = (data) => ({ data, header: true }); | |
| table.push([th('Library'), th('Tag'), th('SHA'), th('Alias'), th('Artifact'), th('Error')]); | |
| for (const [sdkType, refInfo] of Object.entries(versionData)) { | |
| const tagList = []; | |
| const tagToSha = {}; | |
| const headTags = []; | |
| for (const { tag, head, sha, alias, err, release, source } of refInfo) { | |
| const goRepoName = source === 'platform' ? 'platform' : 'otdfctl'; | |
| const sdkRepoUrl = `https://github.com/opentdf/${encodeURIComponent(sdkType == 'js' ? 'web-sdk' : sdkType == 'go' ? goRepoName : sdkType == 'java' ? 'java-sdk' : sdkType)}`; | |
| const sdkLink = `<a href="${htmlEscape(sdkRepoUrl)}">${htmlEscape(sdkType)}</a>`; | |
| const commitLink = sha ? `<a href="${htmlEscape(`${sdkRepoUrl}/commit/${encodeURIComponent(sha)}`)}">${htmlEscape(sha.substring(0, 7))}</a>` : ' . '; | |
| const tagLink = (release && tag) | |
| ? `<a href="${htmlEscape(`${sdkRepoUrl}/releases/tag/${release}`)}">${htmlEscape(tag)}</a>` | |
| : tag ? htmlEscape(tag) : 'N/A'; | |
| const artifactCell = artifactLink(sdkType, tag, release, head, source); | |
| table.push([sdkLink, tagLink, commitLink, alias || 'N/A', artifactCell || 'N/A', err || 'N/A']); | |
| if (err) { | |
| errorCount += 1; | |
| continue; | |
| } | |
| tagList.push(tag); | |
| tagToSha[tag] = sha; | |
| if (head) { | |
| headTags.push(tag); | |
| } | |
| } | |
| core.setOutput(`${sdkType}-tag-list`, JSON.stringify(tagList)); | |
| core.setOutput(`${sdkType}-tag-to-sha`, JSON.stringify(tagToSha)); | |
| core.setOutput(`${sdkType}-heads`, JSON.stringify(headTags)); | |
| core.setOutput(`${sdkType}-version-info`, JSON.stringify(refInfo)); | |
| } | |
| core.summary.addTable(table); | |
| core.summary.write(); | |
| if (errorCount > 0) { | |
| throw new Error('Errors detected in version resolution. Failing the run.'); | |
| } | |
| xct: | |
| timeout-minutes: 60 | |
| runs-on: ubuntu-latest | |
| needs: resolve-versions | |
| permissions: | |
| contents: read | |
| packages: read | |
| checks: write # Needed to publish junit tests | |
| pull-requests: write # Add comments to PRs | |
| env: | |
| FOCUS_SDK: ${{ inputs.focus-sdk || 'all' }} | |
| ENCRYPT_SDK: ${{ matrix.sdk-version }} | |
| SKIP_RELEASED_PAIRS: ${{ github.event_name != 'workflow_dispatch' && !contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| platform-tag: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-list) }} | |
| sdk-version: ${{ fromJSON(needs.resolve-versions.outputs.sdk-version-list) }} | |
| steps: | |
| - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| repository: opentdf/tests | |
| path: otdftests # use different name bc other repos might have tests directories | |
| persist-credentials: false | |
| - name: load extra keys from file | |
| id: load-extra-keys | |
| run: |- | |
| echo "EXTRA_KEYS=$(jq -c <otdftests/xtest/extra-keys.json)" >> "${GITHUB_OUTPUT}" | |
| ######## SPIN UP PLATFORM BACKEND ############# | |
| - name: Check out and start up platform with deps/containers | |
| id: run-platform | |
| uses: opentdf/platform/test/start-up-with-containers@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| platform-ref: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-to-sha)[matrix.platform-tag] }} | |
| ec-tdf-enabled: true | |
| extra-keys: ${{ steps.load-extra-keys.outputs.EXTRA_KEYS }} | |
| log-type: json | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@61cb8a9741eeb8a550a1b8544337180c0fc8476b # v7.2.0 | |
| - uses: bufbuild/buf-action@8f4a1456a0ab6a1eb80ba68e53832e6fcfacc16c # v1.3.0 | |
| with: | |
| setup_only: true | |
| token: ${{ secrets.BUF_TOKEN }} | |
| version: "1.56.0" | |
| - name: Set up JDK | |
| uses: actions/setup-java@5896cecc08fd8a1fbdfaf517e29b571164b031f7 | |
| with: | |
| java-version: "11" | |
| distribution: "adopt" | |
| server-id: github | |
| - name: Set up Node 22 | |
| uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af | |
| with: | |
| node-version: "22.x" | |
| ######## CAPTURE PLATFORM-EMBEDDED OTDFCTL LOCATION ############# | |
| # If this matrix's platform checkout contains otdfctl/ (post-v0.31.0 | |
| # tag or main), the setup-cli-tool action reuses it via symlink when an | |
| # otdfctl version resolves to the same SHA. For older platform tags | |
| # without otdfctl/, both outputs stay empty and setup-cli-tool does a | |
| # fresh opentdf/platform checkout for each go head version. | |
| - name: Capture platform otdfctl location | |
| id: platform-otdfctl | |
| run: |- | |
| if [ -d "$PLATFORM_DIR/otdfctl" ] && [ -f "$PLATFORM_DIR/otdfctl/go.mod" ]; then | |
| echo "dir=$(pwd)/$PLATFORM_DIR/otdfctl" >> "$GITHUB_OUTPUT" | |
| sha=$(git -C "$PLATFORM_DIR" rev-parse HEAD) || { | |
| echo "::error::Failed to get SHA from platform checkout at $PLATFORM_DIR" | |
| exit 1 | |
| } | |
| echo "sha=$sha" >> "$GITHUB_OUTPUT" | |
| echo "Reusable platform-embedded otdfctl at $PLATFORM_DIR/otdfctl ($sha)" | |
| else | |
| echo "Platform checkout at $PLATFORM_DIR has no otdfctl/; each go head will get a fresh platform checkout." | |
| echo "dir=" >> "$GITHUB_OUTPUT" | |
| echo "sha=" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| PLATFORM_DIR: ${{ steps.run-platform.outputs.platform-working-dir }} | |
| ######### CHECKOUT JS CLI ############# | |
| - name: Configure js-sdk | |
| id: configure-js | |
| uses: ./otdftests/xtest/setup-cli-tool | |
| with: | |
| path: otdftests/xtest/sdk | |
| sdk: js | |
| version-info: "${{ needs.resolve-versions.outputs.js }}" | |
| - name: Cache npm | |
| if: fromJson(steps.configure-js.outputs.heads)[0] != null | |
| uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 | |
| with: | |
| path: ~/.npm | |
| key: npm-${{ runner.os }}-${{ hashFiles('otdftests/xtest/sdk/js/src/**/package-lock.json') }} | |
| restore-keys: | | |
| npm-${{ runner.os }}- | |
| ######## SETUP THE JS CLI ############# | |
| - name: build and setup the web-sdk cli | |
| id: build-web-sdk | |
| if: fromJson(steps.configure-js.outputs.heads)[0] != null | |
| run: | | |
| make | |
| working-directory: otdftests/xtest/sdk/js | |
| ######## CHECKOUT GO CLI ############# | |
| - name: Configure otdfctl | |
| id: configure-go | |
| uses: ./otdftests/xtest/setup-cli-tool | |
| with: | |
| path: otdftests/xtest/sdk | |
| sdk: go | |
| version-info: "${{ needs.resolve-versions.outputs.go }}" | |
| platform-otdfctl-dir: ${{ steps.platform-otdfctl.outputs.dir }} | |
| platform-otdfctl-sha: ${{ steps.platform-otdfctl.outputs.sha }} | |
| - name: Cache Go modules | |
| if: fromJson(steps.configure-go.outputs.heads)[0] != null | |
| uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 | |
| with: | |
| path: | | |
| ~/.cache/go-build | |
| ~/go/pkg/mod | |
| key: go-${{ runner.os }}-${{ hashFiles('otdftests/xtest/sdk/go/src/*/go.sum') }} | |
| restore-keys: | | |
| go-${{ runner.os }}- | |
| ######## SETUP THE GO CLI ############# | |
| # otdfctl head builds need the platform's go.work in scope so | |
| # protocol/go, sdk, and lib/* resolve to the platform checkout's local | |
| # sources (otherwise the otdfctl go.mod's pinned release versions are | |
| # used, missing any same-PR additions). The Makefile auto-detects | |
| # GOWORK by looking for a go.work file alongside each src/{tag} symlink | |
| # target — works for both the platform-embedded reuse case and the | |
| # fresh platform-src/{tag} checkout case. | |
| - name: Prepare go cli | |
| if: fromJson(steps.configure-go.outputs.heads)[0] != null | |
| run: |- | |
| make | |
| working-directory: otdftests/xtest/sdk/go | |
| ####### CHECKOUT JAVA SDK ############## | |
| - name: Configure java-sdk | |
| id: configure-java | |
| uses: ./otdftests/xtest/setup-cli-tool | |
| with: | |
| path: otdftests/xtest/sdk | |
| sdk: java | |
| version-info: "${{ needs.resolve-versions.outputs.java }}" | |
| - name: Cache Maven repository | |
| if: fromJson(steps.configure-java.outputs.heads)[0] != null | |
| uses: actions/cache@8b402f58fbc84540c8b491a91e594a4576fec3d7 # v5.0.2 | |
| with: | |
| path: ~/.m2/repository | |
| key: maven-${{ runner.os }}-${{ hashFiles('otdftests/xtest/sdk/java/src/**/pom.xml') }} | |
| restore-keys: | | |
| maven-${{ runner.os }}- | |
| - name: pre-release protocol buffers for java-sdk | |
| if: >- | |
| fromJson(steps.configure-java.outputs.heads)[0] != null | |
| && (startsWith(matrix.sdk-version, 'go@') || startsWith(matrix.sdk-version, 'java@')) | |
| && contains(fromJSON(needs.resolve-versions.outputs.heads), matrix.platform-tag) | |
| run: |- | |
| echo "Replacing .env files for java-sdk..." | |
| echo "Platform tag: $platform_tag" | |
| echo "Java version info: $java_version_info" | |
| for row in $(echo "$java_version_info" | jq -c '.[]'); do | |
| TAG=$(echo "$row" | jq -r '.tag') | |
| HEAD=$(echo "$row" | jq -r '.head') | |
| if [[ "$HEAD" == "true" ]]; then | |
| echo "Creating .env file for tag: [$TAG]; pointing to platform ref [$platform_tag]" | |
| echo "PLATFORM_BRANCH=$platform_ref" > "otdftests/xtest/sdk/java/${TAG}.env" | |
| else | |
| echo "Skipping .env file creation for release version [$TAG]" | |
| fi | |
| done | |
| env: | |
| java_version_info: ${{ needs.resolve-versions.outputs.java }} | |
| platform_ref: ${{ fromJSON(needs.resolve-versions.outputs.platform-tag-to-sha)[matrix.platform-tag] }} | |
| platform_tag: ${{ matrix.platform-tag }} | |
| ####### SETUP JAVA CLI ############## | |
| - name: Prepare java cli | |
| if: fromJson(steps.configure-java.outputs.heads)[0] != null | |
| run: | | |
| make | |
| working-directory: otdftests/xtest/sdk/java | |
| env: | |
| BUF_INPUT_HTTPS_USERNAME: opentdf-bot | |
| BUF_INPUT_HTTPS_PASSWORD: ${{ secrets.PERSONAL_ACCESS_TOKEN_OPENTDF }} | |
| ######## Configure test environment ############# | |
| - name: Lookup current platform version | |
| id: platform-version | |
| run: |- | |
| if ! go run ./service version; then | |
| # NOTE: the version command was added in 0.4.37 | |
| echo "Error: Unable to get platform version; defaulting to tag: [$PLATFORM_TAG]" | |
| echo "PLATFORM_VERSION=$PLATFORM_TAG" >> "$GITHUB_ENV" | |
| exit | |
| fi | |
| # Older version commands output version to stderr; newer versions output to stdout | |
| PLATFORM_VERSION=$(go run ./service version 2>&1) | |
| echo "PLATFORM_VERSION=$PLATFORM_VERSION" >> "$GITHUB_ENV" | |
| echo "## Platform version output: [$PLATFORM_VERSION]" | |
| working-directory: ${{ steps.run-platform.outputs.platform-working-dir }} | |
| env: | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| - name: Check key management support and prepare root key | |
| id: km-check | |
| run: |- | |
| OT_CONFIG_FILE="$(pwd)/opentdf.yaml" | |
| echo "OT_CONFIG_FILE=$OT_CONFIG_FILE" >> "$GITHUB_ENV" | |
| # Determine if the config declares the key_management field | |
| km_value=$(yq e '.services.kas.preview.key_management' "$OT_CONFIG_FILE" 2>/dev/null || echo "null") | |
| case "$km_value" in | |
| true|false) | |
| echo "KEY_MANAGEMENT_SUPPORTED=true" >> "$GITHUB_ENV" | |
| echo "supported=true" >> "$GITHUB_OUTPUT" | |
| ;; | |
| *) | |
| echo "KEY_MANAGEMENT_SUPPORTED=false" >> "$GITHUB_ENV" | |
| echo "supported=false" >> "$GITHUB_OUTPUT" | |
| ;; | |
| esac | |
| # Prepare a root key for use by additional KAS instances | |
| existing_root_key=$(yq e '.services.kas.root_key' "$OT_CONFIG_FILE" 2>/dev/null || echo "") | |
| if [ -n "$existing_root_key" ] && [ "$existing_root_key" != "null" ]; then | |
| echo "Using existing root key from config" | |
| echo "OT_ROOT_KEY=$existing_root_key" >> "$GITHUB_ENV" | |
| echo "root_key=$existing_root_key" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "Generating a new root key for additional KAS" | |
| gen_root_key=$(openssl rand -hex 32) | |
| echo "OT_ROOT_KEY=$gen_root_key" >> "$GITHUB_ENV" | |
| echo "root_key=$gen_root_key" >> "$GITHUB_OUTPUT" | |
| fi | |
| working-directory: ${{ steps.run-platform.outputs.platform-working-dir }} | |
| - name: Install test dependencies | |
| run: uv sync | |
| working-directory: otdftests/xtest | |
| - name: Validate xtest helper library (tests of the test harness and its utilities) | |
| if: ${{ !inputs }} | |
| run: |- | |
| uv run pytest --html=test-results/helper-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" test_self.py test_audit_logs.py | |
| working-directory: otdftests/xtest | |
| env: | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| - name: Validate otdf-local integration tests | |
| if: ${{ !inputs }} | |
| run: |- | |
| uv sync | |
| uv run pytest --maxfail=1 --disable-warnings -v --tb=short -m integration | |
| working-directory: otdftests/otdf-local | |
| env: | |
| OTDF_LOCAL_PLATFORM_DIR: ${{ github.workspace }}/${{ steps.run-platform.outputs.platform-working-dir }} | |
| ######## RUN THE TESTS ############# | |
| - name: Run legacy decryption tests | |
| run: |- | |
| skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") | |
| uv run pytest -n auto --dist worksteal --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-decrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" $skip_flag test_legacy.py | |
| working-directory: otdftests/xtest | |
| env: | |
| PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| - name: Run all standard xtests | |
| if: ${{ env.FOCUS_SDK == 'all' }} | |
| run: |- | |
| skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") | |
| uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v $skip_flag test_tdfs.py test_policytypes.py | |
| working-directory: otdftests/xtest | |
| env: | |
| PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" | |
| SCHEMA_FILE: "manifest.schema.json" | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| - name: Run xtests focusing on a specific SDK | |
| if: ${{ env.FOCUS_SDK != 'all' }} | |
| run: |- | |
| skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") | |
| uv run pytest -n auto --dist loadscope --html=test-results/sdk-${FOCUS_SDK}-${PLATFORM_TAG}.html --self-contained-html --sdks-encrypt "${ENCRYPT_SDK}" -ra -v --focus "$FOCUS_SDK" $skip_flag test_tdfs.py test_policytypes.py | |
| working-directory: otdftests/xtest | |
| env: | |
| PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" | |
| SCHEMA_FILE: "manifest.schema.json" | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| ######## ATTRIBUTE BASED CONFIGURATION ############# | |
| - name: Does platform support multikas? | |
| id: multikas | |
| run: |- | |
| if [[ $PLATFORM_TAG == main ]]; then | |
| echo "Main supports multikas" | |
| echo "supported=true" >> "$GITHUB_OUTPUT" | |
| elif awk -F. '{ if ($1 > 0 || ($1 == 0 && $2 > 4)) exit 0; else exit 1; }' <<< "${PLATFORM_VERSION#v}"; then | |
| echo "Selected version [$PLATFORM_VERSION] supports multikas" | |
| echo "supported=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "At tag [$PLATFORM_TAG], [$PLATFORM_VERSION] probably does not support multikas" | |
| echo "supported=false" >> "$GITHUB_OUTPUT" | |
| fi | |
| env: | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| - name: Start additional kas | |
| id: kas-alpha | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| kas-name: alpha | |
| kas-port: 8181 | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Start additional kas | |
| id: kas-beta | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| kas-name: beta | |
| kas-port: 8282 | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Start additional kas | |
| id: kas-gamma | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| kas-name: gamma | |
| kas-port: 8383 | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Start additional kas | |
| id: kas-delta | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| kas-port: 8484 | |
| kas-name: delta | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Start additional KM kas (km1) | |
| id: kas-km1 | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| key-management: ${{ steps.km-check.outputs.supported }} | |
| kas-name: km1 | |
| kas-port: 8585 | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Start additional KM kas (km2) | |
| id: kas-km2 | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| uses: opentdf/platform/test/start-additional-kas@998929e5c66d41f928b90e6af7dbaa0a14302ca6 # watch-sh-fix | |
| with: | |
| ec-tdf-enabled: true | |
| kas-name: km2 | |
| key-management: ${{ steps.km-check.outputs.supported }} | |
| kas-port: 8686 | |
| log-type: json | |
| root-key: ${{ steps.km-check.outputs.root_key }} | |
| - name: Run attribute based configuration tests | |
| if: ${{ steps.multikas.outputs.supported == 'true' }} | |
| run: |- | |
| skip_flag=$([[ "$SKIP_RELEASED_PAIRS" == "true" ]] && echo "--skip-released-pairs" || echo "") | |
| uv run pytest -ra -v --numprocesses auto --dist loadscope \ | |
| --html test-results/attributes-${FOCUS_SDK}-${PLATFORM_TAG}.html \ | |
| --self-contained-html \ | |
| --audit-log-dir test-results/audit-logs \ | |
| --sdks-encrypt "${ENCRYPT_SDK}" \ | |
| --focus "$FOCUS_SDK" \ | |
| $skip_flag \ | |
| test_abac.py test_pqc.py | |
| working-directory: otdftests/xtest | |
| env: | |
| PLATFORM_DIR: "../../${{ steps.run-platform.outputs.platform-working-dir }}" | |
| PLATFORM_TAG: ${{ matrix.platform-tag }} | |
| PLATFORM_LOG_FILE: "../../${{ steps.run-platform.outputs.platform-log-file }}" | |
| KAS_ALPHA_LOG_FILE: "../../${{ steps.kas-alpha.outputs.log-file }}" | |
| KAS_BETA_LOG_FILE: "../../${{ steps.kas-beta.outputs.log-file }}" | |
| KAS_GAMMA_LOG_FILE: "../../${{ steps.kas-gamma.outputs.log-file }}" | |
| KAS_DELTA_LOG_FILE: "../../${{ steps.kas-delta.outputs.log-file }}" | |
| KAS_KM1_LOG_FILE: "../../${{ steps.kas-km1.outputs.log-file }}" | |
| KAS_KM2_LOG_FILE: "../../${{ steps.kas-km2.outputs.log-file }}" | |
| - name: Sanitize sdk-version for artifact name | |
| id: artifact-name | |
| if: success() || failure() | |
| run: echo "sdk_version=${ENCRYPT_SDK//\//-}" >> "$GITHUB_OUTPUT" | |
| - name: Upload artifact | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| id: upload-artifact | |
| if: success() || failure() | |
| with: | |
| name: ${{ job.status == 'success' && '✅' || job.status == 'failure' && '❌' }} ${{ steps.artifact-name.outputs.sdk_version }}-${{ matrix.platform-tag }} | |
| path: otdftests/xtest/test-results/*.html | |
| - name: Upload audit logs on failure | |
| uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 | |
| if: failure() | |
| with: | |
| name: audit-logs-${{ steps.artifact-name.outputs.sdk_version }}-${{ matrix.platform-tag }} | |
| path: otdftests/xtest/test-results/audit-logs/*.log | |
| if-no-files-found: ignore | |
| publish-results: | |
| runs-on: ubuntu-latest | |
| needs: xct | |
| if: always() # Always try to publish results, even if tests fail | |
| permissions: | |
| pull-requests: write # Add comments to PRs | |
| steps: | |
| - id: comment-artifact-list | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea #v7.0.1 | |
| env: | |
| XCT_RESULT: ${{ needs.xct.result }} | |
| with: | |
| script: | | |
| const { owner, repo} = context.repo; | |
| const runId = context.runId; | |
| const artifacts = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner, | |
| repo, | |
| run_id: runId, | |
| }) | |
| const results = artifacts.data.artifacts.map(({ name, id }) => `[${name}](https://github.com/${owner}/${repo}/actions/runs/${runId}/artifacts/${id})`).join('\n'); | |
| const xctResult = process.env.XCT_RESULT; | |
| const heading = xctResult === 'success' ? 'X-Test Results' : 'X-Test Failure Report'; | |
| const body = `## ${heading}\n${results}` | |
| const issue_number = context.issue?.number ?? context.pull_request?.number; | |
| if (!issue_number) { | |
| core.summary.addRaw(body); | |
| await core.summary.write(); | |
| console.log("No issue or pull request number found in context; skipping comment."); | |
| return; | |
| } | |
| try { | |
| await github.rest.issues.createComment({ owner, repo, issue_number, body }); | |
| } catch (err) { | |
| core.summary.addRaw(body); | |
| await core.summary.write(); | |
| if (err.status === 403) { | |
| console.error("403 when attempting to comment on PR (likely fork)", err); | |
| return; | |
| } | |
| throw err; // Re-throw unexpected errors | |
| } | |
| xtest: | |
| # Capstone job: always runs, evaluates matrix job aggregate result and fails if any xct variant failed | |
| runs-on: ubuntu-latest | |
| needs: xct | |
| if: always() | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Assert all matrix jobs passed | |
| if: ${{ needs.xct.result == 'failure' || needs.xct.result == 'cancelled' || needs.xct.result == 'skipped' }} | |
| run: |- | |
| echo "xct matrix failed or was skipped (overall result: ${XCT_RESULT}). Marking xtest failed." >> "$GITHUB_STEP_SUMMARY" | |
| exit 1 | |
| env: | |
| XCT_RESULT: ${{ needs.xct.result }} | |
| - name: Success summary | |
| if: ${{ needs.xct.result == 'success' }} | |
| run: |- | |
| echo "All xtest jobs succeeded." >> "$GITHUB_STEP_SUMMARY" |