Skip to content

chore(ci): link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment - #9713

Open
eordano wants to merge 31 commits into
devfrom
feat/unity-cloud-build-link
Open

chore(ci): link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment#9713
eordano wants to merge 31 commits into
devfrom
feat/unity-cloud-build-link

Conversation

@eordano

@eordano eordano commented Aug 12, 2026

Copy link
Copy Markdown
Member

Pull Request Description

What does this PR change?

Navigating from a PR to what CI actually did is currently manual: the status comment's badges are bare shields.io images (clicking one opens the image), the Unity Cloud build behind a run must be found by searching cloud.unity.com by hand, and the tests/lint sections link to nothing — the dorny test report, the NUnit XMLs with per-test timings, the InspectCode report, and the visual-regression Allure report are all unreachable from the comment. This PR turns the unified CI status comment into a link hub:

Build section

  • scripts/cloudbuild/build.py captures the Unity Cloud dashboard deep link (links.dashboard_summary / dashboard_log) from the build-API response, prints it as a ::notice::, adds it to the step summary, and persists it to unity_cloud_build_info.env the moment the build id is known — so it exists for failed builds too. build-unitycloud.yml uploads it as a unity_build_info_<target>_<source> artifact (7-day retention).
  • The comment's build rows pair both destinations per target: Windows build | Unity Cloud #4242 · GitHub job — on the success and failure comment. All badges link the Actions run.
  • check-build-ran emits two outputs: player-artifacts keeps gating the success/skipped split (success comments never interpolate missing artifact ids), while build-ran (widened by unity_build_info_*) gates the failure path — a build that fails before producing player artifacts now posts the failure comment with its Unity Cloud link instead of sticking on "Pending".

Tests section (pr-comment-test-failures.yml + the extractor in test.yml)

  • Badge links the Unity Test run (whose summary hosts the dorny report); each suite row links its Test (editmode/playmode) job.
  • New Time column with suite duration and a collapsible Slowest tests top-10 per suite, parsed from the NUnit XML by the extractor and type-checked by the trusted composer (numeric seconds, single-line names) before rendering.
  • Footer links the Test results (…) artifacts (full NUnit XML + Unity editor logs).

Lint section (pr-comment-warnings.yml)

  • Badge links the lint run; a footer links the run and the csharp-lint-reports artifact (the inline findings list is capped, the artifact has everything).

Performance section (new, pr-comment-artifact-url.yml + new pr-comment-perf.yml)

  • Covers both perf lanes. When comment-success dispatches the bare-metal benchmark (decentraland/performance-testing) after a successful build, the section flips to Dispatched linking that workflow's run queue (repository_dispatch returns no run id); the benchmark itself then rewrites this section with its full report (performance-testing#19, approved), falling back to the classic standalone perf-test-summary comment only when the section write fails.
  • For the perf_test-label lane, the new companion pr-comment-perf.yml writes Passed/Failed with links to the run summary (which renders the generated benchmark report) and the Performance test results (JSON) / Performance benchmark report (PDF) artifacts. "Unity Performance Test" fires on every PR event but gates on the label at job level, so the companion checks via the jobs API that the perf job actually ran before touching the section — a skipped run never overwrites the bare-metal dispatch status.
  • The section default documents both lanes (including that perf_test skips normal CI and blocks merge while set).

Automation section (new, visual-regression.yml + ci-status-comment)

  • The status comment gains a fourth section. Default: an on-demand hint for /visual-tests. When the suite dispatches it flips to Running (linked to the run), and lands on Passed/Failed with the Allure report and run links. The reusable workflow's own detailed per-platform comment is unchanged.

