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 23 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 23 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's own perf-test-summary comment stays the detailed result.
  • 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.
  • 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.

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.

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.
  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

  • 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 fence appended).
  • 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

Windows and Mac build successful in Unity Cloud! You can find a link to the downloadable artifact below.

Name Link
Commit e480a3a
Logs https://github.com/decentraland/unity-explorer/actions/runs/32031213048
Download Windows https://github.com/decentraland/unity-explorer/suites/86834531549/artifacts/9289807359
Download Windows S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/feat/unity-cloud-build-link/pr-25206-e480a3a/Decentraland_windows64.zip
Download Mac https://github.com/decentraland/unity-explorer/suites/86834531549/artifacts/9289614139
Download Mac S3 https://explorer-artifacts.decentraland.org/@dcl/unity-explorer/branch/feat/unity-cloud-build-link/pr-25206-e480a3a/Decentraland_macos.zip
Built on 2026-08-17T13:11:44Z

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
# qualifies when it points at this specific build.
for key in ('dashboard_summary', 'dashboard_log', 'dashboard_url'):
candidate = (links.get(key) or {}).get('href')
if candidate and '/builds/' in candidate:

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.

The '/builds/' in candidate test correctly rejects a bare dashboard root, but it also accepts a relative href. Several Unity Cloud Build API links.*.href values are API-relative paths (/api/v1/orgs/…/builds/42), and such a value contains /builds/ so it wins the loop and gets written as DASHBOARD_URL=/api/v1/….

