name: gcp-observatory-read-adapter on: workflow_dispatch: inputs: action: description: Build only, or deploy the protected staging service and run live receipts required: true default: build_only type: choice options: - build_only - deploy_staging permissions: contents: read id-token: write concurrency: group: gcp-observatory-read-adapter-${{ github.ref }} cancel-in-progress: false env: PROJECT_ID: teleo-501523 REGION: europe-west6 ZONE: europe-west6-a REPOSITORY: teleo IMAGE_NAME: observatory-read-adapter SERVICE_NAME: observatory-read-adapter-staging RUNTIME_SERVICE_ACCOUNT: sa-observatory-read-adapter@teleo-501523.iam.gserviceaccount.com DB_IAM_USER: sa-observatory-read-adapter@teleo-501523.iam API_KEY_SECRET: observatory-read-api-key-staging WORKLOAD_IDENTITY_PROVIDER: projects/785938879453/locations/global/workloadIdentityPools/github-actions/providers/living-ip-github DEPLOY_SERVICE_ACCOUNT: sa-artifact-builder@teleo-501523.iam.gserviceaccount.com jobs: build: name: Test, build, and push adapter image runs-on: ubuntu-latest timeout-minutes: 20 outputs: image_ref: ${{ steps.image.outputs.image_ref }} steps: - uses: actions/checkout@v4 - uses: actions/setup-python@v5 with: python-version: "3.11" cache: pip - name: Run focused adapter tests run: | python -m pip install -e '.[dev]' python -m pytest tests/test_observatory_read_adapter.py -q python -m ruff check observatory_read_adapter tests/test_observatory_read_adapter.py - id: auth uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} - uses: google-github-actions/setup-gcloud@v3 with: project_id: ${{ env.PROJECT_ID }} - name: Configure Artifact Registry run: gcloud auth configure-docker "${REGION}-docker.pkg.dev" --quiet - id: image name: Build, smoke, and push immutable image shell: bash run: | set -euo pipefail tag="${GITHUB_SHA::12}" image="${REGION}-docker.pkg.dev/${PROJECT_ID}/${REPOSITORY}/${IMAGE_NAME}:${tag}" digest="$(gcloud artifacts docker images describe "${image}" \ --project="${PROJECT_ID}" \ --format='value(image_summary.digest)' 2>/dev/null || true)" if [[ -n "${digest}" ]]; then docker pull "${image}" else docker build -f Dockerfile.observatory-read-adapter -t "${image}" . fi docker run --rm --env PYTHONPYCACHEPREFIX=/tmp/pycache \ "${image}" python -m compileall -q /app/observatory_read_adapter docker run --rm "${image}" python -c 'import observatory_read_adapter; print("adapter-import-ok")' if [[ -z "${digest}" ]]; then docker push "${image}" | tee docker-push.log digest="$(awk '/digest: sha256:/ {print $3}' docker-push.log | tail -1)" fi test -n "${digest}" image_ref="${image}@${digest}" printf 'image_ref=%s\n' "${image_ref}" >> "${GITHUB_OUTPUT}" printf 'revision=%s\nimage_ref=%s\n' "${GITHUB_SHA}" "${image_ref}" > observatory-adapter-image.txt - uses: actions/upload-artifact@v4 with: name: observatory-adapter-image path: observatory-adapter-image.txt if-no-files-found: error deploy-staging: name: Deploy and prove staging read path if: ${{ inputs.action == 'deploy_staging' }} needs: build runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v4 - id: auth uses: google-github-actions/auth@v3 with: workload_identity_provider: ${{ env.WORKLOAD_IDENTITY_PROVIDER }} service_account: ${{ env.DEPLOY_SERVICE_ACCOUNT }} - uses: google-github-actions/setup-gcloud@v3 with: project_id: ${{ env.PROJECT_ID }} - name: Deploy bounded GCP staging service shell: bash env: IMAGE_REF: ${{ needs.build.outputs.image_ref }} run: | set -euo pipefail gcloud run deploy "${SERVICE_NAME}" \ --project="${PROJECT_ID}" \ --region="${REGION}" \ --image="${IMAGE_REF}" \ --execution-environment=gen2 \ --service-account="${RUNTIME_SERVICE_ACCOUNT}" \ --network=teleo-staging-net \ --subnet=teleo-staging-europe-west6 \ --vpc-egress=private-ranges-only \ --ingress=all \ --allow-unauthenticated \ --port=8080 \ --cpu=1 \ --memory=512Mi \ --concurrency=8 \ --max-instances=2 \ --timeout=15 \ --set-secrets="OBSERVATORY_API_KEY=${API_KEY_SECRET}:latest" \ --set-env-vars="GCP_PROJECT_ID=${PROJECT_ID},CLOUD_SQL_INSTANCE=${PROJECT_ID}:${REGION}:teleo-pgvector-standby,DB_NAME=teleo_canonical,DB_IAM_USER=${DB_IAM_USER},DB_AUTHORIZATION_ROLE=kb_observatory_read,SERVICE_REVISION=${GITHUB_SHA}" \ --labels="livingip-tier=gcp-staging,livingip-surface=observatory-read-adapter" \ --quiet - name: Capture positive and negative live receipts shell: bash run: | set -euo pipefail service_url="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.url)')" revision="$(gcloud run services describe "${SERVICE_NAME}" --project="${PROJECT_ID}" --region="${REGION}" --format='value(status.latestReadyRevisionName)')" api_key="$(gcloud secrets versions access latest --secret="${API_KEY_SECRET}" --project="${PROJECT_ID}")" test -n "${service_url}" test -n "${revision}" test -n "${api_key}" curl --fail --silent --show-error \ --header "X-Api-Key: ${api_key}" \ "${service_url}/v1/claims/sample" > positive.json jq -e ' .schema == "livingip.observatory-canonical-claim.v1" and .read_only == true and .provenance.database == "teleo_canonical" and .provenance.authorization_role == "kb_observatory_read" and .provenance.transaction_read_only == true and .provenance.write_privileges_denied == true and (.canonical.evidence | length) > 0 and .proposal_ledger.distinct_from_canonical == true ' positive.json >/dev/null anonymous_status="$(curl --silent --output anonymous.json --write-out '%{http_code}' "${service_url}/v1/claims/sample")" write_status="$(curl --silent --output write.json --write-out '%{http_code}' \ --request POST --header "X-Api-Key: ${api_key}" --header 'Content-Type: application/json' \ --data '{"status":"applied"}' "${service_url}/v1/claims/sample")" test "${anonymous_status}" = 401 test "${write_status}" = 405 jq -n \ --arg service_url "${service_url}" \ --arg revision "${revision}" \ --arg git_sha "${GITHUB_SHA}" \ --argjson positive "$(cat positive.json)" \ --arg anonymous_status "${anonymous_status}" \ --arg write_status "${write_status}" \ '{ schema: "livingip.observatory-read-adapter-live-receipt.v1", required_tier: "T3_live_readonly", service_url: $service_url, revision: $revision, git_sha: $git_sha, positive: $positive, negative: { anonymous_http_status: ($anonymous_status | tonumber), authenticated_post_http_status: ($write_status | tonumber) }, production_repoint_executed: false }' > observatory-read-adapter-live-receipt.json unset api_key - uses: actions/upload-artifact@v4 with: name: observatory-read-adapter-live-receipt path: observatory-read-adapter-live-receipt.json if-no-files-found: error