InWorld section (new, on-demand)

  • The upsert script gains an inworld section that is not part of the skeleton: the suite only runs on release/hotfix PRs into main, so the fence is appended the first time it reports instead of showing a permanent "waiting" row on every PR. The seed path now appends a missing fence to a fresh skeleton too, so a non-skeleton section that has to create the comment cannot wedge the survive check (covered by a new functional test).
  • decentraland/explorer-automation's run-inworld-suite.yml folds its results table into this section when unity-explorer is the caller (explorer-automation#86), retiring the standalone ## InWorld suite comment; other callers and any failed section write keep the standalone path.
  • With this, every recurring CI bot comment merges into the one status comment: build, lint, tests, performance (performance-testing#19), automation, and InWorld (explorer-automation#86). The avatar/wearable-preview-renderer comments intentionally stay separate.
  • upsert-ci-status.sh now appends a missing section fence to existing comments instead of resetting the whole comment to the skeleton — without this, the automation section's first write would have wiped the build/lint/tests state on every open PR.
  • create-release-branch.yml finds its standalone "latest dev build" comment by github-actions[bot] + an img.shields.io/badge/Build badge — which the unified comment's build section now also carries. Its finder is scoped to exclude the <!-- ci-status --> comment, so on a release PR it no longer short-circuits on the unified badge or PATCHes the whole unified comment away. (Fully collapsing release-PR comments into one is tracked as a follow-up in #9936.)

Rollout / merge order

This PR is the keystone of a three-repo change; the other two check out this repo's ci-status-comment action from dev at runtime and depend on the inworld/performance sections plus the NO_CREATE + SECTION_BODY_FILE external-caller contract introduced here.

  1. Merge this PR to dev first. The comment workflows run from the default branch, so the new unified layout appears for runs after the merge.
  2. Then explorer-automation#86 (InWorld fold) and performance-testing#19 (benchmark fold), in either order.

The two companions are independent repos with no shared merge gate, and both degrade to today's standalone-comment behaviour when dev's action does not yet support them (#19 greps the script for SECTION_BODY_FILE/NO_CREATE and refuses; #86 falls back on the inworld allowlist's exit 2). So merging a companion early is harmless — it keeps posting its own comment until this PR lands, then silently switches to folding. There is no window where a companion duplicates or corrupts the unified comment.

Security notes

  • unity_build_info_* files are produced inside the PR-controlled build workflow, so the consumer (composite action .github/actions/ucb-build-links, shared by the success/failure jobs) treats them as untrusted: numeric build id, Unity-dashboard-origin URL regex, unique heredoc delimiters, tampered rows dropped, gh download errors surfaced instead of swallowed. Same treatment for the duration/slowest fields added to the failed-tests artifact.
  • The dashboard URL contains the Unity org/project slugs, which therefore become visible in (public) PR comments. Opening the link still requires Unity org membership. This exposure is accepted (repo admin sign-off on 2026-08-13); if it is ever reconsidered, the fallback direction is moving the ids from secrets to repo vars.
  • GitHub job/artifact links come from the trusted Actions API, not from artifacts.
  • The build job runs PR-head code while holding pull-requests: write (for the live comment upsert). A re-review flagged this as a trust boundary; it is not one, and the rationale is now recorded at build-unitycloud.yml: the trigger is pull_request (not pull_request_target), so a fork gets a read-only token that permissions: cannot escalate and cannot run the job without the Unity Cloud secrets anyway, and a same-repo branch's author already holds the capability with write access.
  • The per-section truncation cap was resized: the SECTION allowlist grew to six, so the old CAP=20000 (6 × 20000 = 120000) could exceed GitHub's 65,536-char comment ceiling — the very 422 it exists to prevent. Now CAP=10000 (6 × 10000 = 60000, with headroom for the header/fences), with the invariant noted in-code so a seventh section forces a re-derive; the ${#NEW_BODY} > 65000 guard remains the runtime backstop.

Test Instructions

This is a CI-only change (no client code); metaforge explorer run does not apply.

Steps (standard run):

# CI-only change — N/A

Expected result: N/A

Steps (fresh account):

# CI-only change — N/A

Expected result: N/A

Automation (if applicable): N/A

Prerequisites

  • Add the force-build label to this PR (it touches no Explorer/** files, so prebuild would otherwise skip the build), or run Unity Cloud Build via workflow_dispatch on this branch.

Test Steps

  1. Trigger a build on this branch (see prerequisites) and check the ::notice::Unity Cloud build #<id> … annotation opens the build's Unity Cloud page, the step summary carries the link, and unity_build_info_* artifacts exist.
  2. After merge to dev (all comment workflows run from the default branch, so the new comment layout appears for runs after the merge):
    • Build section shows Windows/Mac build rows with Unity Cloud + GitHub job links on success and failure; badges link the run.
    • Tests section: badge/suite/job links, Time column, Slowest tests details, artifact footer.
    • Lint section: badge links the run; footer links the InspectCode report artifact.
    • Performance section flips to Dispatched after a successful build; on a perf_test-labeled PR it shows Passed/Failed with the report + artifact links.
    • Automation section shows the /visual-tests hint; commenting /visual-tests flips it to Running and then to Passed/Failed with the Allure report link.
    • InWorld section (release/hotfix PRs into main only): appears the first time the suite reports, folded in from explorer-automation#86; the standalone ## InWorld suite comment is retired to a one-line pointer.
  3. Failure path: a PR whose build fails before uploading player artifacts gets the "Build failed!" comment with Unity Cloud links instead of staying on "Pending".

Additional Testing Notes

  • End-to-end validation in a throwaway repo (dalkia/ci-status-comment-lab#4) with stub producers standing in for Unity Cloud Build / Unity Test / the InWorld suite: one PR ends with a single unified comment carrying build/lint/tests/performance/automation + a folded inworld section (appended last, not seeded — confirming the on-demand path), the seeded leftover standalone comment retired to a pointer, no create-race duplicate across four concurrent writers, and the hostile lint finding's embedded end-marker rendered as body text without breaking the section. The InWorld fold there exercises explorer-automation#86's exact external-caller invocation (NO_CREATE=1 + SECTION_BODY_FILE).
  • Performance fold demo in the same lab (dalkia/ci-status-comment-lab#5): a benchmark stub replicating performance-testing#19's exact section body folds into the performance section, rendering the linked Regression badge (red, report auto-expanded because a regression is present) with the report's Overall: 🔴 … headline and per-runner tables — the badge is sourced from that same headline, so it can't contradict the report. Flip the stub's BADGE_STATE for the No regressions (green) / Partial (amber) / Failed! (red) variants.
  • All embedded scripts were exercised locally with a stubbed gh and fixture artifacts: build-row pairing, tests-section composer (incl. rejection of a tampered slowest entry), and the upsert append-section path (existing sections preserved, automation + inworld fences appended); the test-upsert-ci-status.sh suite (12 cases, incl. the on-demand inworld create/append case) passes.
  • Transition period: PRs whose head predates this change produce no unity_build_info_*/duration data; rows and columns degrade to today's rendering.
  • The Allure report URL mirrors run-visual-suite.yml's S3 path derivation (mode=test, platform=macos defaults) — noted in-code to keep them in lockstep.

Quality Checklist

  • Changes have been tested locally
  • Documentation has been updated (if required)
  • Performance impact has been considered
  • For SDK features: Test scene is included

Code Review Reference

Please review our Branch & PR Standards before submitting. It explains the automated review flow, QA/DEV approval requirements, and what each label does — especially useful for first-time contributors.

…mment

The PR status comment's build badge was a bare shields.io image (clicking it
opened the image itself), and finding the actual Unity Cloud build meant going
to cloud.unity.com and searching for the target and build id by hand.

- build.py captures the dashboard deep link (links.dashboard_summary /
  dashboard_log) from the first build response that carries one, prints it as
  a ::notice::, adds it to the step summary, and persists it to
  unity_cloud_build_info.env.
- build-unitycloud.yml uploads that file as a unity_build_info_* artifact.
  It is written as soon as the Unity-side build id is known, so it exists for
  failed builds too.
- pr-comment-artifact-url.yml adds "Unity Cloud build (Windows/Mac)" rows
  linking the build id to its Unity Cloud page, on both the success and
  failure comments, and wraps every badge in a link to the Actions run.
  The info files are produced by the PR-controlled build workflow, so ids and
  URLs are validated (numeric id, Unity dashboard origin, conservative
  charset) before being rendered into the comment.
- check-build-ran now also counts unity_build_info_* artifacts as evidence
  that a build ran, so a build that failed before producing player artifacts
  posts the failure comment (with the Unity Cloud link) instead of leaving
  the comment stuck on "Pending".

Note: the dashboard URL comes from the Unity Cloud Build API response and
contains the org/project slugs; it will be visible in PR comments.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eordano
eordano requested review from a team as code owners August 12, 2026 23:10
@decentraland-bot decentraland-bot added the ext-contribution Identifies a contribution which was not initiated by a Unity Developer label Aug 12, 2026
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

🚦 CI Status

Build

Build failed! Check the logs to see what went wrong.
If the error repeats please consider the clean-build tag.

Lint

Waiting for lint to start…

Tests

Waiting for tests to start…

Performance

🏁 Bare-metal benchmark finished — run #31746714469.

  • Apple M1 — ⚪ all within noise
  • Intel Core i5 — 🟢 CPU 1% worst 21% faster, 🟢 CPU 0.1% worst 18% faster, 🟢 GPU 1% worst 21% faster, 🟢 GPU 0.1% worst 21% faster
Full report (per-runner tables)

PR #9713, run #31746714469

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times. The values are the median, and (min–max) is the lowest and highest of those runs — a wide range means the metric is noisy and small differences are not trustworthy.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a real difference: larger than both 3% and the run-to-run range.
  • ⚪ within noise — the difference is smaller than how much the build varies between its own runs, so it cannot be told apart from random variation. Treat it as no change.
  • Exceptions per run — the average number of exceptions in a run's log; more than the baseline is flagged 🔴 even when frame times look fine. The Exception breakdown under each table groups them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.

Apple M1

Metric Baseline Change Δ Result
Samples 4021 (×3) 3972 (×3)
CPU average 22.3 ms (21.7–23.2) 22.5 ms (22.3–23.6) 0.3 ms ⚪ within noise
CPU 1% worst 230.5 ms (224.7–232.1) 232.1 ms (230.2–235.6) 1.5 ms ⚪ within noise
CPU 0.1% worst 238.5 ms (234.0–239.6) 239.9 ms (233.7–243.1) 1.3 ms ⚪ within noise
GPU average 6.9 ms (2.7–7.2) 4.1 ms (2.2–5.4) -2.9 ms ⚪ within noise
GPU 1% worst 35.3 ms (33.7–37.2) 35.9 ms (35.2–37.2) 0.6 ms ⚪ within noise
GPU 0.1% worst 36.3 ms (35.1–38.0) 38.0 ms (36.6–38.4) 1.7 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

Intel Core i5

Metric Baseline Change Δ Result
Samples 2287 (×3) 2354 (×3)
CPU average 39.2 ms (36.8–39.3) 38.0 ms (37.1–38.1) -1.2 ms ⚪ within noise
CPU 1% worst 378.9 ms (366.5–380.5) 297.9 ms (287.9–301.9) -81.0 ms 🟢 21% faster
CPU 0.1% worst 395.9 ms (389.0–396.8) 322.9 ms (306.6–323.9) -73.0 ms 🟢 18% faster
GPU average 9.5 ms (9.4–9.6) 9.4 ms (9.3–9.6) -0.1 ms ⚪ within noise
GPU 1% worst 40.2 ms (39.7–40.3) 31.7 ms (31.2–32.5) -8.5 ms 🟢 21% faster
GPU 0.1% worst 48.7 ms (44.9–49.0) 38.3 ms (35.8–42.7) -10.4 ms 🟢 21% faster
Exceptions per run 66 66 0 ⚪ none new
Exception breakdown
Exception Baseline (3 runs) Change (3 runs)
[UI] DllNotFoundException 192 192
[ENGINE] NullReferenceException 3 3
[ENGINE] ObjectDisposedException 3 3

@github-actions

Copy link
Copy Markdown
Contributor

Slack notification sent to #explorer-ext-contributions for external review.
To re-send, delete this comment and re-add the ext-contribution label.

@claude

This comment has been minimized.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

CI Review — ci: deep-link the Unity Cloud build page from CI and the PR status comment

STEP 2 — Root-cause check: PASS

The PR status comment's build badge was a dead-end image (clicking opened shields.io), and build logs told humans to search cloud.unity.com by hand. This PR fixes the root cause by capturing the dashboard deep link from the Unity Cloud Build API response, persisting it as an artifact, and rendering it — validated — in the PR comment. No symptom-masking.

STEP 3 — Design & integration: PASS

This is a CI-only change (no ECS systems, no runtime code). The data flow is sound:

build.py captures URL from Unity Cloud API → writes .env file → uploaded as artifact → downloaded by comment workflow → validated against conservative regex → rendered in PR comment.

Key design decisions reviewed:

  1. Artifact-based communication — follows the existing pattern between the build and comment workflows. The workflow_run event trigger means these run in separate jobs/workflows, so artifacts are the correct communication channel.
  2. Untrusted input boundary — artifacts from the PR-controlled build workflow are correctly treated as untrusted in the comment workflow. Validation is applied before rendering.
  3. Graceful degradation — if no dashboard link is found (older branches, API changes), rows simply don't render and the comment layout is unchanged.
  4. check-build-ran fix — counting unity_build_info_* artifacts as evidence a build ran is a correct fix for builds that fail before producing Decentraland_* player artifacts (which previously left comments stuck on "Pending" forever).

STEP 4 — Member audit: N/A

CI/workflow code — no C# classes or properties to audit.

STEP 5 — Line-level review

Security analysis — No security issues found

  • Input validation: BUILD_ID validated with ^[0-9]+$, DASHBOARD_URL validated with ^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&-]*$. The charset excludes ), ], <, >, ", ', newlines, and # — no markdown link breakout, no fragment injection, no HTML injection. ✅
  • Heredoc delimiter safety: UCB_ROWS_EOF / UCB_SECTION_EOF delimiters are hardcoded and cannot appear in the validated content (restricted charset, no underscores in sequence). No delimiter injection. ✅
  • Secrets: No hardcoded secrets. GITHUB_TOKEN passed via ${{ github.token }}. ✅
  • Badge link expressions: ${{ github.server_url }}, ${{ github.repository }}, ${{ github.event.workflow_run.id }}, ${{ env.PREVIOUS_JOB_ID }} — all GitHub-controlled or previously-validated values. No injection surface. ✅
  • Information disclosure: Dashboard URLs expose Unity Cloud org/project slugs in public PR comments (author acknowledged this). Access requires Unity org membership. Acceptable trade-off. ✅

