About this example
pr-test-data.yml: copy to .github/workflows/, and ../request.yaml to .datanivra/request.yaml. Status: static-validated (parsed, actions pinned by SHA, every CLI command parsed by the real CLI parser, actionlint); not run on GitHub by this repository.
Prerequisites: self-hosted runner labelled datanivra-ci that can reach the profile's targets; repository variables DATANIVRA_BASE_URL, DATANIVRA_TENANT, DATANIVRA_WORKLOAD_IDENTITY_ID, DATANIVRA_AUDIENCE, DATANIVRA_WHEELS_URL; secret APP_DB_DSN (the runner's own target access).
Permissions: contents: read at the top; id-token: write only on the two jobs that sign in. The trust should allow refs/pull/* (PR runs) and never pull_request_target, which DataNivra refuses anyway: fork code must not run with your credentials.
One-time (administrators):
datanivra workload-identities create --name "claims CI" --provider GITHUB_ACTIONS \
--repository acme/claims-service --audience datanivra:acme --ref 'refs/pull/*' --ref refs/heads/mainthen name the printed trust id in the profile's automation.workload_identity_ids (new profile version, approved by a second person).
Every run: OIDC sign-in → test-data request --ci-from-env → test-data wait → tests → test-data release (if: always(), after a fresh sign-in because workload sessions last ≤ 15 min). On pull_request: closed a clean-up job releases any request of that PR still live.
Cancellation: a cancelled run still executes the always() step; if the runner itself dies, the lease TTL ends the request.
Never logged: the OIDC token and the DataNivra session (stored in $RUNNER_TEMP, owner-only).
Files
Exactly as they are in the examples repository. Every value is a synthetic placeholder; nothing here is a credential.
pr-test-data.yml
# Example GitHub Actions workflow (ADR-0018): each pull request requests an isolated copy of an
# ALREADY CERTIFIED dataset through an approved test-data profile, runs the application tests
# against it and releases it — authenticated by the job's own OIDC token (no DataNivra secret is
# stored in GitHub). Copy to .github/workflows/ in YOUR repository.
#
# OIDC sign-in -> request (decision) -> wait READY -> tests -> release (always) ; PR closed -> release
#
# Requires (one-time, by your DataNivra administrators; see ../README.md):
# * a workload identity trust for this repository and audience (datanivra workload-identities create);
# * an approved profile `claims-pr-regression` v1 whose automation envelope names that trust;
# * the DataNivra operator has enabled workload identity (owner-gated, OFF by default). Without
# automatic approval the request waits for a human (exit 25 when --timeout elapses);
# * a runner that can reach the approved target databases (self-hosted, in your network);
# * repository variables: DATANIVRA_BASE_URL, DATANIVRA_TENANT, DATANIVRA_WORKLOAD_IDENTITY_ID,
# DATANIVRA_AUDIENCE, DATANIVRA_WHEELS_URL (CLI/SDK wheels from your DataNivra release assets:
# the packages are not published to PyPI); secret APP_DB_DSN (the runner's own access to the
# approved target — DataNivra never returns credentials).
#
# Values reach scripts through `env:` and quoted shell variables, never `${{ }}` inside `run:`.
# Third-party actions are pinned by full commit SHA. Nothing printed here contains rows or tokens.
name: pr-tests-with-test-data-api
on:
pull_request:
types: [opened, synchronize, reopened, closed]
permissions:
contents: read
concurrency:
group: datanivra-test-data-${{ github.workflow }}-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
app-tests:
if: github.event.action != 'closed'
runs-on: [self-hosted, datanivra-ci]
timeout-minutes: 60
permissions:
contents: read
id-token: write # this job alone may mint an OIDC token (audience = DATANIVRA_AUDIENCE)
env:
DATANIVRA_BASE_URL: ${{ vars.DATANIVRA_BASE_URL }}
DATANIVRA_TENANT: ${{ vars.DATANIVRA_TENANT }}
DATANIVRA_WORKLOAD_IDENTITY_ID: ${{ vars.DATANIVRA_WORKLOAD_IDENTITY_ID }}
DATANIVRA_AUDIENCE: ${{ vars.DATANIVRA_AUDIENCE }}
DATANIVRA_WHEELS_URL: ${{ vars.DATANIVRA_WHEELS_URL }}
APP_DB_DSN: ${{ secrets.APP_DB_DSN }}
steps:
- uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
- name: Install the DataNivra CLI (release assets; not on PyPI)
run: |
uv tool install --find-links "$DATANIVRA_WHEELS_URL" datanivra-cli
echo "DATANIVRA_CONFIG_DIR=$RUNNER_TEMP/datanivra" >> "$GITHUB_ENV"
- name: Sign in with this job's OIDC token
run: datanivra login --workload-identity "$DATANIVRA_WORKLOAD_IDENTITY_ID" --tenant "$DATANIVRA_TENANT" --audience "$DATANIVRA_AUDIENCE" --make-default
- name: Request test data (exit 24 = denied by the profile)
id: request
run: |
datanivra test-data request .datanivra/request.yaml --ci-from-env -o json > "$RUNNER_TEMP/request.json"
echo "id=$(python3 -c 'import json,sys; print(json.load(open(sys.argv[1]))["id"])' "$RUNNER_TEMP/request.json")" >> "$GITHUB_OUTPUT"
- name: Wait until READY (25 = still waiting for a human, 26 = ended, 27 = timeout)
env:
REQUEST_ID: ${{ steps.request.outputs.id }}
run: datanivra test-data wait "$REQUEST_ID" --timeout 1200 --interval 10
- name: Run the application tests against the approved target
run: ./scripts/run-integration-tests.sh
- name: Release (always — success, failure or cancellation)
if: always() && steps.request.outputs.id != ''
env:
REQUEST_ID: ${{ steps.request.outputs.id }}
run: |
# The workload session lasts at most 15 minutes: sign in again with a fresh OIDC token.
datanivra login --workload-identity "$DATANIVRA_WORKLOAD_IDENTITY_ID" --tenant "$DATANIVRA_TENANT" --audience "$DATANIVRA_AUDIENCE" --make-default
datanivra test-data release "$REQUEST_ID" --reason-code TEST_RUN_FINISHED
pr-closed-cleanup:
if: github.event.action == 'closed'
runs-on: [self-hosted, datanivra-ci]
timeout-minutes: 10
permissions:
contents: read
id-token: write
env:
DATANIVRA_BASE_URL: ${{ vars.DATANIVRA_BASE_URL }}
DATANIVRA_TENANT: ${{ vars.DATANIVRA_TENANT }}
DATANIVRA_WORKLOAD_IDENTITY_ID: ${{ vars.DATANIVRA_WORKLOAD_IDENTITY_ID }}
DATANIVRA_AUDIENCE: ${{ vars.DATANIVRA_AUDIENCE }}
DATANIVRA_WHEELS_URL: ${{ vars.DATANIVRA_WHEELS_URL }}
PR_NUMBER: ${{ github.event.pull_request.number }}
steps:
- uses: astral-sh/setup-uv@d0cc045d04ccac9d8b7881df0226f9e82c39688e # v6
- name: Release every live request of this pull request (idempotent)
run: |
uv tool install --find-links "$DATANIVRA_WHEELS_URL" datanivra-cli
export DATANIVRA_CONFIG_DIR="$RUNNER_TEMP/datanivra"
datanivra login --workload-identity "$DATANIVRA_WORKLOAD_IDENTITY_ID" --tenant "$DATANIVRA_TENANT" --audience "$DATANIVRA_AUDIENCE" --make-default
datanivra test-data list --all -o json > "$RUNNER_TEMP/requests.json"
python3 - "$RUNNER_TEMP/requests.json" "$PR_NUMBER" > "$RUNNER_TEMP/live.txt" <<'PY'
import json, sys
live = {"PENDING_APPROVAL", "PROVISIONING", "READY"}
for item in json.load(open(sys.argv[1]))["items"]:
ref = item.get("ci_ref") or {}
if ref.get("pr_number") == int(sys.argv[2]) and item["state"] in live:
print(item["id"])
PY
while read -r id; do
datanivra test-data release "$id" --reason-code PR_CLOSED
done < "$RUNNER_TEMP/live.txt"