Downstream that produces a broken ::notice:: "link", a bare relative path in the step summary, and a DASHBOARD_URL the comment workflow silently rejects (its URL_RE requires an absolute https:// Unity host) — so the row degrades to a plain #id with no explanation. Cheap to make the two validators agree:

Suggested change
if candidate and '/builds/' in candidate:
if candidate and candidate.startswith('https://') and '/builds/' in candidate:

Comment on lines +278 to +282
{
echo "UCB_ROWS<<UCB_ROWS_EOF"
printf '%s' "$ROWS"
echo "UCB_ROWS_EOF"
} >> "$GITHUB_ENV"

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.

Static heredoc delimiter on $GITHUB_ENV with artifact-derived content. This is not exploitable as written$ROWS is built only from REPLY_ID (^[0-9]+$) and REPLY_URL (charset with no newline), so no line can equal UCB_ROWS_EOF — but the safety depends entirely on the validation two dozen lines up staying exactly as strict. A random delimiter makes it hold regardless:

Suggested change
{
echo "UCB_ROWS<<UCB_ROWS_EOF"
printf '%s' "$ROWS"
echo "UCB_ROWS_EOF"
} >> "$GITHUB_ENV"
DELIM="UCB_ROWS_EOF_$(uuidgen)"
{
echo "UCB_ROWS<<$DELIM"
printf '%s' "$ROWS"
echo "$DELIM"
} >> "$GITHUB_ENV"

Same applies to the UCB_SECTION block in comment-failed (L393-397).

Comment on lines +266 to +270
if [ -n "$REPLY_URL" ]; then
ROWS+="| Unity Cloud build (Windows) | [#${REPLY_ID}](${REPLY_URL}) |"$'\n'
elif [ -n "$REPLY_ID" ]; then
ROWS+="| Unity Cloud build (Windows) | #${REPLY_ID} |"$'\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.

Branching on REPLY_URL first means a file with a valid DASHBOARD_URL but a missing/non-numeric BUILD_ID renders [#](https://…) — an empty link label. Reachable only with a tampered artifact (build.py always writes both), which is exactly the input this block is defending against.

Suggested change
if [ -n "$REPLY_URL" ]; then
ROWS+="| Unity Cloud build (Windows) | [#${REPLY_ID}](${REPLY_URL}) |"$'\n'
elif [ -n "$REPLY_ID" ]; then
ROWS+="| Unity Cloud build (Windows) | #${REPLY_ID} |"$'\n'
fi
if [ -n "$REPLY_ID" ] && [ -n "$REPLY_URL" ]; then
ROWS+="| Unity Cloud build (Windows) | [#${REPLY_ID}](${REPLY_URL}) |"$'\n'
elif [ -n "$REPLY_ID" ]; then
ROWS+="| Unity Cloud build (Windows) | #${REPLY_ID} |"$'\n'
fi

Same in the three sibling blocks (L272-276, L376-380, L382-386).

Comment on lines +87 to +88
ARTIFACT_COUNT=$(gh api "/repos/$OWNER/$REPO/actions/runs/$RUN_ID/artifacts" \
--jq '[.artifacts[] | select(.name | startswith("Decentraland_"))] | length')
--jq '[.artifacts[] | select((.name | startswith("Decentraland_")) or (.name | startswith("unity_build_info_")))] | length')

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.

build-ran is consumed by three jobs, and widening it here changes the meaning for all of them, not just the failure path this PR is targeting:

  • comment-failed — the intended fix. ✅
  • comment-skipped (L98, build-ran == 'false') and comment-success (L121, == 'true') — these split on the same flag. comment-success then looks up Decentraland_windows64 / Decentraland_macos ids and interpolates them into download URLs. Before this change, build-ran == 'true' guaranteed at least one Decentraland_* artifact existed; now a run whose conclusion is success with only unity_build_info_* present posts "Windows and Mac build successful!" with …/artifacts/ (empty id) links.

For an honest in-repo build that combination is hard to reach, but a fork PR controls its own copy of build-unitycloud.yml and can upload an arbitrarily-named artifact, so it's reachable on demand.

Suggest emitting two outputs and keeping the success/skipped split on the narrower one:

PLAYER=$(… startswith("Decentraland_") … | length)
INFO=$(… startswith("unity_build_info_") … | length)
echo "player-artifacts=$([ "$PLAYER" -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"
echo "build-ran=$([ $((PLAYER+INFO)) -gt 0 ] && echo true || echo false)" >> "$GITHUB_OUTPUT"

…with comment-success/comment-skipped gating on player-artifacts and comment-failed on build-ran.

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.

# The info files come out of the PR-controlled build workflow, so treat them as
# untrusted input: accept only a numeric build id and a Unity dashboard URL with
# a conservative charset before letting them anywhere near a comment body.
URL_RE='^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&#-]*$'

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.

The producer now requires '/builds/' in candidate (build.py:641) but the consumer still accepts any path under the three hosts, and the charset admits ?, =, & and %. On the tampered-artifact path that leaves one narrow primitive: a fork can upload DASHBOARD_URL=https://cloud.unity.com/<something>?next=https%3A%2F%2Fevil.example, which passes validation and renders as [#123](…) in the comment — a maintainer-facing link that reads as first-party. It only goes anywhere if Unity has an open redirect, so this is speculative, not a known bug.

Cheap to close by mirroring the producer's own constraint, which also keeps the two validators in sync:

Suggested change
URL_RE='^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&#-]*$'
URL_RE='^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&#-]*/builds/[0-9]+[A-Za-z0-9./_%~?=&#-]*$'

(Worth a quick check against a real dashboard URL first — if the id segment is followed by nothing, the trailing class still matches empty.)

# uploaded as soon as the build id is known, so a build that failed before
# producing player artifacts still posts a failure comment (with the Unity
# Cloud link) instead of leaving the comment stuck on "Pending".
NAMES=$(gh api "/repos/$OWNER/$REPO/actions/runs/$RUN_ID/artifacts" \

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.

Nit, pre-existing but this PR adds two more artifacts per run: gh api without --paginate returns only the first page (30 items) of /artifacts. A full two-target run currently uploads ~10–12 artifacts, so there's headroom — but build-ran silently going false because unity_build_info_* fell off page 1 would reproduce exactly the stuck-on-Pending bug this PR is fixing, and it'd be a puzzling one to diagnose.

Suggested change
NAMES=$(gh api "/repos/$OWNER/$REPO/actions/runs/$RUN_ID/artifacts" \
NAMES=$(gh api --paginate "/repos/$OWNER/$REPO/actions/runs/$RUN_ID/artifacts?per_page=100" \

Note --paginate emits one JSON object per page, so the --jq '[.artifacts[].name]' would need --slurp or a flattening filter — ?per_page=100 alone is the smaller change if you'd rather not touch the jq.

…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.

d=$(jq -r '.duration // empty' "$file")
if [[ "$d" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then dur=$(fmt_secs "$d"); else dur="—"; fi
slow=$(jq -r --arg mode "$mode" \
'.slowest[]? | select((.seconds|type=="number") and (.name|type=="string")) | "- [\($mode)] \(.seconds)s \(.name | gsub("[\r\n]"; " "))"' \

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.

The type-check + newline flattening stops the value from breaking the heredoc or the section fence, but the name still reaches the comment as raw markdown. failed_list has always had this property; the difference is that slowest_list renders on every run, including green ones, so the channel is now always open rather than only on failures.

A fork controls test.yml and the NUnit XML, so a test named [Download the fix](https://evil.example) renders in the bot's CI status comment as a plain, first-party-looking link. GitHub's sanitizer keeps <img> too (that's how the badges in this file work), and </details> in a name closes the block early and spills the rest into the comment body. Nothing escapes the `` fence — upsert-ci-status.sh:60 only strips whole-line markers and `awk` matches whole lines — so this is presentation/phishing surface, not structural.

Wrapping the name in inline code (after stripping backticks and pipes alongside the newlines) neutralises all of it in one place:

Suggested change
'.slowest[]? | select((.seconds|type=="number") and (.name|type=="string")) | "- [\($mode)] \(.seconds)s \(.name | gsub("[\r\n]"; " "))"' \
'.slowest[]? | select((.seconds|type=="number") and (.name|type=="string")) | "- [\($mode)] \(.seconds)s `\(.name | gsub("[\r\n`|]"; " "))`"' \

Worth giving failed_list (L133) the same treatment while you're here — same input, same rendering path.

Comment on lines +830 to +833
try:
seconds = float(test_case.get("duration") or 0)
except ValueError:
seconds = 0.0

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.

float() accepts nan, inf and overflowing literals like 1e999 without raising — only malformed strings hit the ValueError branch. Any of those makes duration non-finite, and json.dump then writes bare NaN / Infinity, which is not valid JSON.

The consumer reads this file with jq under set -euo pipefail, and the very first read (jq -r '.hasResults' "$file", pr-comment-test-failures.yml:107) would abort the compose step — so one odd duration= attribute silently costs the whole tests status comment, not just the Time column. Before this commit the JSON held only ints and strings, so the failure mode is new.

Suggested change
try:
seconds = float(test_case.get("duration") or 0)
except ValueError:
seconds = 0.0
try:
seconds = float(test_case.get("duration") or 0)
except ValueError:
seconds = 0.0
if not math.isfinite(seconds):
seconds = 0.0

(needs math on the import at L808)

@@ -179,3 +206,67 @@ jobs:
commit_sha: ${{ needs.resolve.outputs.head_short_sha }}
branch_label: ${{ needs.resolve.outputs.head_ref }}
secrets: inherit

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.

Pre-existing, and I'm not counting it against this PR — but this commit adds two jobs to this file, so it's the natural moment to look at it.

secrets: inherit hands every secret on unity-explorer to a workflow resolved at @main in another repository. The header comment enumerates the six the suite actually needs (ALTTESTER_LICENSE, REPOS_READ_ONLY_TOKEN, the four DEV_EXPLORER_TEAM_*), so the blast radius is already documented — passing them explicitly costs four lines and drops the rest of the vault out of reach:

    secrets:
      ALTTESTER_LICENSE: ${{ secrets.ALTTESTER_LICENSE }}
      REPOS_READ_ONLY_TOKEN: ${{ secrets.REPOS_READ_ONLY_TOKEN }}
      # …the four DEV_EXPLORER_TEAM_* values

SHA-pinning the reusable workflow isn't the right answer here — the @main rationale in the comment above is sound, and a pin would freeze the suite. Narrowing inherit is the part that's free.

Related, on the same file: top-level permissions: contents: write (L38-40) is inherited by this call as the ceiling for the callee's GITHUB_TOKEN, and neither of the new jobs needs it — they only comment. Job-level permissions: {contents: read, pull-requests: write} on automation-pending and report would at least keep the new surface minimal, even if the top-level block has to stay for the callee.

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.

@claude

This comment has been minimized.

The build id is known before the build API returns its dashboard_summary/dashboard_log links, which left the first status-comment write with an unlinked build label. The classic dashboard log page is constructible from ORG_ID/PROJECT_ID/TARGET alone, so the live row, the build-info artifact, and the unhealthy-build message now link it immediately; the API's own deep link still replaces it on the first poll that carries one. The constructed URL passes the composite action's URL_RE (allowed origin + /builds/<digits>).
@eordano eordano added the force-build Used to trigger a build on draft PR label Aug 13, 2026
@claude

This comment has been minimized.

… page

The live-test click-through showed the classic developer.cloud.unity3d.com path does not resolve for this org; the working page is https://cloud.unity.com/home/organizations/{org}/projects/{project}/cloud-build/buildtargets/{target}/builds/{N}, and ORG_ID/PROJECT_ID already hold exactly the identifiers that page expects (verified against the live row's rendered URL). Renamed the helper to _dashboard_build_url since it now links the build page, not the log tab.
- The comment header shows the Decentraland logo instead of the traffic-light emoji, wrapped in <picture> — the one construct GitHub's renderer leaves unlinked, so clicking it no longer opens the raw image. Existing comments' headers migrate on the next section write; the decorative logo next to the build badges is dropped in favour of the header one.
- Build rows show each platform's duration: build.py rewrites the link-info file at terminal status with the queue/build split, and the composite validates the numbers and renders "⏱ 1h 12m (6m queued)" per row.
- The lint footer shows the Lint job's wall time; the tests table gains a Job time column (wall time incl. setup, from the trusted Actions API) next to the existing test-sum Time column, renamed Tests time for contrast.
@claude

This comment has been minimized.

@eordano eordano changed the title ci: link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment chore(ci): link Unity Cloud builds, test reports, timings, performance and automation from the CI status comment Aug 13, 2026
Blocker: the composite's GITHUB_OUTPUT heredoc glued the delimiter onto non-empty cell values (printf without a trailing newline), failing the step whenever a cell had content; values now emit through a guarded helper that always terminates the line.

Correctness: the survive check retries on the surviving oldest comment when its own write landed on a younger duplicate that GC will delete; reconcile probing stops after 3 consecutive confirmed checks instead of polling the comments API for the whole build; the link-info written-flag settles only on a successful write so an OSError no longer suppresses retries; the info/log artifact names take the install source from a new composite input instead of hardcoding launcher.

Presentation: duration cells read "⏱ 1h 12m build + 6m queue"; tables headed "Platform | Links & timing"; in-flight badges use the readable named yellow; the header logo carries alt text (older header spellings migrate); the tests table explains Tests time vs Job time in a footnote; the automation table links its commit/report/run rows.

Docs: workflow and action descriptions caught up with the five-section comment, the duration cell, direct script callers and body truncation.
@claude

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

…bel the perf lanes

- build.py's build flow moves under a __main__ guard (top-level if, so module-global semantics are unchanged), making the helpers importable; scripts/cloudbuild/test_build_helpers.py covers _platform_key, _dashboard_build_url (including a drift guard asserting the constructed URL passes ucb-build-links' URL_RE), and the link-info writer's href/elapsed/rewrite semantics.
- test-upsert-ci-status.sh exercises upsert-ci-status.sh against a stubbed gh: skeleton creation, section isolation, fence append, header migration, marker stripping, duplicate GC, NO_CREATE exit 3, section allowlist exit 2, and truncation re-closing constructs.
- New ci-scripts-tests.yml runs both suites on PRs touching these paths.
- Durations render with one h/m/s convention everywhere (fmt_secs gains hours and drops zero-padding; the lint footer follows suit), with lockstep notes at each copy.
- The shared performance section's two writers now label their lanes in the rendered body, so a dispatch status replacing a suite verdict is legible; the lint Pending badge joins the readable named yellow.
@claude

This comment has been minimized.

@eordano eordano added force-build Used to trigger a build on draft PR and removed force-build Used to trigger a build on draft PR labels Aug 13, 2026
Back to the plain emoji header; both picture spellings join the retired
list so existing comments migrate on their next section write.

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

This comment has been minimized.

The upsert tests still asserted the retired logo header; migration now
runs picture->emoji, so the create case asserts the emoji header and the
append case seeds a picture-headed comment and asserts it migrates.

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

This comment has been minimized.

@decentraland-bot

This comment has been minimized.

eordano and others added 2 commits August 14, 2026 09:19
- warnings: LINT_SECS degrades to no-duration on an API failure; the
  false no-set-e comment now states the real default shell; jq guards
  null started_at (also in test-failures' job_secs)
- ci-scripts-tests installs from requirements.txt (pinned requests)
- ucb-build-links: fmt_dur forces base-10 so artifact-fed 0900 can't die
  as octal
- build.py: API hrefs pass the same Unity-host regex the consumer
  enforces (shared pattern under a drift-guard test); the unhealthy-build
  message prints target+id instead of a secrets-masked URL; a failed
  first live-comment write is retried by reconcile instead of disabling
  live rows for the job
- upsert-ci-status: doc header diagram matches the emoji header; the
  truncation re-cuts until closers+note fit inside the cap; two new
  functional tests (embedded-marker wedge, stale re-read convergence)
  plus a reconcile-retry unit test
- artifact-url: randomized PLATFORM_ROWS heredoc delimiter

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Audited every token-consuming operation per job: prebuild needs
contents:read (checkout + version composite's git fetch), statuses:write
(the four createCommitStatus calls) and pull-requests:read (changed-files
REST fallback); build needs contents:read, actions:read (runs/{id}/jobs)
and pull-requests:write (status-comment CRUD via build.py); build-gate
touches no token at all. The two workflow_call callers' build jobs get
the union block so the calls keep working if the repo default token
ever tightens (a caller caps its callee).

Fork PRs already run with a read-only token regardless of these blocks;
status/comment writes there fail today and are unchanged by this.

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

This comment has been minimized.

@decentraland-bot

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

claude Bot commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

Claude finished @eordano's task in 7m 11s —— View job


Security-focused review — round 21 (e480a3ad7)

  • Diff the new commit against round-20 state (8f4123e53)
  • STEP W on the changed surface
  • Re-check the round-20 findings and the carried nits
  • STEP 1–3 dependency review
  • Post verdict

One commit, 3 files, +147/−42 — the human-reviewer must-fixes (mikhail-dcl, pravusjif, nickkhalow, dalkia) plus a note on popuz's split ask. I traced the race the commit claims to close rather than the claim, and the headline is narrower than the message says: the new post-write verify catches the mirror image of the race it documents. It's still a real improvement, and the direction it does catch now heals in seconds instead of ≤240s.

What's right

  • The constants rename is faithful. MAX_LIVE_ASSERTS / MAX_LIVE_CONFIRMS / LIVE_RECONCILE_INTERVAL_SECS (build.py:87-93) carry the same 3/3/240 values to the same three call sites at L873/L878/L880. No behaviour change, which is what a naming commit should be.
  • CR-immune, which I checked because the shell needed explicit normalization for the same bytes. GitHub returns comment bodies CRLF-normalized, but section.splitlines() treats \r\n as one boundary and strips both, so rows never carries a trailing \r; and row in post_section is a substring test, so it still matches a \r\n-terminated line on the way back. Both halves of the new verify hold without touching upsert-ci-status.sh's ${VAR//$'\r'/}.
  • No livelock between the two matrix jobs. own_row is computed once above the loop (L815), so two jobs that lose to each other on attempt 1 both compose the identical {Windows, Mac} union on attempt 2 — whoever writes second leaves a body that satisfies both verifies. Convergent, and bounded by LIVE_COMMENT_WRITE_ATTEMPTS regardless.
  • The longer block can't trip the log-stall watchdog. A call that now blocks up to ~9 min skips several size probes, but the next probe hits elif log_bytes > last_log_byte_count and resets last_log_growth before the 900s stall branch is reachable — a probe gap reads as growth, not as a stall. No spurious cancel_build.
  • The persistent-403 route that would make finding 2 unbounded is closed447ab7ad2 gave the build job pull-requests: write (build-unitycloud.yml:565-568), and fork PRs can't reach the poll loop at all (no secrets → no build id → no upsert_live_comment).
  • test_gives_up_after_exhausting_attempts is tight: [''] * (2 * ATTEMPTS) is exactly consumed, so an added read raises StopIteration rather than passing silently.

Findings

1 — LOW–MED · The post-write verify checks the opposite direction of the race the commit and the test describe.

post_section = _build_section_of_status_comment()
if post_section is not None and all(row in post_section for row in rows):

build.py:855-856

rows is this process's union. So the check covers:

  • "my row got clobbered" — a sibling write lands after our PATCH, own_row vanishes → caught, retried. ✅ Genuine gain: previously this returned True and waited out the 240s reconcile.
  • "I clobbered a row I never read" — the sibling row arrived between our read and our PATCH. It is by definition not in rows, so there is nothing for all(...) to miss. Our post-read shows {W}, rows == [own_row], verify passes, return True, and the sibling's row is gone. ❌

The second is the direction the docstring names ("a sibling platform's row arriving between this function's read and that write is invisible to it") and the direction the commit message names ("a second writer's stale union could silently drop the first writer's row"). It's still handled only by the sibling's own 240s reconcile — the same mechanism as before this commit.

The test doesn't pin what its docstring claims either. Its fixture is reads = ['', MAC_ROW, MAC_ROW, MAC+WIN] — a post-write read showing Mac only, Windows missing, i.e. our row was dropped. The inline comment narrates it as "Mac's row raced in underneath us … permanently dropping Mac's row", but in the state it asserts, Mac's row is the one that survived. Direction A is tested; direction B is described.

Residual severity is low — losing B requires the read to miss a write only seconds old, and once a sibling's row is stable any later read carries it — but the reviewer thread this closes was about B.

The root-cause shape is that the merge and the PATCH are in different processes. Passing the row and its marker down (SECTION_ROW, ROW_MARKER) and merging against CURRENT_BODY at upsert-ci-status.sh:199 puts the union inside the shell's own read→PATCH→exact-equality-confirm loop, which then covers both directions for free. Fix this →

2 — LOW–MED · The None path still moves neither counter, and this commit tripled what that costs per tick (round-19 nit 1 / round-20 finding 2).

upsert_live_comment returns None on a failed read/write or on a union that never settled, and maybe_update_live_comment advances neither _live_comment_asserts nor _live_comment_confirms on it (L887-897) — correct for the transient case, but it means nothing ever gives up. Two multipliers are new:

  • One call is now up to 3 × subprocess.run(timeout=180) = a 540s ceiling, plus 6 paginated comment reads, where it was 180s + 1 read.
  • _live_comment_last_attempt is stamped before the call (L885). A call that blocks longer than LIVE_RECONCILE_INTERVAL_SECS therefore makes the next reconcile fire back-to-back instead of 240s later — the rate limit silently stops rate-limiting exactly when the thing it guards is misbehaving.

POLL_TIME is 60s, so in that state the build poll loop is mostly comment work. A _live_comment_failures counter capped at 3, plus re-stamping the timestamp after the call, bounds both.

3 — NIT · only_if_missing short-circuits every retry, so a retry reports "already present" for a write it just made.

The check sits inside the loop (L821). In reconcile mode (only_if_missing=True), if attempt 1 writes and the verify fails on a carried row while own_row survived, attempt 2's fresh read sees our own marker and returns False — "no write was needed", for a write that just happened. maybe_update_live_comment then does _live_comment_confirms += 1 and leaves _live_comment_asserts alone, mis-scoring both budgets in opposite directions. Evaluating the short-circuit on the first iteration only keeps the three return values meaning what the docstring says.

4 — NIT · The loop retries the benign case and bails on the transient one.

if result.returncode != 0: return None (L852) leaves attempts 2 and 3 unused for the one failure that most wants them — and that failure is reachable, because round-20 finding 1 is still open: the two bare gh api … --paginate --slurp reads at upsert-ci-status.sh:168 and L253 still kill the script under set -e, so one transient list failure is the whole shell run. continue here (tracking whether any attempt wrote) makes the new budget cover it.

5 — NIT · The new env: block trades set -u protection for an empty string.

SUITE_ID: ${{ env.SUITE_ID }} (pr-comment-artifact-url.yml:306) resolves to '' when the producing step didn't set it, where the bare ${SUITE_ID} in compose_row previously aborted the step under set -euo pipefail. Not reachable today — I checked: SUITE_ID and both *_ARTIFACT_IDs come from the same step, that step has no set -e so a partial run still writes SUITE_ID first, and the suites URL is already gated on -n "$art_id" — but the guard the declaration removes was the only thing making an empty value loud. ${SUITE_ID:?}, or adding SUITE_ID to the -n gate, restores it. Separately, GITHUB_SERVER_URL / GITHUB_REPOSITORY are runner defaults, so those two lines are documentation only — worth saying so in the comment above them, since the rest of the block is load-bearing.

6 — NIT · The commit message's rationale points at a file that isn't in the repo. see FIXNOTES.md for why it's out of reach here and the concrete split to do as a follow-upgit log -- FIXNOTES.md is empty and the tree is clean, so the answer to popuz's blocking ask exists only in a local scratch file. Commit messages are permanent; the split rationale belongs in the PR description or a comment where the reviewer will actually find it.

Carried findings — all unchanged this round

Verified by grep rather than assumed; the commit message scopes this round to the human-reviewer items, so not taking them is a consistent call.

From Finding Status
r20 #1 bare gh api --paginate reads at upsert L168/L253 die under set -e open — now also caps finding 4
r20 #3 60k cap bounds failed_list only; slowest_list names unbounded in length (L157) open
r20 #4 run-suite's !cancelled() widens past resolve failing (visual-regression.yml:204-205) open
r20 #5 timed_out / stale / neutral / failure-without-artifacts still strand "New build in progress" open
r20 #6 single unretried curl -sfIL gates the Allure link (L266) open
r20 #7 ${failed_list:0:60000} can sever an inline-code pair open
r19 #2 _DASHBOARD_LINK_RE.match() admits a trailing \n; the shell twin doesn't (build.py:674) open
r19 #3 static SIZE_REPORT<<EOF beside the randomized PLATFORM_ROWS delimiter open

STEP W — workflow surface

pr-comment-artifact-url.yml is the only workflow touched, and the change adds five env: entries to one step. No new trigger, no new uses:, no permissions: change, no ${{ }} interpolated into a run: body, no prompt file. The five values are GitHub-API-derived (workflow_run.check_suite_id, artifact ids) or context literals, and step-level env: values are not shell-evaluated. Nothing in W.1–W.10 is newly triggered.

Dependency review (STEP 1–3)

git diff 8f4123e53..HEAD touches three files: one workflow, build.py, test_build_helpers.py. No Packages/manifest.json, no packages-lock.json, no .asmdef, no .dll/.so/.dylib, no requirements.txt, no install hooks. Across the whole PR, git diff origin/dev...HEAD -- Packages/ Explorer/ scripts/cloudbuild/requirements.txt '*.asmdef' '*.dll' '*.so' '*.dylib' is still empty. Nothing ships to the desktop client; STEP 1.5 registry lookups N/A.

dependency / file version source type scope risk confidence
requests (scripts/cloudbuild/requirements.txt, unchanged) ==2.32.2 PyPI source (pure Python) CI build job only LOW HIGH
(no manifest / lockfile / plugin / asmdef / binary changes anywhere in the PR) HIGH

The new-dependency label remains a labeler false positive.

Verdict

The mechanical asks — named thresholds, explicit env: — are done cleanly. The race fix delivers real value in one direction and is documented and tested for the other; that mismatch is finding 1 and it's the only thing I'd want the author to look at before merge, and even then as a comment/test correction plus an optional root-cause move of the merge into the shell. Findings 2–6 are LOW-and-below.

I still can't execute here — python3 -m unittest discover -s scripts/cloudbuild and bash .github/actions/ci-status-comment/test-upsert-ci-status.sh were both denied this round as in rounds 17–20, so everything above is from reading. ci-scripts-tests.yml runs both on any push touching scripts/cloudbuild/** or .github/actions/ci-status-comment/**, which is what keeps the three new test cases honest.

DEPENDENCY_REVIEW: NEEDS_ATTENTION — unchanged in substance from rounds 19–20. The sole item driving it is still pre-existing and already triaged: visual-regression.yml:213 calls decentraland/explorer-automation/.github/workflows/run-visual-suite.yml@main with secrets: inherit (W.7a-shaped). The author's rationale is on record and I agree with it — the callee declares no workflow_call secrets, so explicit passing is a syntax error until that repo changes first. Two-repo migration, not this PR's work. None of findings 1–6 block the merge.


· feat/unity-cloud-build-link ·

@decentraland-bot

Copy link
Copy Markdown
Contributor

PR #9713, run #32033813439

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.

Intel Core i5

Metric Baseline Change Δ Result
Samples 2701 (×3) 2271 (×3)
CPU average 33.2 ms (33.2–34.6) 39.5 ms (37.9–39.8) 6.3 ms 🔴 19% slower
CPU 1% worst 34.3 ms (33.5–184.5) 370.3 ms (350.4–453.8) 336.0 ms 🔴 981% slower
CPU 0.1% worst 41.4 ms (33.7–332.0) 389.1 ms (368.0–476.5) 347.7 ms 🔴 840% slower
GPU average 9.3 ms (9.2–9.4) 9.6 ms (9.4–9.7) 0.3 ms 🔴 3% slower
GPU 1% worst 20.7 ms (19.8–26.9) 39.1 ms (36.3–47.3) 18.4 ms 🔴 89% slower
GPU 0.1% worst 36.3 ms (31.6–37.7) 46.4 ms (43.6–55.2) 10.0 ms ⚪ within noise
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

Apple M1

Metric Baseline Change Δ Result
Samples 4368 (×3) 4030 (×3)
CPU average 20.5 ms (20.3–21.6) 22.2 ms (22.2–22.7) 1.7 ms 🔴 8% slower
CPU 1% worst 34.7 ms (33.9–34.7) 228.7 ms (219.2–234.5) 194.0 ms 🔴 559% slower
CPU 0.1% worst 34.9 ms (34.9–35.3) 239.8 ms (239.6–240.5) 204.8 ms 🔴 586% slower
GPU average 1.0 ms (0.1–1.6) 6.9 ms (6.8–10.5) 6.0 ms 🔴 616% slower
GPU 1% worst 34.2 ms (7.7–34.8) 35.3 ms (34.8–35.6) 1.1 ms ⚪ within noise
GPU 0.1% worst 35.9 ms (35.1–37.2) 36.4 ms (36.3–36.6) 0.5 ms ⚪ within noise
Exceptions per run 0 0 0 ⚪ none new

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.

2 participants