Code quality observations (non-blocking)

  1. parse_info() duplication — The shell function appears identically in both the success job and failure job, differing only in the run-ID variable (PREVIOUS_JOB_ID vs RUN_ID). This is inherent to GitHub Actions' job isolation (jobs can't share inline shell functions). A reusable composite action could deduplicate this, but that would be over-engineering for a self-contained helper. Noting for future maintenance — if the validation logic ever needs updating, both copies must change in sync.

  2. Python code consistency — The id parameter in record_build_link_info(id, response_json) shadows Python's built-in id(), but this is consistent with the existing codebase (cancel_build(id), poll_build(id), download_artifact(id), download_log(id), delete_build(id), get_log_byte_count(id)). The os.getenv('TARGET') call without a default is also consistent with 10+ existing uses in the file.

  3. Call-site guard logic — The interaction between the call-site guard (if dashboard_url is None) and the internal early-return (if _build_link_info_written and not href: return) is correct and complementary: the file is written on first poll (with or without URL), updated if a later response carries a URL, and the call site skips entirely once a URL is captured. Well-designed.

  4. Success vs. failure comment format — The success path appends UCB_ROWS to an existing table (header already present), while the failure path builds a standalone UCB_SECTION with its own table header. This correctly accounts for the different comment structures. ✅

STEP 6 — Complexity: SIMPLE

STEP 7 — QA: NO

CI-only changes to GitHub Actions workflows and a Python build script. No runtime code, no user-facing behavior changes.

STEP 8 — Non-blocking warnings

None. Main.unity not modified.

STEP 9 — Verdict

REVIEW_RESULT: PASS ✅
COMPLEXITY: SIMPLE
COMPLEXITY_REASON: CI-only changes to GitHub Actions workflows and a Python build script; no runtime Unity code touched.
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by eordano via Slack

Comment thread scripts/cloudbuild/build.py Outdated
Comment thread .github/workflows/pr-comment-artifact-url.yml
Comment thread .github/workflows/pr-comment-artifact-url.yml Outdated
Comment thread .github/workflows/pr-comment-artifact-url.yml Outdated
Addresses the security review on #9713:

