fix: make debug console log ingestion thread-safe via pending queue #17
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: In-World Tests | |
| # Dispatcher for the InWorld NUnit suite. The mechanics — Explorer install, | |
| # AltTester, `mf explorer test`, Allure upload, PR comment — live in | |
| # decentraland/explorer-automation's `run-inworld-suite.yml`. This file only | |
| # decides when to run and against which build, and shares its build resolution | |
| # with visual-regression.yml via .github/actions/resolve-explorer-build. | |
| # | |
| # Triggers: | |
| # - Every PR into main from a `release/**` or `hotfix/**` branch. `pull_request` | |
| # and not `workflow_run` on the build, because a workflow_run reports against | |
| # the default branch's SHA: it never joins the PR's checks and so can never | |
| # gate a merge. Here the check is on the PR from the moment it opens, and the | |
| # first job waits out the ~40-minute Unity Cloud Build the same event started. | |
| # - Manually, from the Actions tab, against any ref. | |
| # | |
| # To make a red suite block the merge, add `InWorld suite result` to main's | |
| # required status checks. That job is the gate: it is the only name worth | |
| # requiring, it stays stable when the reusable workflow's own job names change, | |
| # and it passes for PRs into main that are not release or hotfix. | |
| # | |
| # Release PRs are opened by create-release-branch.yml with GITHUB_TOKEN, and | |
| # events that token raises start no workflow runs — so that workflow dispatches | |
| # this one itself. A dispatch attaches its checks to the dispatched ref's tip, | |
| # which is the PR head, so the gate still lands on the PR. | |
| # | |
| # Required vars: | |
| # - vars.EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL Artifact bucket, and where the | |
| # Allure report is published | |
| # | |
| # Inherited by the reusable workflow via `secrets: inherit`: | |
| # - secrets.ALTTESTER_LICENSE | |
| # - secrets.REPOS_READ_ONLY_TOKEN | |
| # - secrets.EXPLORER_TEAM_S3_BUCKET | |
| # - secrets.EXPLORER_TEAM_AWS_DEFAULT_REGION | |
| # - secrets.EXPLORER_TEAM_AWS_ACCESS_KEY_ID | |
| # - secrets.EXPLORER_TEAM_AWS_SECRET_ACCESS_KEY | |
| on: | |
| pull_request: | |
| branches: [main] | |
| types: [opened, reopened, synchronize, ready_for_review] | |
| workflow_dispatch: | |
| inputs: | |
| filter: | |
| description: 'NUnit filter. Empty runs the whole Category=InWorld suite, split across Windows runners; anything narrower runs on one.' | |
| required: false | |
| default: '' | |
| type: string | |
| build_url: | |
| description: 'Artifact URL to test. Empty resolves the selected ref''s own Unity Cloud Build.' | |
| required: false | |
| default: '' | |
| type: string | |
| tests_ref: | |
| description: 'explorer-automation ref for the tests. Empty matches this ref''s name there, else main.' | |
| required: false | |
| default: '' | |
| type: string | |
| record_perf: | |
| description: 'Record fixture-level perf samples. Off by default — paravirt numbers are unreliable.' | |
| required: false | |
| default: false | |
| type: boolean | |
| # A newer commit supersedes the run in flight: the question is whether the branch | |
| # is good now, and the newer commit contains the older one. | |
| # | |
| # Keyed on the branch, not the PR number, because both triggers have to land in | |
| # one group: create-release-branch.yml dispatches this workflow against the same | |
| # release branch a `pull_request` run also covers, and a PR-number key would put | |
| # the two in groups that can never cancel each other — two gates on one commit, | |
| # four shards on the self-hosted GPU pool, and whichever finishes last owning the | |
| # required check. `github.head_ref` is the source branch on `pull_request` (where | |
| # `ref_name` is the merge ref) and empty everywhere else, so the fallback is what | |
| # a dispatch and a manual run against any ref key on. | |
| # | |
| # Keyed on the head repo as well, because a branch name is not ours to reserve: | |
| # a fork PR into main may name its branch `release/2026-08-10` too, and | |
| # `cancel-in-progress` is applied when the run is created — before any job's | |
| # `if:` is evaluated, so the fork exclusion in `resolve`'s own `if:` (:113) | |
| # only skips the jobs of a run that has already cancelled the real gate. | |
| # Without this segment an outsider could red a release's required check at | |
| # will, by guessing a name that release cuts make predictable. | |
| # `head.repo.full_name` is the head repo on `pull_request` — ours on a same-repo | |
| # PR, `attacker/whatever` on a fork — and absent on `workflow_dispatch`, whose | |
| # payload carries only `inputs`, `ref` and `workflow`; the fallback therefore | |
| # resolves to the same string a same-repo PR produces, which is what keeps the | |
| # two triggers in one group. A fork cannot collide with us: everything before | |
| # the first `/` is the head repo's owner, and only we can be `decentraland`. | |
| concurrency: | |
| group: in-world-tests-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref_name }} | |
| cancel-in-progress: true | |
| jobs: | |
| resolve: | |
| name: Wait for the build | |
| # Skipped rather than filtered at the trigger, so the gate job below still | |
| # reports on every PR into main and a required check never hangs unreported. | |
| # | |
| # Drafts are excluded to match build-unitycloud.yml, which does not build | |
| # them without a `force-build` label — waiting for a build that will never | |
| # start just turns into a timeout. `ready_for_review` above picks the PR up | |
| # the moment it leaves draft. | |
| # | |
| # Fork PRs are excluded because they get no secrets, and the suite needs the | |
| # AltTester licence. Release and hotfix branches never come from a fork. | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| ((startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) && | |
| github.event.pull_request.draft == false && | |
| github.event.pull_request.head.repo.full_name == github.repository) | |
| runs-on: ubuntu-latest | |
| # Covers the wait below plus the artifact check. Builds measure ~40m. | |
| timeout-minutes: 100 | |
| # `actions: read` for the build-unitycloud lookup, `contents: read` for the | |
| # sparse checkout, `pull-requests: read` for the `gh pr list` below. Read | |
| # access to public data survives whatever this block says while the repo is | |
| # public, but the grant should not hinge on that. Nothing here writes. | |
| permissions: | |
| actions: read | |
| contents: read | |
| pull-requests: read | |
| env: | |
| HEAD_REF: ${{ github.head_ref || github.ref_name }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }} | |
| outputs: | |
| build_url: ${{ steps.resolve.outputs.build-url }} | |
| windows_build_url: ${{ steps.resolve.outputs.windows-build-url }} | |
| commit_sha: ${{ steps.resolve.outputs.short-sha }} | |
| tests_ref: ${{ inputs.tests_ref || steps.resolve.outputs.tests-ref }} | |
| branch_label: ${{ github.head_ref || github.ref_name }} | |
| pr_number: ${{ github.event.pull_request.number || steps.pr.outputs.number }} | |
| steps: | |
| # `uses: ./…` resolves against $GITHUB_WORKSPACE, so the action has to be | |
| # on disk. Sparse, because the full tree is ~21k files and none of the | |
| # rest is read here. | |
| - name: Fetch the resolver action | |
| uses: actions/checkout@v6 | |
| with: | |
| sparse-checkout: .github/actions/resolve-explorer-build | |
| - name: Resolve build URL and tests ref | |
| id: resolve | |
| uses: ./.github/actions/resolve-explorer-build | |
| with: | |
| head-ref: ${{ env.HEAD_REF }} | |
| head-sha: ${{ env.HEAD_SHA }} | |
| # A PR opens before its build exists, so wait one out. Manual runs | |
| # pick a ref whose build is already there — or say which to use. | |
| wait-minutes: ${{ github.event_name == 'pull_request' && 90 || 0 }} | |
| # Both suites run, so both zips have to exist before either starts. | |
| build-legs: macos,windows64 | |
| build-url-override: ${{ inputs.build_url }} | |
| public-url-prefix: ${{ vars.EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL }} | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| automation-token: ${{ secrets.REPOS_READ_ONLY_TOKEN }} | |
| # A dispatched run carries no PR, and the release gate is started that way | |
| # — create-release-branch.yml dispatches it, because the PR it opens with | |
| # GITHUB_TOKEN raises no events. Without this the result reaches only the | |
| # job summary, on the one path where the PR is the whole point. | |
| # `--head` matches on branch name alone, hence the fork exclusion. | |
| - name: Find the branch's open PR | |
| id: pr | |
| if: github.event_name == 'workflow_dispatch' | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| set -euo pipefail | |
| NUMBER=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$HEAD_REF" --state open \ | |
| --json number,isCrossRepository \ | |
| --jq '[.[] | select(.isCrossRepository == false)] | .[0].number // ""') | |
| if [ -n "$NUMBER" ]; then | |
| echo "::notice::Results will be commented on PR #${NUMBER}." | |
| fi | |
| echo "number=${NUMBER}" >> "$GITHUB_OUTPUT" | |
| run-suite: | |
| name: InWorld suite | |
| needs: resolve | |
| # @main pins the merged version of the reusable, so PRs to | |
| # explorer-automation that touch it cannot change release validation here. | |
| uses: decentraland/explorer-automation/.github/workflows/run-inworld-suite.yml@main | |
| # Must be a superset of run-inworld-suite.yml's own `permissions:`, or the | |
| # call dies at startup. The write is the PR comment it posts; this is the | |
| # only job that needs one, hence per job rather than at the top of the file. | |
| permissions: | |
| contents: read | |
| pull-requests: write | |
| with: | |
| build_url: ${{ needs.resolve.outputs.build_url }} | |
| filter: ${{ inputs.filter || '' }} | |
| # From the event on a PR run, looked up on a dispatched one. | |
| pr_number: ${{ needs.resolve.outputs.pr_number }} | |
| tests_ref: ${{ needs.resolve.outputs.tests_ref }} | |
| commit_sha: ${{ needs.resolve.outputs.commit_sha }} | |
| branch_label: ${{ needs.resolve.outputs.branch_label }} | |
| # `== true` rather than the raw input: a pull_request event supplies no | |
| # inputs at all, and the reusable's parameter is typed boolean. | |
| record_perf: ${{ inputs.record_perf == true }} | |
| # Both platforms gate a release. `windows` defaults to true upstream, but | |
| # say it here anyway: a default that changes underneath a caller pinned to | |
| # @main is how this repo silently acquired a Windows leg it could not run | |
| # (explorer-automation#55, fixed by #73). | |
| windows: true | |
| # `build_url` is the macOS zip; the Windows leg needs its own or it | |
| # resolves the newest dev build and reports on the wrong commit. | |
| windows_build_url: ${{ needs.resolve.outputs.windows_build_url }} | |
| # What the Windows leg splits across runners. The gate runs everything, so | |
| # say ALL — the same thing inworld-main.yml says for the same reason. | |
| # | |
| # The planner would infer it: an empty scope plus the whole-category filter | |
| # reads as ALL. But that inference exists to stop callers who resolved | |
| # nothing from running unsplit, and a gate should not lean on a fallback | |
| # for its runtime. Empty here is reserved for the other case — a manual run | |
| # that passed a `filter` at all. This repo cannot map an arbitrary NUnit | |
| # filter to a scope, so the planner infers the split from the filter | |
| # instead; a narrow one lands on one runner as `1/1`. | |
| scope: ${{ (inputs.filter || '') == '' && 'ALL' || '' }} | |
| secrets: inherit | |
| result: | |
| name: InWorld suite result | |
| needs: [resolve, run-suite] | |
| if: always() | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| # Reads its `needs` results and nothing else — no API call, no checkout. | |
| permissions: {} | |
| steps: | |
| # The one check worth requiring on main. Everything above is either | |
| # plumbing or a job name owned by another repository. | |
| - name: Gate on the suite | |
| env: | |
| RESOLVE: ${{ needs.resolve.result }} | |
| SUITE: ${{ needs.run-suite.result }} | |
| run: | | |
| set -euo pipefail | |
| if [ "$RESOLVE" = "skipped" ]; then | |
| echo "::notice::Not a release or hotfix PR — the InWorld suite does not gate this one." | |
| exit 0 | |
| fi | |
| if [ "$RESOLVE" = "success" ] && [ "$SUITE" = "success" ]; then | |
| echo "::notice::InWorld suite passed." | |
| exit 0 | |
| fi | |
| # Cancelled counts as failure on purpose: a merge gate must not go | |
| # green because the run was interrupted. The superseding run reports | |
| # its own result. | |
| echo "::error::InWorld gate failed — build: ${RESOLVE}, suite: ${SUITE}. See the Allure report linked in the suite's job summary." | |
| exit 1 |