Hardware and Wokwi tests #2068
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: Hardware and Wokwi tests | |
| on: | |
| workflow_run: | |
| workflows: ["Runtime Tests"] | |
| types: | |
| - completed | |
| # No permissions by default | |
| permissions: | |
| contents: read | |
| jobs: | |
| get-artifacts: | |
| name: Get required artifacts | |
| if: github.event.workflow_run.conclusion != 'cancelled' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| statuses: write | |
| pull-requests: read | |
| outputs: | |
| pr_num: ${{ steps.set-ref.outputs.pr_num }} | |
| ref: ${{ steps.set-ref.outputs.ref }} | |
| base: ${{ steps.set-ref.outputs.base }} | |
| hw_types: ${{ steps.set-ref.outputs.hw_types }} | |
| hw_targets: ${{ steps.set-ref.outputs.hw_targets }} | |
| wokwi_types: ${{ steps.set-ref.outputs.wokwi_types }} | |
| wokwi_targets: ${{ steps.set-ref.outputs.wokwi_targets }} | |
| hw_tests_enabled: ${{ steps.set-ref.outputs.hw_tests_enabled }} | |
| wokwi_tests_enabled: ${{ steps.set-ref.outputs.wokwi_tests_enabled }} | |
| rerun_wokwi_test: ${{ steps.set-ref.outputs.rerun_wokwi_test }} | |
| push_time: ${{ steps.set-ref.outputs.push_time }} | |
| steps: | |
| - name: Report pending | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Wokwi (Get artifacts) (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: 'pending', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| - name: Download and extract event file | |
| uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event.workflow_run.id }} | |
| name: event_file | |
| path: artifacts/event_file | |
| - name: Download and extract matrix info | |
| uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event.workflow_run.id }} | |
| name: matrix_info | |
| path: artifacts/matrix_info | |
| - name: Get info | |
| env: | |
| GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }} | |
| WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| WR_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| WR_EVENT: ${{ github.event.workflow_run.event }} | |
| WR_CONCLUSION: ${{ github.event.workflow_run.conclusion }} | |
| WR_RUN_ID: ${{ github.event.workflow_run.id }} | |
| WR_HEAD_SHA: ${{ github.event.workflow_run.head_sha || github.sha }} | |
| id: set-ref | |
| run: | | |
| # Security: derive ref, base, and labels from trusted sources only. | |
| # - workflow_run event properties are passed as env vars (tamper-proof, injection-safe) | |
| # - PR metadata is fetched via GitHub API (tamper-proof) | |
| # - Artifact event file is NOT trusted for security-critical values | |
| # (a fork PR could upload a crafted event file in the triggering workflow) | |
| # - All workflow_run values are passed via env vars rather than inline | |
| # expressions to prevent script injection through crafted branch names | |
| # - For PR events, the script fails closed if the base branch cannot be | |
| # determined from trusted sources (never falls back to head branch) | |
| pr_num="" | |
| base="" | |
| ref="" | |
| has_hil_label="false" | |
| has_rerun_label="false" | |
| if [ "$WR_EVENT" == "pull_request" ]; then | |
| echo "Original event: pull_request — resolving PR info from GitHub API..." | |
| # Validate SHA format before using in API calls | |
| if ! echo "$WR_HEAD_SHA" | grep -qE '^[a-f0-9]{40}$'; then | |
| echo "ERROR: Invalid SHA format: $WR_HEAD_SHA" | |
| exit 1 | |
| fi | |
| # Trusted: get PR number from commit SHA via GitHub API | |
| pr_num=$(gh api "repos/$GH_REPO/commits/$WR_HEAD_SHA/pulls" \ | |
| --jq '.[0].number // empty' 2>/dev/null | tr -cd "[:digit:]") || true | |
| # Fallback: commits API only works for same-repo PRs; use search API for fork PRs | |
| if [ -z "$pr_num" ]; then | |
| echo "Commits API returned empty, scanning open PRs by head SHA for fork PRs..." | |
| pr_num=$(gh api "repos/$GH_REPO/pulls?state=open&per_page=100" \ | |
| --paginate \ | |
| --jq ".[] | select(.head.sha == \"$WR_HEAD_SHA\") | .number" \ | |
| 2>/dev/null | head -1 | tr -cd "[:digit:]") || true | |
| fi | |
| if [ -n "$pr_num" ]; then | |
| echo "Found PR #$pr_num" | |
| ref="$pr_num" | |
| # Trusted: get base branch and labels from the PR via API | |
| pr_data=$(gh api "repos/$GH_REPO/pulls/$pr_num" 2>/dev/null) || true | |
| if [ -n "$pr_data" ]; then | |
| base=$(echo "$pr_data" | jq -r '.base.ref // empty' | tr -cd "[:alnum:]/_.-") | |
| has_hil_label=$(echo "$pr_data" | jq -r '[.labels[].name] | if index("hil_test") then "true" else "false" end') | |
| has_rerun_label=$(echo "$pr_data" | jq -r '[.labels[].name] | if index("Re-trigger Wokwi Tests") then "true" else "false" end') | |
| echo "PR base: $base" | |
| fi | |
| else | |
| echo "WARNING: Could not find PR for SHA $WR_HEAD_SHA" | |
| fi | |
| # Fail closed: refuse to fall back to head branch for PR events | |
| if [ -z "$ref" ] || [ -z "$base" ]; then | |
| echo "ERROR: Could not determine PR number or base branch from GitHub API" | |
| echo " - pr_num='$pr_num' base='$base' head_sha='$WR_HEAD_SHA'" | |
| echo "Failing closed for security — will not fall back to head branch for PR events" | |
| exit 1 | |
| fi | |
| else | |
| echo "Original event: $WR_EVENT — using workflow_run.head_branch" | |
| # For non-PR events (workflow_dispatch, schedule), head_branch is the correct value | |
| if [ -z "$ref" ]; then | |
| ref=$(echo "$WR_HEAD_BRANCH" | tr -cd "[:alnum:]/_.-") | |
| fi | |
| if [ -z "$base" ]; then | |
| base=$(echo "$WR_HEAD_BRANCH" | tr -cd "[:alnum:]/_.-") | |
| fi | |
| fi | |
| # Non-security-critical values from artifacts | |
| action=$(jq -r '.action' artifacts/event_file/event.json | tr -cd "[:alpha:]_") | |
| if [ "$action" == "null" ]; then | |
| action="" | |
| fi | |
| push_time=$(jq -r '.repository.pushed_at' artifacts/event_file/event.json | tr -cd "[:alnum:]:-") | |
| if [ -z "$push_time" ] || [ "$push_time" == "null" ]; then | |
| push_time="" | |
| fi | |
| # HW tests enablement | |
| if [ -n "$GITLAB_ACCESS_TOKEN" ]; then | |
| hw_tests_enabled="true" | |
| if [[ -n "$pr_num" && "$has_hil_label" != "true" ]]; then | |
| echo "PR does not have hil_test label, hardware tests will be disabled" | |
| hw_tests_enabled="false" | |
| fi | |
| else | |
| echo "GITLAB_ACCESS_TOKEN is not set, hardware tests will be disabled" | |
| hw_tests_enabled="false" | |
| fi | |
| if [ -n "$WOKWI_CLI_TOKEN" ]; then | |
| wokwi_tests_enabled="true" | |
| else | |
| echo "WOKWI_CLI_TOKEN is not set, wokwi tests will be disabled" | |
| wokwi_tests_enabled="false" | |
| fi | |
| rerun_wokwi_test="false" | |
| if [[ -n "$pr_num" && "$has_rerun_label" == "true" ]]; then | |
| rerun_wokwi_test="true" | |
| echo "PR has Re-trigger Wokwi Tests label, cached results will be ignored" | |
| fi | |
| # Test matrix from artifacts (non-security-critical) | |
| hw_targets=$(jq -c '.hw_targets' artifacts/matrix_info/test_matrix.json | tr -cd "[:alnum:],[]\"") | |
| hw_types=$(jq -c '.hw_types' artifacts/matrix_info/test_matrix.json | tr -cd "[:alpha:],[]\"") | |
| wokwi_targets=$(jq -c '.wokwi_targets' artifacts/matrix_info/test_matrix.json | tr -cd "[:alnum:],[]\"") | |
| wokwi_types=$(jq -c '.wokwi_types' artifacts/matrix_info/test_matrix.json | tr -cd "[:alpha:],[]\"") | |
| qemu_tests_enabled=$(jq -r '.qemu_enabled' artifacts/matrix_info/test_matrix.json | tr -cd "[:alpha:]") | |
| qemu_targets=$(jq -c '.qemu_targets' artifacts/matrix_info/test_matrix.json | tr -cd "[:alnum:],[]\"") | |
| qemu_types=$(jq -c '.qemu_types' artifacts/matrix_info/test_matrix.json | tr -cd "[:alpha:],[]\"") | |
| echo "base = $base" | |
| echo "hw_targets = $hw_targets" | |
| echo "hw_types = $hw_types" | |
| echo "wokwi_targets = $wokwi_targets" | |
| echo "wokwi_types = $wokwi_types" | |
| echo "qemu_tests_enabled = $qemu_tests_enabled" | |
| echo "qemu_targets = $qemu_targets" | |
| echo "qemu_types = $qemu_types" | |
| echo "pr_num = $pr_num" | |
| echo "hw_tests_enabled = $hw_tests_enabled" | |
| echo "wokwi_tests_enabled = $wokwi_tests_enabled" | |
| echo "rerun_wokwi_test = $rerun_wokwi_test" | |
| echo "push_time = $push_time" | |
| echo "Has hil_test label: $has_hil_label" | |
| echo "Has Re-trigger Wokwi Tests label: $has_rerun_label" | |
| cat > artifacts/workflow_info.json <<EOF | |
| { | |
| "hw_tests_enabled": $hw_tests_enabled, | |
| "hw_targets": $hw_targets, | |
| "hw_types": $hw_types, | |
| "wokwi_tests_enabled": $wokwi_tests_enabled, | |
| "wokwi_targets": $wokwi_targets, | |
| "wokwi_types": $wokwi_types, | |
| "qemu_tests_enabled": $qemu_tests_enabled, | |
| "qemu_targets": $qemu_targets, | |
| "qemu_types": $qemu_types, | |
| "ref": "$ref", | |
| "event": "$WR_EVENT", | |
| "sha": "$WR_HEAD_SHA", | |
| "action": "$action", | |
| "run_id": "$WR_RUN_ID", | |
| "conclusion": "$WR_CONCLUSION" | |
| } | |
| EOF | |
| echo "Ref = $ref" | |
| echo "Event name = $WR_EVENT" | |
| echo "Head SHA = $WR_HEAD_SHA" | |
| echo "Action = $action" | |
| echo "Run ID = $WR_RUN_ID" | |
| echo "Conclusion = $WR_CONCLUSION" | |
| if [ -z "$ref" ] || [ "$ref" == "null" ]; then | |
| echo "Failed to get PR number or ref" | |
| exit 1 | |
| fi | |
| echo "pr_num=$pr_num" >> $GITHUB_OUTPUT | |
| echo "base=$base" >> $GITHUB_OUTPUT | |
| echo "hw_targets=$hw_targets" >> $GITHUB_OUTPUT | |
| echo "hw_types=$hw_types" >> $GITHUB_OUTPUT | |
| echo "wokwi_targets=$wokwi_targets" >> $GITHUB_OUTPUT | |
| echo "wokwi_types=$wokwi_types" >> $GITHUB_OUTPUT | |
| echo "ref=$ref" >> $GITHUB_OUTPUT | |
| echo "hw_tests_enabled=$hw_tests_enabled" >> $GITHUB_OUTPUT | |
| echo "wokwi_tests_enabled=$wokwi_tests_enabled" >> $GITHUB_OUTPUT | |
| echo "rerun_wokwi_test=$rerun_wokwi_test" >> $GITHUB_OUTPUT | |
| echo "push_time=$push_time" >> $GITHUB_OUTPUT | |
| - name: Download and extract parent QEMU results | |
| uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 | |
| continue-on-error: true | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event.workflow_run.id }} | |
| pattern: test-results-qemu-* | |
| merge-multiple: true | |
| path: artifacts/results/qemu | |
| - name: Upload parent artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| with: | |
| name: parent-artifacts | |
| path: artifacts | |
| if-no-files-found: error | |
| - name: Report conclusion | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| if: always() | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Wokwi (Get artifacts) (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: '${{ job.status }}', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| hardware-test: | |
| name: Internal Hardware Tests | |
| if: | | |
| (github.event.workflow_run.conclusion == 'success' || | |
| github.event.workflow_run.conclusion == 'failure' || | |
| github.event.workflow_run.conclusion == 'timed_out') && | |
| needs.get-artifacts.outputs.hw_tests_enabled == 'true' | |
| runs-on: ubuntu-latest | |
| needs: get-artifacts | |
| env: | |
| id: ${{ needs.get-artifacts.outputs.ref }}-${{ github.event.workflow_run.head_sha || github.sha }} | |
| permissions: | |
| actions: read | |
| statuses: write | |
| steps: | |
| - name: Report pending | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Internal Hardware Tests (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: 'pending', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| - name: Check if already passed | |
| id: get-cache-results | |
| if: needs.get-artifacts.outputs.pr_num | |
| uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 | |
| with: | |
| key: test-${{ env.id }}-results-hw | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Evaluate if tests should be run | |
| id: check-tests | |
| env: | |
| CACHE_HIT: ${{ steps.get-cache-results.outputs.cache-hit }} | |
| run: | | |
| enabled=true | |
| # Check cache first | |
| if [[ "$CACHE_HIT" == 'true' ]]; then | |
| echo "Already ran, skipping GitLab pipeline trigger" | |
| enabled=false | |
| else | |
| echo "Cache miss, hardware tests will run" | |
| fi | |
| echo "enabled=$enabled" >> $GITHUB_OUTPUT | |
| - name: Wait for GitLab sync and prepare variables | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| id: prepare-variables | |
| env: | |
| PUSH_TIME: ${{ needs.get-artifacts.outputs.push_time }} | |
| HW_TYPES: ${{ needs.get-artifacts.outputs.hw_types }} | |
| HW_TARGETS: ${{ needs.get-artifacts.outputs.hw_targets }} | |
| run: | | |
| # A webhook to sync the repository is sent to GitLab when a commit is pushed to GitHub | |
| # We wait for 10 minutes after the push to GitHub to be safe | |
| echo "Ensuring GitLab sync has completed before triggering pipeline..." | |
| # Use push time determined in get-artifacts job | |
| push_time="$PUSH_TIME" | |
| if [ -n "$push_time" ]; then | |
| echo "Push time: $push_time" | |
| # Convert push time to epoch | |
| push_epoch=$(date -d "$push_time" +%s 2>/dev/null || echo "") | |
| if [ -n "$push_epoch" ]; then | |
| current_epoch=$(date +%s) | |
| elapsed_minutes=$(( (current_epoch - push_epoch) / 60 )) | |
| echo "Elapsed time since push: ${elapsed_minutes} minutes" | |
| if [ $elapsed_minutes -lt 10 ]; then | |
| wait_time=$(( (10 - elapsed_minutes) * 60 )) | |
| echo "Waiting ${wait_time} seconds for GitLab sync to complete..." | |
| sleep $wait_time | |
| else | |
| echo "GitLab sync should be complete (${elapsed_minutes} minutes elapsed)" | |
| fi | |
| else | |
| echo "Could not parse push timestamp, waiting 60 seconds as fallback..." | |
| sleep 60 | |
| fi | |
| else | |
| echo "Could not determine push time, waiting 60 seconds as fallback..." | |
| sleep 60 | |
| fi | |
| echo "Proceeding with GitLab pipeline trigger..." | |
| # Make targets/types comma-separated strings (remove brackets and quotes) | |
| test_types=$(printf '%s' "$HW_TYPES" | sed -e 's/[][]//g' -e 's/"//g') | |
| test_chips=$(printf '%s' "$HW_TARGETS" | sed -e 's/[][]//g' -e 's/"//g') | |
| echo "test_types=$test_types" | |
| echo "test_chips=$test_chips" | |
| # Expose as step outputs | |
| echo "test_types=$test_types" >> $GITHUB_OUTPUT | |
| echo "test_chips=$test_chips" >> $GITHUB_OUTPUT | |
| - name: Trigger GitLab Pipeline and Download Artifacts | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| uses: digital-blueprint/gitlab-pipeline-trigger-action@20e77989b24af658ba138a0aa5291bdc657f1505 # v1.3.0 | |
| id: gitlab-trigger | |
| with: | |
| host: ${{ secrets.GITLAB_URL }} | |
| id: ${{ secrets.GITLAB_PROJECT_ID }} | |
| ref: ${{ needs.get-artifacts.outputs.base || github.ref }} | |
| trigger_token: ${{ secrets.GITLAB_TRIGGER_TOKEN }} | |
| access_token: ${{ secrets.GITLAB_ACCESS_TOKEN }} | |
| download_artifacts: 'true' | |
| download_artifacts_on_failure: 'true' | |
| download_path: './gitlab-artifacts' | |
| variables: >- | |
| { | |
| "TEST_TYPES":"${{ steps.prepare-variables.outputs.test_types }}", | |
| "TEST_CHIPS":"${{ steps.prepare-variables.outputs.test_chips }}", | |
| "PIPELINE_ID":"${{ env.id }}", | |
| "BINARIES_RUN_ID":"${{ github.event.workflow_run.id }}", | |
| "GITHUB_REPOSITORY":"${{ github.repository }}", | |
| "GITHUB_REF":"${{ needs.get-artifacts.outputs.base || github.ref }}" | |
| } | |
| - name: Process Downloaded Artifacts | |
| if: ${{ always() && steps.check-tests.outputs.enabled == 'true' }} | |
| env: | |
| GL_STATUS: ${{ steps.gitlab-trigger.outputs.status }} | |
| GL_ARTIFACTS_DOWNLOADED: ${{ steps.gitlab-trigger.outputs.artifacts_downloaded }} | |
| run: | | |
| echo "GitLab Pipeline Status: $GL_STATUS" | |
| echo "Artifacts Downloaded: $GL_ARTIFACTS_DOWNLOADED" | |
| # Create tests directory structure expected by GitHub caching | |
| mkdir -p tests | |
| # Process downloaded GitLab artifacts | |
| if [ "$GL_ARTIFACTS_DOWNLOADED" = "true" ]; then | |
| echo "Processing downloaded GitLab artifacts..." | |
| # Find and copy test result files while preserving directory structure | |
| # The GitLab artifacts have the structure: gitlab-artifacts/job_*/artifacts/tests/... | |
| # We want to preserve the tests/... part of the structure | |
| for job_dir in ./gitlab-artifacts/job_*; do | |
| if [ -d "$job_dir/artifacts/tests" ]; then | |
| # Merge results into tests/ without failing on non-empty directories | |
| echo "Merging $job_dir/artifacts/tests/ into tests/" | |
| cp -a "$job_dir/artifacts/tests/." tests/ | |
| fi | |
| done | |
| echo "Test results found:" | |
| ls -laR tests/ || echo "No test results found" | |
| else | |
| echo "No artifacts were downloaded from GitLab" | |
| fi | |
| - name: Upload hardware results as cache | |
| uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 | |
| if: steps.check-tests.outputs.enabled == 'true' && needs.get-artifacts.outputs.pr_num | |
| with: | |
| key: test-${{ env.id }}-results-hw | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Upload hardware results as artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| if: always() | |
| with: | |
| name: test-results-hw | |
| overwrite: true | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Report conclusion | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| if: always() | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Internal Hardware Tests (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: '${{ job.status }}', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| wokwi-test: | |
| name: Wokwi ${{ matrix.chip }} ${{ matrix.type }} tests | |
| if: | | |
| (github.event.workflow_run.conclusion == 'success' || | |
| github.event.workflow_run.conclusion == 'failure' || | |
| github.event.workflow_run.conclusion == 'timed_out') && | |
| needs.get-artifacts.outputs.wokwi_tests_enabled == 'true' | |
| runs-on: ubuntu-latest | |
| needs: get-artifacts | |
| env: | |
| id: ${{ needs.get-artifacts.outputs.ref }}-${{ github.event.workflow_run.head_sha || github.sha }}-${{ matrix.chip }}-${{ matrix.type }} | |
| permissions: | |
| actions: read | |
| statuses: write | |
| pull-requests: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| type: ${{ fromJson(needs.get-artifacts.outputs.wokwi_types) }} | |
| chip: ${{ fromJson(needs.get-artifacts.outputs.wokwi_targets) }} | |
| steps: | |
| - name: Report pending | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Wokwi (${{ matrix.type }}, ${{ matrix.chip }}) / Wokwi ${{ matrix.chip }} ${{ matrix.type }} tests (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: 'pending', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| - name: Check if already passed | |
| id: get-cache-results | |
| if: needs.get-artifacts.outputs.pr_num && needs.get-artifacts.outputs.rerun_wokwi_test == 'false' | |
| uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 | |
| with: | |
| key: test-${{ env.id }}-results-wokwi | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Evaluate if tests should be run | |
| id: check-tests | |
| env: | |
| CACHE_HIT: ${{ steps.get-cache-results.outputs.cache-hit }} | |
| RERUN_WOKWI: ${{ needs.get-artifacts.outputs.rerun_wokwi_test }} | |
| run: | | |
| if [[ "$RERUN_WOKWI" == "true" ]]; then | |
| echo "Re-trigger Wokwi Tests label is present, running tests (cache restore was skipped)" | |
| enabled=true | |
| elif [[ "$CACHE_HIT" == "true" ]]; then | |
| echo "Already ran, skipping" | |
| enabled=false | |
| else | |
| echo "No cache found, tests will run" | |
| enabled=true | |
| fi | |
| echo "enabled=$enabled" >> $GITHUB_OUTPUT | |
| # Security: for PRs, `base` is the PR's base branch (from API), so only trusted code is checked out. | |
| # For workflow_dispatch/schedule, `base` is the selected/default branch (from workflow_run event). | |
| # DO NOT use artifact-derived values for the checkout ref — they can be forged by a malicious PR. | |
| - name: Checkout repository | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 | |
| with: | |
| ref: ${{ needs.get-artifacts.outputs.base || github.ref }} | |
| - uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38 # v5.0.4 | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| with: | |
| cache-dependency-path: tests/requirements.txt | |
| cache: "pip" | |
| python-version: "3.x" | |
| - name: Install dependencies | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| run: | | |
| pip install -U pip | |
| pip install -r tests/requirements.txt | |
| - name: Wokwi CI Server | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| uses: wokwi/wokwi-ci-server-action@a6fabb5a49e080158c7a1d121ea5b789536a82c3 # v1 | |
| - name: Install Wokwi CLI | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| run: | | |
| curl -L https://wokwi.com/ci/install.sh | sh | |
| source ~/.bashrc | |
| wokwi-cli --version | |
| - name: Get binaries | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| uses: actions/download-artifact@95815c38cf2ff2164869cbab79da8d1f422bc89e # v4.2.1 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| run-id: ${{ github.event.workflow_run.id }} | |
| name: test-bin-${{ matrix.chip }}-${{ matrix.type }} | |
| path: | | |
| ~/.arduino/tests/${{ matrix.chip }} | |
| - name: Run Tests | |
| if: ${{ steps.check-tests.outputs.enabled == 'true' }} | |
| env: | |
| WOKWI_CLI_TOKEN: ${{ secrets.WOKWI_CLI_TOKEN }} | |
| WOKWI_WIFI_SSID: "Wokwi-GUEST" | |
| # The Wokwi Wi-Fi does not have a password, so we use an empty string | |
| WOKWI_WIFI_PASSWORD: "" | |
| TEST_TYPE: ${{ matrix.type }} | |
| TEST_CHIP: ${{ matrix.chip }} | |
| run: | | |
| bash .github/scripts/tests_run.sh -c -type "$TEST_TYPE" -t "$TEST_CHIP" -i 0 -m 1 -W -wifi-ssid "$WOKWI_WIFI_SSID" -wifi-password "$WOKWI_WIFI_PASSWORD" | |
| - name: Upload ${{ matrix.chip }} ${{ matrix.type }} Wokwi results as cache | |
| uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 | |
| if: steps.check-tests.outputs.enabled == 'true' && needs.get-artifacts.outputs.pr_num | |
| with: | |
| key: test-${{ env.id }}-results-wokwi | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Upload ${{ matrix.chip }} ${{ matrix.type }} Wokwi results as artifacts | |
| uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | |
| if: always() | |
| with: | |
| name: test-results-wokwi-${{ matrix.chip }}-${{ matrix.type }} | |
| overwrite: true | |
| path: | | |
| tests/**/*.xml | |
| tests/**/result_*.json | |
| - name: Report conclusion | |
| uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1 | |
| if: always() | |
| with: | |
| script: | | |
| const owner = '${{ github.repository_owner }}'; | |
| const repo = '${{ github.repository }}'.split('/')[1]; | |
| const sha = '${{ github.event.workflow_run.head_sha }}'; | |
| core.debug(`owner: ${owner}`); | |
| core.debug(`repo: ${repo}`); | |
| core.debug(`sha: ${sha}`); | |
| const { context: name, state } = (await github.rest.repos.createCommitStatus({ | |
| context: 'Runtime Tests / Wokwi (${{ matrix.type }}, ${{ matrix.chip }}) / Wokwi ${{ matrix.chip }} ${{ matrix.type }} tests (${{ github.event.workflow_run.event }} -> workflow_run)', | |
| owner: owner, | |
| repo: repo, | |
| sha: sha, | |
| state: '${{ job.status }}', | |
| target_url: 'https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}' | |
| })).data; | |
| core.info(`${name} is ${state}`); | |
| - name: Remove Re-trigger Wokwi Tests label if it was present | |
| if: | | |
| always() && | |
| needs.get-artifacts.outputs.pr_num && | |
| needs.get-artifacts.outputs.rerun_wokwi_test == 'true' | |
| continue-on-error: true | |
| run: | | |
| gh pr edit "$PR_NUM" --repo "$GITHUB_REPOSITORY" --remove-label 'Re-trigger Wokwi Tests' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| PR_NUM: ${{ needs.get-artifacts.outputs.pr_num }} | |
| GITHUB_REPOSITORY: ${{ github.repository }} |