- check-build-ran now emits two outputs: player-artifacts (Decentraland_* only)
  keeps gating the success/skipped split so comment-success never interpolates
  missing artifact ids, while build-ran (player or unity_build_info_*) widens
  only the failure path.
- The duplicated parse/compose logic moved into a composite action
  (.github/actions/ucb-build-links) used by both comment jobs, with the
  validation in one place, unique GITHUB_OUTPUT heredoc delimiters, no
  empty-label "[#](url)" rows on tampered input, and gh download errors
  surfaced in the log instead of swallowed.
- build.py only accepts absolute https:// dashboard hrefs (matching the
  consumer regex) and writes the info file immediately after the build id is
  known, not on the first poll.
- unity_build_info_* uploads use retention-days 7; URL charset allows fragments.

The org/project-slug exposure in public comments (finding 1) is accepted:
the ids grant no access without Unity org membership, and the deep link in
the comment is the point of the feature.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

Comment thread .github/actions/ucb-build-links/action.yml Outdated
Comment thread .github/workflows/pr-comment-artifact-url.yml Outdated
…s, automation)

Extends the unified CI status comment into a navigation hub:

- Build rows now pair each target's Unity Cloud build page with its GitHub
  job log ("Windows build | Unity Cloud #id . GitHub job").
- Tests section: badge links the Unity Test run (where the dorny report
  lives), each suite links its job, a Time column shows suite duration, a
  collapsible "Slowest tests" top-10 is parsed from the NUnit XML, and a
  footer links the Test results artifacts (XML + editor logs). The extractor
  in test.yml now records duration and slowest tests; the trusted composer
  type-checks both before rendering (numeric seconds, single-line names).
- Lint section: badge links the lint run; footer links the run and the
  csharp-lint-reports artifact (the inline findings list is capped).
- New "automation" section in the status comment: defaults to an on-demand
  hint for /visual-tests, flips to Running when the suite dispatches, and
  lands on Passed/Failed with the Allure report + run links. The reusable
  workflow's own detailed comment is unchanged.
- ci-status-comment now appends a missing section fence to existing comments
  instead of resetting the whole comment to the skeleton (which would have
  wiped the other sections' state when the automation section first writes).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eordano eordano changed the title ci: deep-link the Unity Cloud build page from CI and the PR status comment ci: link Unity Cloud builds, test reports, timings and automation from the CI status comment Aug 13, 2026
@claude

This comment has been minimized.

Comment thread .github/workflows/pr-comment-test-failures.yml Outdated
Comment thread .github/workflows/test.yml
Comment thread .github/workflows/visual-regression.yml
Adds a fifth "performance" section to the unified CI status comment,
covering both perf lanes:

- Bare-metal benchmark (decentraland/performance-testing): when
  comment-success dispatches it after a successful build, the section flips
  to "Dispatched" linking the target workflow's run queue; the benchmark's
  own perf-test-summary comment (which links its run) remains the detailed
  result, as repository_dispatch returns no run id to link directly.
- In-repo Unity Performance Test (perf_test label): new companion
  pr-comment-perf.yml (workflow_run, trusted context) writes Passed/Failed
  with links to the run summary (which renders the benchmark report) and the
  JSON results + PDF report artifacts. The workflow fires on every PR event
  but its job gates on the label, so the companion checks the job actually
  ran (jobs API) before touching the section - a skipped run must not
  overwrite the bare-metal dispatch status.
- Section default documents both lanes, including that perf_test skips
  normal CI and blocks merge while set.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@eordano eordano changed the title ci: link Unity Cloud builds, test reports, timings and automation from the CI status comment ci: link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment Aug 13, 2026
@claude

This comment has been minimized.

…line

- artifact-url grants actions:read (the composite's cross-run artifact
  reads 403 without it), gains a comment-cancelled job so a cancelled
  build can't leave the live In-progress claim up, and marks the
  link/size/compose steps continue-on-error so the status write always
  lands
- upsert-ci-status normalizes CRLF out of the body and every API read
  (a web-UI edit resubmits \r\n and defeated the whole-line marker
  matching), retries failed POST/PATCH inside the loop instead of dying
  under set -e, and warns when the whole comment nears GitHub's 65k cap;
  new functional test covers the CRLF round trip
- the composite documents the 128KiB env-transport limit and
  test-failures bounds its only unbounded list at composition
- build.py: comment reads distinguish 'absent' from 'unreadable' so a
  transient 502 can't compose a section that wipes the sibling row (page
  bound raised 3->30); failed upsert writes no longer count as asserts;
  record + reconcile both run every poll, so a missing dashboard href or
  a failed info-file write keeps retrying instead of stranding
- visual-regression orders the Running write before the suite so it can
  never overwrite the final verdict, and probes the Allure URL before
  rendering it as a link
- pr-comment-perf resolves fork-PR numbers via the commit->PRs lookup
  when workflow_run.pull_requests is empty
- the unit tests silence build.py's prints so its ::notice:: line stops
  annotating the test job's check run

Not changed: the dashboard URL's org/project ids in public comments stay
by explicit earlier decision.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

The live PR status-comment writer in build.py composed the matrix
platforms' row union from a single read taken before handing the body
to upsert-ci-status.sh, which only verifies that its own write landed
— it has no visibility into a sibling platform's row arriving in the
window between that read and its write, so a second writer's stale
union could silently drop the first writer's row (mikhail-dcl). Fix
the root cause: upsert_live_comment now loops (bounded by the new
LIVE_COMMENT_WRITE_ATTEMPTS) re-reading the section, recomposing the
union, and re-reading once more after the write to confirm every row
it composed actually survived, retrying against a fresh read instead
of trusting a stale snapshot. Added UpsertLiveCommentRaceTest, which
reproduces the interleaving and pins the fix (pravusjif).

Also: named the 3/3/240 reconcile thresholds in
maybe_update_live_comment instead of leaving them as inline magic
numbers (nickkhalow), and declared SUITE_ID/WINDOWS_ARTIFACT_ID/
MAC_ARTIFACT_ID/GITHUB_SERVER_URL/GITHUB_REPOSITORY explicitly in the
"Compose platform rows" step's own env: block instead of relying on
implicit $GITHUB_ENV inheritance from an earlier step in the same job
(dalkia).

The PR-split ask (popuz, blocking) is a submission/process concern —
landing the ci-status-comment hardening, the build-link feature, and
the performance/automation sections as separate sequenced PRs — not a
code defect this branch can fix; see FIXNOTES.md for why it's out of
reach here and the concrete split to do as a follow-up.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018c638dR1vPysCMbYt2qQg5
@claude

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

@claude

This comment has been minimized.

