Skip to content

PR Emulator.wtf

PR Emulator.wtf #893

name: PR Emulator.wtf
on: # yamllint disable-line rule:truthy
workflow_run:
workflows: ['Pull Request']
types: [completed]
concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }}
cancel-in-progress: true
permissions: {}
jobs:
emulator_wtf:
name: "Instrumentation Test app"
runs-on: ubuntu-latest
environment: ui-test
if: >
github.event.workflow_run.event == 'pull_request'
&& github.event.workflow_run.conclusion == 'success'
permissions:
id-token: write # Needed for OIDC authentication
actions: read # Needed for retrieving artifacts
checks: write # Needed to publish this job's status as a PR check
steps:
# Surface this job's own success/failure as a check on the PR commit.
# workflow_run jobs are detached from the PR, so the status is not shown
# otherwise. A check run created via the API for a PR-head SHA gets grouped
# under an arbitrary existing check suite on that commit (here CodeQL's, so
# it renders as "CodeQL / Instrumentation Test app (dynamic)"); the suite
# cannot be chosen via the API. We accept that prefix to stay consistent
# with the publish-unit-test-result-action check below, which has the same
# limitation. Every input here comes from the trusted workflow_run context
# (no fork-controlled data is consumed).
- name: Create PR check (in progress)
id: create_check
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# On PR-context check_suites GitHub absorbs API-created check_runs into
# the triggering "Pull Request" workflow's suite and silently rewrites
# details_url to the legacy `runs/{check_run_id}` form, which lands on
# an empty synthetic-job page in the parent run. We can't prevent that,
# but a Markdown link in output.summary renders on whichever page the
# Details button ends up showing, so the real emulator.wtf job logs
# are always one click away.
run: |
# Identify this job by its runner, not by display name. GitHub does
# not expose the current job's database id directly (no GITHUB_JOB_ID,
# no /actions/jobs/current endpoint — confirmed against the REST API
# docs), and $GITHUB_JOB is the YAML key, not the id we need for the
# URL. The jobs API returns a `runner_name` field on every job, and
# the runner exports its name as $RUNNER_NAME, so matching the two
# uniquely identifies this job regardless of how many siblings exist.
# `env.RUNNER_NAME` is jq's safe way to read the env var (no shell
# interpolation of the value into the filter string).
JOB_ID=$(gh api "repos/$REPO/actions/runs/$GITHUB_RUN_ID/jobs?per_page=100" \
--jq '[.jobs[] | select(.runner_name == env.RUNNER_NAME) | .id][0]' || true)
# Defence-in-depth: only use JOB_ID in a URL if it looks like an id.
# gh returns integers here, but validating keeps the URL safe even if
# GitHub's response format ever changes, and guards against the
# selector ever matching zero or multiple jobs.
if [[ "$JOB_ID" =~ ^[0-9]+$ ]]; then
JOB_URL="$RUN_URL/job/$JOB_ID"
else
JOB_URL="$RUN_URL"
fi
SUMMARY="Full logs: [open this emulator.wtf job →]($JOB_URL)"
check_id=$(jq -n \
--arg sha "$HEAD_SHA" \
--arg url "$JOB_URL" \
--arg summary "$SUMMARY" \
'{
name: "Instrumentation Test app",
head_sha: $sha,
status: "in_progress",
details_url: $url,
output: {title: "Running on emulator.wtf", summary: $summary}
}' \
| gh api --method POST "repos/$REPO/check-runs" --input - --jq '.id')
{
echo "check_id=$check_id"
echo "job_url=$JOB_URL"
} >> "$GITHUB_OUTPUT"
- name: Download emulator.wtf inputs
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: emulator-wtf-inputs
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: emulator-wtf-inputs
- name: Load device list
id: devices
# The artifact comes from a PR build that may originate from a fork; validate
# every line strictly before piping the contents into GITHUB_OUTPUT to prevent
# heredoc-terminator injection.
run: |
if [ ! -s emulator-wtf-inputs/devices.txt ]; then
echo "::error::devices.txt is empty or missing"
exit 1
fi
while IFS= read -r line; do
if [[ ! "$line" =~ ^model=Pixel7,version=(2[3-9]|3[0-9]|40)$ ]]; then
echo "::error::Invalid line in devices.txt"
exit 1
fi
done < emulator-wtf-inputs/devices.txt
{
echo 'list<<DEVICES_EOF'
cat emulator-wtf-inputs/devices.txt
echo 'DEVICES_EOF'
} >> "$GITHUB_OUTPUT"
- uses: emulator-wtf/actions/configure-credentials@72f86422814fad7432a286a1b29699cc9e73d561 # v1.1.0
with:
oidc-configuration-id: ${{ vars.EMULATOR_WTF_OIDC_CONFIGURATION_ID }}
- name: Run tests full
uses: emulator-wtf/actions/run-tests@72f86422814fad7432a286a1b29699cc9e73d561 # v1.1.0
with:
devices: ${{ steps.devices.outputs.list }}
app: emulator-wtf-inputs/app/build/outputs/apk/full/debug/app-full-debug.apk
test: emulator-wtf-inputs/app/build/outputs/apk/androidTest/full/debug/app-full-debug-androidTest.apk
outputs-dir: build/test-results/full
- name: Run tests minimal
uses: emulator-wtf/actions/run-tests@72f86422814fad7432a286a1b29699cc9e73d561 # v1.1.0
with:
devices: ${{ steps.devices.outputs.list }}
app: emulator-wtf-inputs/app/build/outputs/apk/minimal/debug/app-minimal-debug.apk
test: emulator-wtf-inputs/app/build/outputs/apk/androidTest/minimal/debug/app-minimal-debug-androidTest.apk
outputs-dir: build/test-results/minimal
- name: Upload test results
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Instrumentation Test app results
path: |
**/build/test-results/**/TEST-*.xml
**/build/test-results/**/results.xml
- name: Upload test reports
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: Instrumentation Test app reports
path: build/test-results/**
# job.status reflects everything above (check creation, downloads, test runs) and is
# one of success/failure/cancelled, all valid check-run conclusions.
# We re-send the same output.summary link so the completed check page
# keeps the deep-link to the emulator.wtf job logs (see the Create step
# for why this matters on PR-context check_suites).
- name: Finalize PR check
if: always() && steps.create_check.outputs.check_id != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
REPO: ${{ github.repository }}
CHECK_ID: ${{ steps.create_check.outputs.check_id }}
CONCLUSION: ${{ job.status }}
JOB_URL: ${{ steps.create_check.outputs.job_url }}
run: |
SUMMARY="Full logs: [open this emulator.wtf job →]($JOB_URL)"
jq -n \
--arg conclusion "$CONCLUSION" \
--arg summary "$SUMMARY" \
'{
status: "completed",
conclusion: $conclusion,
output: {title: "Completed on emulator.wtf", summary: $summary}
}' \
| gh api --method PATCH "repos/$REPO/check-runs/$CHECK_ID" --input -
publish_test_results:
name: "Publish Tests Results"
needs: [emulator_wtf]
if: always()
runs-on: ubuntu-latest
permissions:
checks: write
pull-requests: write
actions: read
contents: read
steps:
- name: Download artifacts from pr.yml run
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: pr-artifacts
# If emulator_wtf was skipped (non-PR trigger or triggering workflow did not succeed) or
# cancelled/failed before running tests, it uploads no results artifact, and
# downloading it by name errors. Tolerate that so we still publish the
# pr.yml results below. We cannot gate on needs.emulator_wtf.result because
# a real test failure also fails the job yet does produce this artifact,
# which we must still publish.
- name: Download emulator.wtf results from this run
continue-on-error: true
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: Instrumentation Test app results
path: pr-artifacts/Instrumentation Test app results
# Build a minimal event payload from the trusted workflow_run context.
# Supplying any event_file flips the is_fork check to false in the
# publish-unit-test-result-action source
# (publish_test_results.py: `is_fork = event_file is None and ...`), so it
# publishes the check run instead of disabling it for fork PRs. We build
# the payload here from trusted context rather than forwarding the real
# pull_request payload, which on a fork PR could be forged.
# The PR for the failures comment is resolved from the commit SHA via the
# API (the action's publisher.py get_pulls_from_commit), so no PR number
# is needed here.
#
# This payload is intentionally minimal: it was verified sufficient against
# publish-unit-test-result-action (the pinned SHA below). Every
# other event field the action reads degrades gracefully when absent (it
# only drops the cosmetic "vs base"/"vs earlier" deltas). If you bump the
# action version, re-check which event fields it reads and update this.
- name: Build minimal event payload
env:
HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }}
run: |
jq -n --arg head "$HEAD_REPO" \
'{pull_request: {head: {repo: {full_name: $head}}}}' > event.json
- name: Publish Test Results
uses: EnricoMi/publish-unit-test-result-action@d0a4676d0e0b938bc201470d88276b7c74c712b3 # v2.24.0
with:
commit: ${{ github.event.workflow_run.head_sha }}
event_name: ${{ github.event.workflow_run.event }}
event_file: "event.json"
comment_mode: "failures"
action_fail: true
files: |
pr-artifacts/**/TEST-*.xml
pr-artifacts/**/results.xml