Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
00ea5fd
ci: deep-link the Unity Cloud build page from CI and the PR status co…
eordano Aug 12, 2026
68875b9
ci: address security review findings on Unity Cloud build links
eordano Aug 13, 2026
26cb13e
ci: turn the CI status comment into a link hub (jobs, reports, timing…
eordano Aug 13, 2026
8ee55d7
ci: add a performance section to the CI status comment
eordano Aug 13, 2026
3b856eb
ci: surface a failed performance-test dispatch in the PR status comment
eordano Aug 13, 2026
94e21fc
ci: warn about PERFORMANCE_TESTING_PAT expiry inside the CI status co…
eordano Aug 13, 2026
34aad87
ci: let external callers write CI status sections (file body, no-create)
eordano Aug 13, 2026
29b7e7b
ci: address review findings across the status-comment pipeline
eordano Aug 13, 2026
1c93440
ci: clamp the duration accumulator and truncate section bodies struct…
eordano Aug 13, 2026
414b0f3
ci: collapse the build table to one row per platform and link Unity C…
eordano Aug 13, 2026
b5186df
ci: derive the live row's platform from TARGET's stable prefix
eordano Aug 13, 2026
4eb8b84
ci: drop the live-build intro line and stop #N autolinking to issues
eordano Aug 13, 2026
53dc7e3
ci: link the Unity Cloud build log page before the API deep link arrives
eordano Aug 13, 2026
23e9577
ci: point the constructed dashboard link at the cloud.unity.com build…
eordano Aug 13, 2026
a94f73d
ci: DCL logo header, and durations for builds, lint and tests
eordano Aug 13, 2026
0532ad4
ci: review-round fixes across the status-comment pipeline
eordano Aug 13, 2026
5d2384c
ci: commit tests for the status-comment plumbing, unify durations, la…
eordano Aug 13, 2026
63a0cf4
ci: drop the logo from the CI status comment header
eordano Aug 13, 2026
a9f8e93
test: follow the header back to the emoji spelling
eordano Aug 13, 2026
536e961
ci: close review findings 2-13 across the status-comment pipeline
eordano Aug 14, 2026
447ab7a
ci: give every Unity Cloud Build job a least-privilege permissions block
eordano Aug 14, 2026
8f4123e
ci: close the verified review findings across the status-comment pipe…
eordano Aug 16, 2026
e480a3a
fix: close review must-fix items on the Unity Cloud build-link PR
eordano Aug 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .github/workflows/build-unitycloud.yml
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,17 @@ jobs:
path: unity_cloud_log.log
if-no-files-found: error

# Written by build.py as soon as the Unity-side build id is known, so it exists for
# failed builds too. The PR status comment uses it to deep-link the Unity Cloud
# build page instead of asking humans to search cloud.unity.com by hand.
- name: Upload Unity Cloud build info
if: ${{ always() && hashFiles('unity_cloud_build_info.env') != '' }}
uses: actions/upload-artifact@v6
with:
name: unity_build_info_${{ matrix.target }}_${{ needs.prebuild.outputs.install_source }}
path: unity_cloud_build_info.env
if-no-files-found: error

- name: Print cloud logs
if: ${{ always() && hashFiles('unity_cloud_log.log') != '' }}
run: cat unity_cloud_log.log
Expand Down
122 changes: 115 additions & 7 deletions .github/workflows/pr-comment-artifact-url.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ jobs:
section: build
github-token: ${{ github.token }}
body: |-
![Build](https://img.shields.io/badge/Build-Pending!-ffff00?logo=github&style=for-the-badge) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
[![Build](https://img.shields.io/badge/Build-Pending!-ffff00?logo=github&style=for-the-badge)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">

New build in progress, come back later!

Expand All @@ -80,9 +80,12 @@ jobs:
REPO: ${{ github.event.repository.name }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
# Check if any build artifacts exist (they only exist when Build jobs ran)
# Check if any build artifacts exist (they only exist when Build jobs ran).
# unity_build_info_* is uploaded as soon as the Unity-side build starts, so a
# build that failed before producing player artifacts still counts as "ran"
# and gets a failure comment with a Unity Cloud link instead of staying pending.
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.

echo "Build artifact count: $ARTIFACT_COUNT"
if [ "$ARTIFACT_COUNT" -gt 0 ]; then
echo "build-ran=true" >> "$GITHUB_OUTPUT"
Expand All @@ -109,7 +112,7 @@ jobs:
section: build
github-token: ${{ github.token }}
body: |-
![Build](https://img.shields.io/badge/Build-Skipped-yellow?logo=unity&logoColor=white&style=for-the-badge) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
[![Build](https://img.shields.io/badge/Build-Skipped-yellow?logo=unity&logoColor=white&style=for-the-badge)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">

Build skipped — no changes detected under `Explorer/`.

Expand Down Expand Up @@ -230,14 +233,62 @@ jobs:
echo "SIZE_REPORT=" >> "$GITHUB_ENV"
fi

- name: Fetch Unity Cloud build links
env:
GITHUB_TOKEN: ${{ github.token }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
run: |
set -euo pipefail

# 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 the comment body.
URL_RE='^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&-]*$'
parse_info() {
local target="$1"
local dir="ucb_info_${target}"
REPLY_ID=""
REPLY_URL=""
if gh run download "$PREVIOUS_JOB_ID" \
--repo "$OWNER/$REPO" \
--name "unity_build_info_${target}_launcher" \
--dir "$dir" 2>/dev/null; then
REPLY_ID=$(grep -m1 '^BUILD_ID=' "$dir/unity_cloud_build_info.env" | cut -d= -f2- || true)
REPLY_URL=$(grep -m1 '^DASHBOARD_URL=' "$dir/unity_cloud_build_info.env" | cut -d= -f2- || true)
[[ "$REPLY_ID" =~ ^[0-9]+$ ]] || REPLY_ID=""
[[ "$REPLY_URL" =~ $URL_RE ]] || REPLY_URL=""
fi
}

ROWS=""
parse_info windows64
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).

parse_info macos
if [ -n "$REPLY_URL" ]; then
ROWS+="| Unity Cloud build (Mac) | [#${REPLY_ID}](${REPLY_URL}) |"$'\n'
elif [ -n "$REPLY_ID" ]; then
ROWS+="| Unity Cloud build (Mac) | #${REPLY_ID} |"$'\n'
fi

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

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


- name: Update build section
uses: ./.github/actions/ci-status-comment
with:
pr-number: ${{ needs.pre-validation.outputs.pr-number }}
section: build
github-token: ${{ github.token }}
body: |-
![Build](https://img.shields.io/badge/Build-Success!-3fb950?logo=unity&logoColor=white&style=for-the-badge) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
[![Build](https://img.shields.io/badge/Build-Success!-3fb950?logo=unity&logoColor=white&style=for-the-badge)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ env.PREVIOUS_JOB_ID }}) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">

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

Expand All @@ -250,6 +301,7 @@ jobs:
| Download Mac | ${{ github.server_url }}/${{ github.repository }}/suites/${{ env.SUITE_ID }}/artifacts/${{ env.MAC_ARTIFACT_ID }} |
| Download Mac S3 | ${{ format('{0}/{1}/Decentraland_macos.zip', vars.EXPLORER_TEAM_S3_BUCKET_PUBLIC_URL, env.ARTIFACT_S3_DESTINATION_PATH) }} |
| Built on | ${{ env.BUILD_DATE }} |
${{ env.UCB_ROWS }}

${{ env.SIZE_REPORT }}

Expand Down Expand Up @@ -290,14 +342,70 @@ jobs:
sparse-checkout-cone-mode: false
persist-credentials: false

- name: Fetch Unity Cloud build links
env:
GITHUB_TOKEN: ${{ github.token }}
OWNER: ${{ github.repository_owner }}
REPO: ${{ github.event.repository.name }}
RUN_ID: ${{ github.event.workflow_run.id }}
run: |
set -euo pipefail

# 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 the comment body.
URL_RE='^https://(cloud\.unity\.com|developer\.cloud\.unity3d\.com|dashboard\.unity3d\.com)/[A-Za-z0-9./_%~?=&-]*$'
parse_info() {
local target="$1"
local dir="ucb_info_${target}"
REPLY_ID=""
REPLY_URL=""
if gh run download "$RUN_ID" \
--repo "$OWNER/$REPO" \
--name "unity_build_info_${target}_launcher" \
--dir "$dir" 2>/dev/null; then
REPLY_ID=$(grep -m1 '^BUILD_ID=' "$dir/unity_cloud_build_info.env" | cut -d= -f2- || true)
REPLY_URL=$(grep -m1 '^DASHBOARD_URL=' "$dir/unity_cloud_build_info.env" | cut -d= -f2- || true)
[[ "$REPLY_ID" =~ ^[0-9]+$ ]] || REPLY_ID=""
[[ "$REPLY_URL" =~ $URL_RE ]] || REPLY_URL=""
fi
}

ROWS=""
parse_info windows64
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
parse_info macos
if [ -n "$REPLY_URL" ]; then
ROWS+="| Unity Cloud build (Mac) | [#${REPLY_ID}](${REPLY_URL}) |"$'\n'
elif [ -n "$REPLY_ID" ]; then
ROWS+="| Unity Cloud build (Mac) | #${REPLY_ID} |"$'\n'
fi

SECTION=""
if [ -n "$ROWS" ]; then
SECTION="| Name | Link |"$'\n'"| -------- | ----------------------- |"$'\n'"$ROWS"
fi

{
echo "UCB_SECTION<<UCB_SECTION_EOF"
printf '%s' "$SECTION"
echo "UCB_SECTION_EOF"
} >> "$GITHUB_ENV"

- name: Update build section
uses: ./.github/actions/ci-status-comment
with:
pr-number: ${{ needs.pre-validation.outputs.pr-number }}
section: build
github-token: ${{ github.token }}
body: |-
![Build](https://img.shields.io/badge/Build-Failed!-ff0000?logo=unity&logoColor=white&style=for-the-badge) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">
[![Build](https://img.shields.io/badge/Build-Failed!-ff0000?logo=unity&logoColor=white&style=for-the-badge)](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) <img src="https://ui.decentraland.org/decentraland_256x256.png" width="30">

Build failed! Check the logs to see what went wrong.
Build failed! Check the [logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }}) to see what went wrong.
If the error repeats please consider the `clean-build` tag.

${{ env.UCB_SECTION }}
53 changes: 52 additions & 1 deletion scripts/cloudbuild/build.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ def _extract_member(self, member, targetpath, pwd):

build_healthy = True

# Deep link to this build in the Unity Cloud dashboard, captured from the first build
# response that carries one. Persisted to BUILD_LINK_INFO_PATH so the workflow can
# upload it and the PR status comment can link the build directly.
BUILD_LINK_INFO_PATH = 'unity_cloud_build_info.env'
dashboard_url = None
_build_link_info_written = False

parser = argparse.ArgumentParser()
parser.add_argument('--resume', help='Resume tracking a running build stored in build_info.json', action='store_true')
parser.add_argument('--cancel', help='Cancel a running build stored in build_info.json', action='store_true')
Expand Down Expand Up @@ -616,6 +623,44 @@ def try_resume_build():
return None


def record_build_link_info(id, response_json):
"""Persist the Unity Cloud dashboard deep link for this build (best-effort).

Build API responses carry dashboard links; the workflow uploads the written file
as an artifact so the PR status comment can link the build id directly instead
of telling humans to search cloud.unity.com by hand.
"""
global dashboard_url, _build_link_info_written

links = response_json.get('links') or {}
href = None
# dashboard_summary is the build's page and dashboard_log its log tab;
# dashboard_url can be just the dashboard root, so a candidate only
# 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:

href = candidate
break

if _build_link_info_written and not href:
return

try:
with open(BUILD_LINK_INFO_PATH, 'w') as f:
f.write(f'BUILD_TARGET={os.getenv("TARGET")}\n')
f.write(f'BUILD_ID={id}\n')
if href:
f.write(f'DASHBOARD_URL={href}\n')
except OSError as e:
print(f'Warning: could not write {BUILD_LINK_INFO_PATH}: {e}')

if href:
dashboard_url = href
print(f'::notice::Unity Cloud build #{id} ({os.getenv("TARGET")}): {href}')
_build_link_info_written = True


def write_step_summary(target, build_id, final_status, phase_durations, queue_reasons, queue_elapsed, build_elapsed):
"""Append a phase breakdown to $GITHUB_STEP_SUMMARY (best-effort)."""
summary_path = os.environ.get('GITHUB_STEP_SUMMARY')
Expand All @@ -635,6 +680,8 @@ def fmt(seconds):
lines.append('')
lines.append(f'- Target: `{target}`')
lines.append(f'- Build ID: `{build_id}`')
if dashboard_url:
lines.append(f'- Unity Cloud build page: {dashboard_url}')
lines.append(f'- Final outcome: `{final_status}`')
if queue_reasons:
lines.append(f"- Queue reasons seen: {', '.join(f'`{r}`' for r in sorted(queue_reasons))}")
Expand Down Expand Up @@ -711,6 +758,9 @@ def run_poll_loop(id, build_already_active=False, resumed_build_elapsed=0):

keep_polling, status, response_json = poll_build(id)

if dashboard_url is None:
record_build_link_info(id, response_json)

queued_reason = response_json.get('queuedReason')
if queued_reason and status in QUEUE_STATUSES:
queue_reasons.add(queued_reason)
Expand Down Expand Up @@ -920,7 +970,8 @@ def probe_latest_build():
download_log(id)

if not build_healthy:
print(f'Build unhealthy - check the downloaded logs or go to https://cloud.unity.com/ and search for target "{os.getenv('TARGET')}" and build ID "{id}"')
where = dashboard_url or f'https://cloud.unity.com/ (search for target "{os.getenv("TARGET")}" and build ID "{id}")'
print(f'Build unhealthy - check the downloaded logs or the Unity Cloud build page: {where}')
sys.exit(1)

# Cleanup (only if build is healthy and not release)
Expand Down
Loading