The InWorld suite (run-inworld-suite.yml in explorer-automation) and the
bare-metal benchmark (performance-testing) still post standalone PR
comments; both fold into the unified comment instead. The inworld section
is not seeded in the skeleton — the suite only runs on release/hotfix PRs
into main, so the fence is appended the first time it reports. The seed
path now appends a missing fence to a fresh skeleton too, so a
non-skeleton section that has to create the comment cannot wedge the
survive check.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9713, run #33200618068

Overall: ✅ no significant changes

Builds: Windows change, Windows baseline, macOS change, macOS baseline

How to read this table
  • Each build is measured 3 times, interleaved with the other build (change, baseline, change, baseline, ...) in the same session, so both see the same world content and machine state. The values are the median, and (min–max) is the lowest and highest of those runs.
  • Δ is Change minus Baseline (a negative Δ means Change is faster).
  • 🟢 faster / 🔴 slower — a difference that passed every check: the runs are fully separated (every run of one build faster than every run of the other), and the median difference is at least 3% and at least 0.5 ms.
  • ⚪ within noise — the builds' runs overlap, or the difference is tiny; it cannot be told apart from random variation. Treat it as no change.
  • — informational — the 0.1% worst metrics average only the few worst frames of a run, so a single OS hiccup swings them by a lot; they are shown for context and never earn a verdict.
  • ⚠️ no verdict — the two builds' sessions were not comparable (very different sample counts, or too few usable runs), so no conclusion is drawn from them.
  • Exceptions per run — the average number of exceptions in a run's log, not counting teardown ones logged while the app quits. Flagged only on a difference of at least 2 per run and 1.5× the other build; exception kinds the baseline never threw are called out under the table. The Exception breakdown groups all of them by the explorer's report category and exception type (as totals across the runs).
  • A run that logged unusually many exceptions (at least 10 and 5× the median of its build's runs — e.g. a service was down during it) is excluded from all numbers and called out under the table.
  • The Overall line at the top only reacts to a metric that moved on two or more machines, or by 10% or more on one — a single modest 🟢/🔴 cell can still be a statistical fluke.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2381 (×3) 2357 (×3)
CPU average 37.5 ms (36.5–38.1) 38.0 ms (37.9–38.1) 0.5 ms ⚪ within noise
CPU 1% worst 325.8 ms (311.4–340.4) 326.8 ms (286.7–340.8) 0.9 ms ⚪ within noise
CPU 0.1% worst 353.1 ms (341.1–368.9) 354.0 ms (307.3–389.9) 0.9 ms — informational
GPU average 23.3 ms (22.6–24.3) 23.7 ms (23.3–24.3) 0.4 ms ⚪ within noise
GPU 1% worst 332.0 ms (318.3–332.4) 324.9 ms (293.3–346.7) -7.1 ms ⚪ within noise
GPU 0.1% worst 360.4 ms (348.8–363.3) 354.6 ms (323.0–355.9) -5.8 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

Apple M1

Metric Baseline Change Δ Result
Samples 3381 (×3) 3361 (×3)
CPU average 26.5 ms (25.4–27.1) 26.6 ms (26.2–26.8) 0.1 ms ⚪ within noise
CPU 1% worst 239.7 ms (234.4–240.3) 238.0 ms (236.2–238.8) -1.8 ms ⚪ within noise
CPU 0.1% worst 245.2 ms (244.0–249.2) 243.2 ms (240.8–244.6) -2.0 ms — informational
GPU average 18.9 ms (18.3–21.2) 18.5 ms (17.5–18.6) -0.4 ms ⚪ within noise
GPU 1% worst 53.8 ms (45.9–57.6) 55.3 ms (52.0–55.8) 1.5 ms ⚪ within noise
GPU 0.1% worst 61.3 ms (53.3–61.5) 58.9 ms (58.8–59.3) -2.5 ms — informational
Exceptions per run 0 0 0 ⚪ no significant change

@claude

This comment has been minimized.

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR Review: chore(ci): link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment

Step 2 — Root-cause check: PASS

The PR solves the correct problem: CI status comment badges were unlinked images, Unity Cloud builds had to be found by hand-searching cloud.unity.com, and test/lint/automation reports were unreachable from the comment. The diff adds links at their source (workflow output, API response, artifact ID) and composes them into the comment. This addresses the cause — missing linkage — not a symptom.

Step 3 — Design & integration: PASS

This is CI infrastructure (GitHub Actions YAML, bash, Python). No ECS systems, components, or runtime code.

Concurrent-write modelupsert-ci-status.sh uses read-modify-write with a 5-attempt verify-and-retry loop that re-reads after each write to confirm the section landed and no duplicate slipped in. The new on-demand section approach (inworld fence appended on first report, not in the skeleton) avoids permanent "waiting" noise on PRs that never run the suite. The NO_CREATE flag for external callers prevents foreign-token comment creation that would spawn unfindable duplicates.

Live comment updates from build.py — Race detection is well-designed: reads the section, composes a row union preserving any sibling platform's row, writes, re-reads to verify the composed union survived, and retries on mismatch (bounded by LIVE_COMMENT_WRITE_ATTEMPTS=3). Rate-limited with MAX_LIVE_ASSERTS=3, MAX_LIVE_CONFIRMS=3, and LIVE_RECONCILE_INTERVAL_SECS=240 to avoid burning the comments-API budget.

Two-tier gatingplayer-artifacts gates the success comment (which interpolates artifact IDs that must exist), while build-ran (widened by unity_build_info_*) gates the failure path so builds failing before producing player artifacts still post a failure comment with the Unity Cloud link. Clean separation.

comment-cancelled job — Correctly handles the gap where a cancelled build leaves an "In progress" badge stale. Fires for any cancelled run regardless of whether a build started — the right tradeoff ("Cancelled" is accurate and avoids stuck state).

Step 4 — Member audit: No issues

All new Python functions have clear responsibilities and appropriate consumer counts:

  • record_build_link_info — 2 callers (poll loop, record_final_elapsed). Persists dashboard link + timing to file.
  • _github_api — 2 callers (_build_section_of_status_comment, _own_job_url). Shared HTTP helper.
  • _build_section_of_status_comment — 1 caller (upsert_live_comment). Extracts current build section from the live comment.
  • upsert_live_comment — 1 caller (maybe_update_live_comment). Distinct responsibility from the rate-limiting/budget logic.
  • maybe_update_live_comment — 2 callers (record_build_link_info, poll loop). Gate and rate-limiter.

No single-use merge candidates or derived-predicate smell.

Step 5 — Line-level review: No blocking issues

Security review: No security issues found

  • Untrusted artifact data is validated: build IDs as ^[0-9]+$, URLs against a compiled allowlist regex (_DASHBOARD_LINK_RE / URL_RE — drift-guarded by test_matches_consumer_url_re), durations as numeric-only, test names sanitized (gsub("[\r\n\x60|]"; " ")).
  • Marker-shaped lines stripped from section bodies to prevent comment structure injection (tested in scenario 4 and 9 of test-upsert-ci-status.sh).
  • PAT expiry probe (HEAD /rate_limit) keeps the token in env (out of argv via GH_TOKEN="$PAT") and surfaces only the expiry date, not the token.
  • Permissions explicitly scoped per job — pull-requests: write only where the comment upsert runs, {} on the pure-bash gate job.
  • Oversized body truncation re-closes severed code fences and <details> blocks to prevent rendering bleed into neighbouring sections.
  • CRLF normalization (${SECTION_BODY//$'\r'/}) applied to both incoming bodies and API reads, preventing duplicate fences from a GitHub web-editor round-trip.

P2 observations (non-blocking)

  1. Duration formatter duplicationfmt_dur/fmt_secs is implemented three times (ucb-build-links, pr-comment-test-failures, pr-comment-warnings). The PR description notes "keep the three in lockstep" and each uses a slightly different shell dialect (bash arithmetic vs. awk). Acceptable for independent CI scripts that run in different job contexts.

  2. math.isfinite() belt-and-suspenders in test.yml — Guarding both individual float() results and the accumulated sum against nan/inf/1e999 is correct: float() admits non-finite values without raising, and json.dump would emit bare NaN/Infinity — invalid JSON that breaks the consumer's very first jq read.

  3. Backward compatibility — Old PRs without unity_build_info_* or duration data gracefully degrade: rows omit the parts whose data was absent, and the continue-on-error: true on the fetch/compose steps means a missing artifact silently produces empty cells rather than failing the job.

Tests: Comprehensive

test-upsert-ci-status.sh (11 scenarios): skeleton creation, section update preservation, missing fence append + header migration, marker-shaped body stripping, duplicate GC, NO_CREATE exit code, unknown section rejection, oversized body truncation with construct re-closing, embedded own-section marker wedge, stale re-read convergence, CRLF normalization.

test_build_helpers.py: platform key extraction (template targets, branch-derived, unknown), dashboard URL construction + consumer-regex drift guard, link info file lifecycle (first write, API href replacement, final elapsed rewrite, rejection of non-build links and consumer-failing links, negative elapsed clamping), live comment reconcile budgets (failed first write retry, caps), and the sibling-row race recovery with a pinned multi-attempt scenario.

Step 6 — Complexity: COMPLEX

17 files, +2021/−209 lines across GitHub Actions workflows, bash CI scripts, and the Python build orchestrator. Introduces concurrent-write handling, race detection, live comment updates, new workflow jobs, and comprehensive test coverage.

Step 7 — QA: NO

Changes are limited to CI/CD workflows (.github/), bash scripts, and Python build scripts. No runtime code is affected — no Unity player code, no user-facing behavior changes.

Step 8 — Non-blocking warnings: None

Main scene not modified.


REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: Touches 17 CI infrastructure files across GitHub Actions workflows, bash scripts, and the Python build orchestrator with concurrent-write handling and race detection.
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni [Dalkia] (<@U03JSUQ5Z7U>) via Slack

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review

STEP 1 — Load context & set scope

Read CLAUDE.md, docs/README.md, and docs/build-and-ci.md; checked out PR #9713 locally and reviewed the diff against origin/dev. Changed files are CI workflows/actions plus scripts/cloudbuild/build.py and its Python tests. I also ran the shell functional tests and Python helper tests locally.

STEP 2 — Root-cause check

The PR is solving a real navigation gap: CI status comments did not link Unity Cloud builds, detailed reports, timings, performance runs, or automation outputs. The artifact-based workflow_run sections address the root cause. The new live build-comment path, however, moves part of comment writing into PR-head build code instead of keeping writes in the trusted post-run comment workflows.

STEP 3 — Design & integration

Relevant owners searched/read: .github/actions/ci-status-comment/upsert-ci-status.sh owns the unified comment upsert; .github/actions/ci-status-comment/action.yml wraps it; .github/workflows/pr-comment-artifact-url.yml, pr-comment-test-failures.yml, pr-comment-warnings.yml, pr-comment-perf.yml, and visual-regression.yml are the trusted section writers; scripts/cloudbuild/build.py owns Unity Cloud build lifecycle. No new long-lived runtime/ECS unit is introduced. The artifact-based design fits the existing owner model, but the live comment writer in build.py duplicates the trusted comment-writer responsibility and requires a write-capable token in code checked out from the PR head.

Teardown/consumption trace: Unity build link artifacts are consumed by .github/actions/ucb-build-links/action.yml; the build-status section is consumed by upsert-ci-status.sh; temporary files in build.py live-comment writes are unlinked in finally; downloaded artifacts are job-local. No subscription/resource leak found.

STEP 4 — Member audit

No public C# members/accessors were added or changed. New Python helpers are module-private workflow helpers and are covered by scripts/cloudbuild/test_build_helpers.py.

STEP 5 — Line-level review

[P1] The build job grants pull-requests: write and runs PR-head scripts/cloudbuild/build.py with GH_TOKEN. That breaks the trust boundary already used by the CI comment workflows: PR-controlled code should produce artifacts, while trusted default-branch workflows should write comments. See inline suggestions.

Security review: one security issue found — the PR-head build job receives a PR-write token. No hardcoded secrets or shell-injection issues found in the new artifact parser path; untrusted Unity build-info artifacts are validated before rendering by .github/actions/ucb-build-links/action.yml.

Sub-agents run: security-sentinel, architecture-strategist, pattern-recognition-specialist, code-simplicity-reviewer, kieran-python-reviewer.

STEP 6 — Complexity assessment

COMPLEX: 17 files and ~2k LOC changed across GitHub Actions orchestration, shell comment upsert logic, and the Unity Cloud Python build handler.

STEP 7 — QA assessment

QA_REQUIRED: NO — changes are limited to CI/workflow/scripts and do not affect Unity player runtime behavior.

STEP 8 — Non-blocking warnings

No main scene changes. Local checks run: python -m py_compile scripts/cloudbuild/build.py scripts/cloudbuild/test_build_helpers.py, python scripts/cloudbuild/test_build_helpers.py, and bash .github/actions/ci-status-comment/test-upsert-ci-status.sh all passed. GitHub checks currently show the Windows build/build gate failing and enforce-approvals failing.

REVIEW_RESULT: FAIL ❌
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: CI orchestration, comment-writing workflows/actions, and the Unity Cloud Python build handler are all changed.
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni [Dalkia] (<@U03JSUQ5Z7U>) via Slack

Comment thread .github/workflows/build-unitycloud.yml
Comment thread .github/workflows/build-unitycloud.yml
@claude

This comment has been minimized.

The build job runs PR-head code with a write token, which a security
re-review flagged as a trust boundary. Record why it is not one:
pull_request (not pull_request_target) gives forks a read-only token
that permissions cannot escalate, forks cannot run the job without the
Unity Cloud secrets anyway, and a same-repo branch's author already
holds the capability. No behaviour change.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

The per-section CAP was 20000, but the section allowlist grew to six
(build, lint, tests, performance, automation, inworld). 6 × 20000 =
120000 > 65536, so the cap could not actually prevent the 422 it exists
to prevent. Drop it to 10000 (6 × 10000 = 60000, ~5.5k headroom for the
header and fences) and note the invariant so a future section forces a
re-derive. Test assertion updated to match.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

…comment

Its build-links comment finder matches any github-actions[bot] comment
containing an img.shields.io/badge/Build badge — which the unified CI
status comment's build section now also carries. On a release PR that
has one, this either short-circuited on its "Build-Success!" badge (so
the dev-build-links comment was never posted) or PATCHed the entire
unified comment away with the release message. Exclude the unified
comment by its <!-- ci-status --> marker, matching the finders in the
perf and InWorld folds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@claude

This comment has been minimized.

@claude

claude Bot commented Sep 1, 2026

Copy link
Copy Markdown
Contributor

Claude finished @dalkia's task in 1m 48s —— View job


Security-focused review — round 29 (b738c353b)

  • Identify what changed since round 28 (d4eb6b2f1)
  • STEP W on the changed surface
  • STEP 1–3 dependency review
  • Re-verify the carried findings
  • Post verdict

This round is a merge only — the PR's own surface is byte-identical to round 28.

b738c353b is a two-parent merge of dev (ee7128d62) into the branch. Everything it brought is dev-side C#:

Explorer/…/DCL/Audio/WorldAudioPlaybackController.cs
Explorer/…/ProcessOtherAvatarsInteractionSystem.cs (+ its test)
Explorer/…/Systems/UpdateAudioSourceSystem.cs (+ its test)

Three checks say the merge is clean and not an "evil merge":

Check Result
git diff d4eb6b2f1 b738c353b -- .github/ scripts/ empty — every PR-owned file is unchanged from round 28
git diff --name-only ee7128d62 b738c353b -- Explorer/ unity-shared-dependencies/ avatar-preview-renderer/ empty — the merge added nothing on top of dev outside the PR surface
git rev-list --count HEAD..origin/dev 0 — the branch is fully current with dev; no stale-base drift left

So git diff --name-only ee7128d62 b738c353b is exactly the 18 PR files, and git diff --stat origin/dev...HEAD is unchanged at 18 files, +2038/−210. There is no new code to review; nothing I verified in rounds 1–28 can have regressed here, and I re-read the three values my open findings anchor on to confirm the line references still hold (CAP=10000 at :57, the six-entry SECTION allowlist at :94, the two bare --paginate --slurp calls at :180/:270).

STEP W — nothing to bind to

No workflow or prompt file changed in this commit. Across the whole PR the trigger surface is unchanged since round 28: no pull_request_target anywhere (the only grep hit is the explanatory comment at build-unitycloud.yml:823 recording why the pull_request trigger makes the build job's pull-requests: write safe), no new ${{ github.event.* }} interpolated into a run: body, no heredoc regressions, no LLM invocation, no allowedTools, no retention change, .github/prompts/** untouched.

STEP 1–3 — N/A

The dependency-shaped glob over Packages/**, Explorer/** plugins, *.asmdef, *.dll/.so/.dylib/.bundle, **/requirements.txt, **/package.json returns nothing for origin/dev...HEAD. scripts/cloudbuild/requirements.txt is untouched by this PR and still the single pinned requests==2.32.2. The new-dependency label remains a labeler false positive.

Carried findings — all unchanged (same file bytes)

From Finding Status
r23 #1 CAP × 6 sections over the 65536 ceiling ✅ fixed in 855249521
r28 #1 create-release-branch.yml has no permissions: block at all while it force-pushes, opens a PR and PATCHes comments open — NIT
r27 #1 CAP=10000 sits below MAX_FINDINGS: 50's rendered size and the 60000 failed_list bound, so specific artifact-pointer notes get replaced by the generic one open — NIT
r23 #2 / #3 section_default inworld never rendered; automation default describes only the /visual-tests lane open — NIT
r22 #1#3 !cancelled() && …authorized lets a failed resolve dispatch an empty build_url; resolve has no job-level permissions:; "(unnamed)" on failed but not timings open
r21 #1#4 post-write verify checks "my rows present", not "I dropped a sibling's row"; _live_comment_last_attempt stamped before a ≤540s call; only_if_missing short-circuits every retry; returncode != 0 → return None spends no retry open
r20 #1 bare gh api … --paginate --slurp dies under set -e open
r20 #5#7 timed_out/stale/neutral strand the build section; single unretried curl -sfIL gates the Allure link; ${failed_list:0:60000} can sever an inline-code pair open
r19 #2/#3 _DASHBOARD_LINK_RE.match() $ admits a trailing \n; static SIZE_REPORT<<EOF beside the randomized PLATFORM_ROWS delimiter open

None of these block. Every one is a NIT or a bounded-degradation case in a status-comment path that already degrades gracefully, and each has been triaged in an earlier round.

Test suites

CI Scripts Tests is green on this merge head (run 33541059567) — the 12-case test-upsert-ci-status.sh suite and test_build_helpers.py both pass at CAP=10000.

As in rounds 17–28, I could not execute either suite locally: bash .github/actions/ci-status-comment/test-upsert-ci-status.sh was denied by the tool policy. Everything above is from git plumbing and reading the tree; CI's green run is the executable evidence.

Verdict

A clean, up-to-date merge with no functional delta on the reviewed surface. Nothing outstanding blocks the merge from my side. The only red check is Enforce QA and DEV Approvals — an approvals gate, not a code finding — and dalkia has since approved.

DEPENDENCY_REVIEW: NEEDS_ATTENTION — unchanged in substance since round 19 and untouched by this commit. Still driven by the same pre-existing, already-triaged W.7a item: visual-regression.yml:183 and in-world-tests.yml:190 both call decentraland/explorer-automation reusables at @main with secrets: inherit. Neither line is this PR's, and both need the callee to declare workflow_call secrets before explicit passing is syntactically possible.

--- · feat/unity-cloud-build-link

@decentraland-bot decentraland-bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final Review — chore(ci): link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment

STEP 1 — Load context & set scope

Read CLAUDE.md, docs/README.md. Loaded the diff (18 files, +2038/−210). Changed files are CI-only: GitHub Actions workflows/actions, bash scripts (upsert-ci-status.sh, test-upsert-ci-status.sh), Python (build.py, test_build_helpers.py), and a new composite action (ucb-build-links). No runtime Unity code is touched.

Reviewed inline comments from previous reviews (20+ findings from Aug 12–13, two full reviews from Aug 31). Confirmed that prior P1 findings (URL validation alignment, heredoc delimiters, player-artifacts/build-ran split, grep -qxF whole-line matching, failure() && dispatch guard, PAT probe continue-on-error, NO_CREATE delay, truncation with fence re-closing, math.isfinite() guards, test name sanitization, _platform_key() prefix match) were addressed in subsequent commits.

Ran parallel sub-agents: security-sentinel, architecture-strategist, code-quality-reviewer.

STEP 2 — Root-cause check: PASS

The PR solves a real navigation gap: CI status comment badges were dead-end images, Unity Cloud builds had to be found by hand-searching cloud.unity.com, and test/lint/performance/automation reports were unreachable from the comment. The diff adds links at their source (workflow output, API response, artifact ID) and composes them into the comment. This addresses the cause — missing linkage — not a symptom.

STEP 3 — Design & integration: PASS

No ECS, runtime, or scene code. The section-fence model with concurrent-write safety is well-designed:

  • Skeleton vs on-demand sections: 5 always-present sections seeded in the skeleton, inworld appended lazily. The grep -qxF whole-line match prevents embedded marker-shaped content from being mistaken for a real fence.
  • Concurrent writer safety: upsert-ci-status.sh's 5-attempt verify-and-retry loop guarantees a fixed body lands in a section. Where content is a union from multiple concurrent writers (live build rows from windows64 + macos), build.py's upsert_live_comment layers its own read-compose-write-verify on top — two different problems at two different layers, with a dedicated regression test (UpsertLiveCommentRaceTest) pinning the exact race.
  • Two-tier gating: player-artifacts gates success/skipped (which interpolate artifact IDs that must exist), build-ran gates failure (any evidence of a build). Clean split.
  • External caller contract: NO_CREATE prevents foreign-token orphan comments; SECTION_BODY_FILE for bodies too large for env.
  • Trust boundary (re-verified): The build job runs PR-head code with pull-requests: write on a pull_request trigger. Forks get a read-only token regardless of permissions: and can't run the job without Unity Cloud secrets; same-repo branch authors already hold write access. No escalation path. Rationale correctly documented in build-unitycloud.yml.

Teardown trace: temp files in upsert_live_comment cleaned in finally; downloaded artifacts job-local. No resource leak.

STEP 4 — Member audit: N/A

No public C# members added/changed. Python helpers are module-private and covered by test_build_helpers.py.

STEP 5 — Line-level review

Security review: Ran the security-review skill checklist. Untrusted artifact data validated (numeric IDs, URL regex with drift guard, test name sanitization). CRLF normalization applied consistently. Marker-shaped lines stripped. PAT stays in env (not argv). secrets: inherit on visual-regression.yml's suite call is pre-existing and unchanged — the two new jobs scope down to contents: read + pull-requests: write. No security issues found in new code.

Three P2 findings — see inline comments with suggestion blocks.

STEP 6 — Complexity: COMPLEX

18 files, +2038/−210 across GitHub Actions workflows, bash CI scripts, Python build orchestrator, and two new test suites. Concurrent-write handling, race detection, live comment updates.

STEP 7 — QA: NO

CI-only changes. No runtime code, no user-facing behavior.

STEP 8 — Non-blocking warnings: None

Main scene not modified.

REVIEW_RESULT: PASS ✅
COMPLEXITY: COMPLEX
COMPLEXITY_REASON: CI orchestration, comment-writing workflows/actions, and the Unity Cloud Python build handler changed across 18 files.
QA_REQUIRED: NO


Reviewed by Jarvis 🤖 · Requested by Juan Ignacio Molteni [Dalkia] (<@U03JSUQ5Z7U>) via Slack

description: Markdown for this section (inline badge + message). Rendered as-is between the section markers.
description: >-
Markdown for this section (inline badge + message). Rendered between the
section markers after dropping marker-shaped lines; bodies over 20000

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Documentation says "over 20000" but the actual CAP in upsert-ci-status.sh is 10000 (6 × 10000 = 60000, with headroom under 65536). External callers relying on the documented threshold will hit truncation at half the expected size.

Suggested change
section markers after dropping marker-shaped lines; bodies over 20000
section markers after dropping marker-shaped lines; bodies over 10000

Comment on lines +67 to +71
while :; do
TRUNCATED="${SECTION_BODY:0:CUT}"
if [ $(( $(grep -c '^```' <<< "$TRUNCATED") % 2 )) -ne 0 ]; then
TRUNCATED="$TRUNCATED"$'\n''```'
fi

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Two improvements for the truncation loop:

  1. Iteration cap — the loop terminates (CUT strictly decreases to 0, body degenerates to just NOTE) but has no safety valve on iteration count. A section body with many <details> tags from adversarial test names could force many iterations, each forking grep/wc. A cap of 10 is generous and makes the bound explicit.

  2. Single-backtick rebalancing — fences and <details> are rebalanced but inline backticks are not. Test names render in ` pairs; a cut between the pair leaves a dangling backtick that swallows subsequent sections as monospace — the same rendering-bleed this block exists to prevent, just for a different construct.

Suggested change
while :; do
TRUNCATED="${SECTION_BODY:0:CUT}"
if [ $(( $(grep -c '^```' <<< "$TRUNCATED") % 2 )) -ne 0 ]; then
TRUNCATED="$TRUNCATED"$'\n''```'
fi
for _rebalance in 1 2 3 4 5 6 7 8 9 10; do
TRUNCATED="${SECTION_BODY:0:CUT}"
if [ $(( $(grep -c '^```' <<< "$TRUNCATED") % 2 )) -ne 0 ]; then
TRUNCATED="$TRUNCATED"$'\n''```'
fi
# Close a dangling inline backtick — test names arrive in `…` pairs,
# and a cut between the pair eats everything after it as monospace.
ticks=$(grep -o '`' <<< "$TRUNCATED" | wc -l || true)
if [ $(( ticks % 2 )) -ne 0 ]; then
TRUNCATED="$TRUNCATED"'`'
fi

Comment on lines +813 to +814
link = dashboard_url or _dashboard_build_url(build_id)
parts.append(f'[Unity Cloud #{build_id}]({link})' if link else f'Unity Cloud build {build_id}')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Defense-in-depth: the artifact-based path (ucb-build-links) validates URLs against URL_RE, but the live-comment path interpolates dashboard_url or the constructed URL directly into a markdown link without re-checking _DASHBOARD_LINK_RE. Today this is safe because TARGET is sanitized upstream in clone_current_target(), but the safety depends on that unrelated sanitizer — there's no drift guard here the way test_matches_consumer_url_re pins the artifact path.

Suggested change
link = dashboard_url or _dashboard_build_url(build_id)
parts.append(f'[Unity Cloud #{build_id}]({link})' if link else f'Unity Cloud build {build_id}')
link = dashboard_url or _dashboard_build_url(build_id)
if link and not _DASHBOARD_LINK_RE.match(link):
link = None
parts.append(f'[Unity Cloud #{build_id}]({link})' if link else f'Unity Cloud build {build_id}')

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ext-contribution Identifies a contribution which was not initiated by a Unity Developer force-build Used to trigger a build on draft PR new-dependency

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants