Skip to content

Commit a3aea13

Browse files
authored
Merge pull request #9311 from decentraland/release/2026-07-06
release: 2026-07-06
2 parents 356807b + 3fd446c commit a3aea13

78 files changed

Lines changed: 1154 additions & 358 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/auto_assign_config_dev.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@ reviewGroups:
1717
- pravusjif
1818
- NickKhalow
1919
- lorenzo-ranciaffi
20-
- biotech77
21-
- sandrade-dcl
22-
- Maurizio-dcl
2320

2421
filterLabels:
2522
exclude:

.github/workflows/build-release-main.yml

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,10 @@ jobs:
3131
echo "" >> $GITHUB_STEP_SUMMARY
3232
echo "> ⚠️ If some artefacts are missing please take a look at previous attempts" >> $GITHUB_STEP_SUMMARY
3333
34-
# Run a clean build (no cache)
34+
# Enable cache reuse for release builds: cache_strategy=library turns on the remote
35+
# library/shader cache and clean_build=false lets the build consume it. The shared,
36+
# dev-isolated release/hotfix/main cache pool (cold genesis) is wired in build.py's
37+
# clone_current_target.
3538
build:
3639
name: Build Unity Cloud
3740
needs: get-info
@@ -41,8 +44,8 @@ jobs:
4144
uses: ./.github/workflows/build-unitycloud.yml
4245
with:
4346
profile: none
44-
clean_build: true
45-
cache_strategy: none
47+
clean_build: false
48+
cache_strategy: library
4649
version: ${{ needs.get-info.outputs.version }}
4750
sentry_enabled: true
4851
is_release_build: true

.github/workflows/claude-pr-review.yml

Lines changed: 151 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,79 @@ on:
66
pull_request_review_comment:
77
types: [created]
88
pull_request:
9-
types: [opened, synchronize, ready_for_review]
9+
types: [opened, synchronize, ready_for_review, labeled]
1010

1111
jobs:
12-
auto-review:
12+
check-author:
1313
if: |
1414
github.event_name == 'pull_request' &&
1515
github.event.pull_request.draft == false &&
16+
github.event.pull_request.user.login != 'github-actions[bot]' &&
17+
!startsWith(github.head_ref, 'release/') &&
1618
!contains(github.event.pull_request.labels.*.name, 'no review') &&
17-
!contains(github.event.pull_request.labels.*.name, 'auto-pr')
19+
!contains(github.event.pull_request.labels.*.name, 'auto-pr') &&
20+
(github.event.action != 'labeled' || github.event.label.name == 'ext-contribution')
21+
runs-on: ubuntu-latest
22+
permissions:
23+
contents: read
24+
pull-requests: write
25+
outputs:
26+
is-team-dev: ${{ steps.check.outputs.is-team-dev }}
27+
steps:
28+
- name: Check for ext-contribution label
29+
id: label-check
30+
run: |
31+
if [[ "${{ contains(github.event.pull_request.labels.*.name, 'ext-contribution') }}" == "true" ]]; then
32+
echo "has-label=true" >> "$GITHUB_OUTPUT"
33+
else
34+
echo "has-label=false" >> "$GITHUB_OUTPUT"
35+
fi
36+
37+
- name: Checkout config file
38+
if: steps.label-check.outputs.has-label != 'true'
39+
uses: actions/checkout@v6
40+
with:
41+
ref: ${{ github.event.pull_request.base.sha }}
42+
sparse-checkout: .github/auto_assign_config_dev.yml
43+
fetch-depth: 1
44+
45+
- name: Check if PR author is a team dev
46+
id: check
47+
env:
48+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
49+
run: |
50+
if [[ "${{ steps.label-check.outputs.has-label }}" == "true" ]]; then
51+
echo "Author $PR_AUTHOR has ext-contribution label — treating as external"
52+
echo "is-team-dev=false" >> "$GITHUB_OUTPUT"
53+
exit 0
54+
fi
55+
TEAM_DEVS=$(yq eval '.reviewGroups | .[] | .[]' .github/auto_assign_config_dev.yml)
56+
echo "Team devs:"
57+
echo "$TEAM_DEVS"
58+
if echo "$TEAM_DEVS" | grep -qxF "$PR_AUTHOR"; then
59+
echo "Author $PR_AUTHOR is a team dev"
60+
echo "is-team-dev=true" >> "$GITHUB_OUTPUT"
61+
else
62+
echo "Author $PR_AUTHOR is NOT a team dev"
63+
echo "is-team-dev=false" >> "$GITHUB_OUTPUT"
64+
fi
65+
66+
- name: Add ext-contribution label for non-team PRs
67+
if: |
68+
steps.check.outputs.is-team-dev == 'false' &&
69+
steps.label-check.outputs.has-label != 'true' &&
70+
(github.event.action == 'opened' || github.event.action == 'ready_for_review')
71+
env:
72+
GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
73+
run: |
74+
gh pr edit ${{ github.event.pull_request.number }} --add-label ext-contribution --repo ${{ github.repository }} || echo "::warning::Could not add label (expected for fork PRs)"
75+
echo "Added ext-contribution label to PR #${{ github.event.pull_request.number }}"
76+
77+
auto-review:
78+
needs: check-author
79+
if: |
80+
needs.check-author.result == 'success' &&
81+
needs.check-author.outputs.is-team-dev == 'true'
1882
runs-on: ubuntu-latest
1983
concurrency:
2084
group: claude-auto-review-${{ github.event.pull_request.number }}
@@ -296,6 +360,90 @@ jobs:
296360
gh run rerun "$WORKFLOW_RUN_ID" --repo "${{ github.repository }}"
297361
echo "Re-run triggered successfully."
298362
363+
notify-ext-contribution:
364+
needs: check-author
365+
if: |
366+
needs.check-author.result == 'success' &&
367+
needs.check-author.outputs.is-team-dev == 'false' &&
368+
github.event.action == 'labeled' &&
369+
github.event.label.name == 'ext-contribution'
370+
runs-on: ubuntu-latest
371+
permissions:
372+
pull-requests: write
373+
issues: write
374+
steps:
375+
- name: Check for existing Slack notification marker
376+
id: dedup
377+
env:
378+
GH_TOKEN: ${{ github.token }}
379+
run: |
380+
MARKER="<!-- ext-contribution-slack-sent -->"
381+
COMMENTS=$(gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments?per_page=100" --jq '.[].body')
382+
if echo "$COMMENTS" | grep -qF "$MARKER"; then
383+
echo "Slack notification already sent for this PR — skipping"
384+
echo "already-sent=true" >> "$GITHUB_OUTPUT"
385+
else
386+
echo "already-sent=false" >> "$GITHUB_OUTPUT"
387+
fi
388+
389+
- name: Verify PR author is a decentraland org member
390+
if: steps.dedup.outputs.already-sent != 'true'
391+
id: check-org
392+
env:
393+
GH_TOKEN: ${{ secrets.ORG_ACCESS_TOKEN }}
394+
PR_AUTHOR: ${{ github.event.pull_request.user.login }}
395+
run: |
396+
if gh api "/orgs/decentraland/members/$PR_AUTHOR" --silent 2>/dev/null; then
397+
echo "is-member=true" >> "$GITHUB_OUTPUT"
398+
else
399+
echo "::notice::PR author $PR_AUTHOR is not a member of decentraland org; Slack notification skipped"
400+
echo "is-member=false" >> "$GITHUB_OUTPUT"
401+
fi
402+
403+
- name: Extract Slack requester tag from PR description
404+
if: steps.dedup.outputs.already-sent != 'true' && steps.check-org.outputs.is-member == 'true'
405+
id: extract-slack-tag
406+
env:
407+
PR_BODY: ${{ github.event.pull_request.body }}
408+
run: |
409+
SLACK_TAG=$(echo "$PR_BODY" | grep -oP '(?<=Requested by ).*?(\(<@U[A-Z0-9]+>\))' | grep -oP '<@U[A-Z0-9]+>' | head -1)
410+
if [ -n "$SLACK_TAG" ]; then
411+
echo "Found Slack tag: $SLACK_TAG"
412+
echo "slack-tag=$SLACK_TAG" >> "$GITHUB_OUTPUT"
413+
else
414+
echo "No Slack tag found in PR description, will use GitHub username"
415+
echo "slack-tag=" >> "$GITHUB_OUTPUT"
416+
fi
417+
418+
- name: Post to Slack #explorer-ext-contributions
419+
if: steps.dedup.outputs.already-sent != 'true' && steps.check-org.outputs.is-member == 'true'
420+
env:
421+
SLACK_WEBHOOK_URL: ${{ secrets.EXPLORER_EXT_CONTRIBUTIONS_WEBHOOK }}
422+
PR_URL: ${{ github.event.pull_request.html_url }}
423+
PR_AUTHOR_LOGIN: ${{ github.event.pull_request.user.login }}
424+
SLACK_TAG: ${{ steps.extract-slack-tag.outputs.slack-tag }}
425+
run: |
426+
if [ -n "$SLACK_TAG" ]; then
427+
AUTHOR_REF="$SLACK_TAG"
428+
else
429+
AUTHOR_REF="$PR_AUTHOR_LOGIN"
430+
fi
431+
PAYLOAD=$(jq -n --arg url "$PR_URL" --arg author "$AUTHOR_REF" \
432+
'{"text": "<@U0AJ91P1KBP> review \($url) (initiated by \($author))"}')
433+
curl -fsS -X POST "$SLACK_WEBHOOK_URL" \
434+
-H 'Content-Type: application/json' \
435+
-d "$PAYLOAD"
436+
437+
- name: Post dedup marker comment
438+
if: steps.dedup.outputs.already-sent != 'true' && steps.check-org.outputs.is-member == 'true'
439+
env:
440+
GH_TOKEN: ${{ github.token }}
441+
run: |
442+
gh api "repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/comments" \
443+
-f body="<!-- ext-contribution-slack-sent -->
444+
Slack notification sent to #explorer-ext-contributions for external review.
445+
To re-send, delete this comment and re-add the \`ext-contribution\` label."
446+
299447
check-member:
300448
if: |
301449
github.event.issue.pull_request != null &&

.github/workflows/pr-comment-warnings.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,19 @@ jobs:
5656
fi
5757
count=$(jq -r '.count' warning-result.json)
5858
baseline=$(jq -r '.baseline // ""' warning-result.json)
59+
allow_equal=$(jq -r '.allow_equal // false' warning-result.json)
5960
echo "found=true" >> "$GITHUB_OUTPUT"
6061
echo "count=$count" >> "$GITHUB_OUTPUT"
6162
echo "baseline=$baseline" >> "$GITHUB_OUTPUT"
63+
echo "allow-equal=$allow_equal" >> "$GITHUB_OUTPUT"
6264
6365
- name: Compose comment body
6466
if: steps.result.outputs.found == 'true'
6567
id: body
6668
env:
6769
COUNT: ${{ steps.result.outputs.count }}
6870
BASELINE: ${{ steps.result.outputs.baseline }}
71+
ALLOW_EQUAL: ${{ steps.result.outputs.allow-equal }}
6972
run: |
7073
{
7174
echo "body<<EOF"
@@ -74,6 +77,8 @@ jobs:
7477
echo "**Warnings counted: $COUNT** (no baseline established yet)"
7578
elif [ "$COUNT" -lt "$BASELINE" ]; then
7679
echo "**Warnings count reduced: $BASELINE => $COUNT**"
80+
elif [ "$ALLOW_EQUAL" = "true" ] && [ "$COUNT" -eq "$BASELINE" ]; then
81+
echo "**Warnings unchanged: $BASELINE => $COUNT** — allowed on release/hotfix branches."
7782
else
7883
echo "**Warnings not reduced: $BASELINE => $COUNT** — remove at least one warning to merge."
7984
fi

.github/workflows/test.yml

Lines changed: 72 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,12 @@ jobs:
8787

8888
name: Lint
8989
runs-on: ubuntu-latest
90+
# PRs from or into a release/hotfix branch may merge with an unchanged warning count
91+
# (equality passes the ratchet below); the dev line still requires a strict reduction.
92+
env:
93+
IS_RELEASE_OR_HOTFIX: ${{ startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/') || startsWith(github.base_ref, 'release/') || startsWith(github.base_ref, 'hotfix/') }}
9094
steps:
91-
95+
9296
- name: Checkout CI actions
9397
uses: actions/checkout@v6
9498
with:
@@ -259,17 +263,73 @@ jobs:
259263
- name: Download ReSharper Command Line Tools
260264
run: bash scripts/lint/download-resharper.sh rsharp
261265

262-
- name: Run ReSharper InspectCode (static analysis)
263-
run: bash scripts/lint/run-inspectcode.sh Explorer/Explorer.sln "$INSPECT_REPORT_JSON_FILE_NAME"
264-
265-
# Filter false positives ('.CSharpErrors'/'.CppCompilerErrors' from '--no-build') and count.
266+
# The ReSharper CLI needs a .NET runtime, which the Unity image lacks.
267+
# Derive an image with the SDK (runtime + MSBuild for project-model evaluation)
268+
- name: Build lint image with .NET SDK
269+
id: lint-dotnet-img
270+
run: |
271+
base="${{ steps.img-lint.outputs.ghcr }}"
272+
derived="${base}-dotnet"
273+
cat > /tmp/Dockerfile.dotnet <<EOF
274+
FROM $base
275+
RUN apt-get update \
276+
&& apt-get install -y --no-install-recommends dotnet-sdk-8.0 \
277+
&& rm -rf /var/lib/apt/lists/*
278+
EOF
279+
docker build -t "$derived" -f /tmp/Dockerfile.dotnet /tmp
280+
echo "image=$derived" >> "$GITHUB_OUTPUT"
281+
282+
# The solution generated by the GameCI step above bakes in absolute paths only valid there ('/opt/unity/...', and the
283+
# workspace as '/github/workspace' — the exact path GameCI mounts it at, so the -v
284+
# target below must match it). Running on the host leaves those references dangling
285+
# and ReSharper resolves them heuristically per-run
286+
- name: Run ReSharper InspectCode (inside Unity container)
287+
run: |
288+
docker run --rm \
289+
-v "${{ github.workspace }}:/github/workspace" \
290+
-w /github/workspace \
291+
-e RSHARP_HOME=/github/workspace/rsharp \
292+
-e HOME=/tmp \
293+
"${{ steps.lint-dotnet-img.outputs.image }}" \
294+
bash scripts/lint/run-inspectcode.sh Explorer/Explorer.sln "$INSPECT_REPORT_JSON_FILE_NAME"
295+
296+
# Filter false positives ('.CSharpErrors'/'.CppCompilerErrors', residual unresolved-ref noise) and count.
266297
- name: Count warnings
267298
id: warnings
268299
uses: ./.github/actions/count-warnings
269300
with:
270301
report: ${{ env.INSPECT_REPORT_JSON_FILE_NAME }}
271302
filtered_output: filtered.json
272303

304+
# With full in-container resolution, C# compiler-level diagnostics should be near
305+
# zero (baseline: 11 stable Google.Protobuf Span-overload quirks). A spike means
306+
# references stopped resolving (image change, GameCI mount path change) and the
307+
# warning count is measuring environment breakage, not code - fail loudly instead
308+
# of letting filter-warnings.sh discard the evidence. Only '.CSharpErrors' counts:
309+
# '.CppCompilerErrors' is ReSharper's shader/C++ analyzer noise (~500 in .hlsl/
310+
# .cginc files), unrelated to C# resolution health.
311+
- name: Assert reference resolution is healthy
312+
run: |
313+
set -euo pipefail
314+
errs=$(jq '[.runs[0].results[] | select(.ruleId == ".CSharpErrors")] | length' "$INSPECT_REPORT_JSON_FILE_NAME")
315+
echo "C# compiler-error diagnostics in report: $errs (filtered warning count: ${{ steps.warnings.outputs.count }})"
316+
if [ "$errs" -gt 20 ]; then
317+
echo "::error::$errs C# compiler-level errors in the InspectCode report - references are not resolving, the warning count cannot be trusted."
318+
exit 1
319+
fi
320+
321+
# Keep the raw and filtered reports inspectable for investigation purposes in a case of failure from "Assert reference resolution is healthy"
322+
- name: Upload InspectCode reports
323+
if: ${{ !cancelled() }}
324+
uses: actions/upload-artifact@v4
325+
with:
326+
name: inspect-code-reports
327+
path: |
328+
${{ env.INSPECT_REPORT_JSON_FILE_NAME }}
329+
filtered.json
330+
if-no-files-found: ignore
331+
retention-days: 3
332+
273333
- name: Read warnings baseline from S3
274334
id: baseline
275335
env:
@@ -285,6 +345,7 @@ jobs:
285345
echo "Baseline warnings: ${baseline:-<none>}"
286346
287347
# Hard gate: a PR may only merge if it strictly reduces the warning count.
348+
# Exception: release/hotfix PRs may merge when the count is unchanged (equality passes).
288349
# No baseline yet -> pass (the first dev push seeds it). Add the 'no-warning-ratchet'
289350
# label to bypass on a PR that legitimately cannot touch warnings.
290351
- name: Enforce warning reduction (PR)
@@ -302,6 +363,10 @@ jobs:
302363
echo "Warnings reduced: $COUNT < $BASELINE"
303364
exit 0
304365
fi
366+
if [ "$IS_RELEASE_OR_HOTFIX" = "true" ] && [ "$COUNT" -eq "$BASELINE" ]; then
367+
echo "Warnings unchanged ($COUNT == $BASELINE) on a release/hotfix branch - passing."
368+
exit 0
369+
fi
305370
if [ "$BYPASS" = "true" ]; then
306371
echo "::warning::Warnings not reduced ($COUNT >= $BASELINE) but 'no-warning-ratchet' label present - bypassing."
307372
exit 0
@@ -350,7 +415,8 @@ jobs:
350415
--argjson pr "${{ github.event.pull_request.number }}" \
351416
--argjson count "$COUNT" \
352417
--arg baseline "$BASELINE" \
353-
'{pr: $pr, count: $count, baseline: (if $baseline == "" then null else ($baseline | tonumber) end)}' \
418+
--argjson allow_equal "$IS_RELEASE_OR_HOTFIX" \
419+
'{pr: $pr, count: $count, baseline: (if $baseline == "" then null else ($baseline | tonumber) end), allow_equal: $allow_equal}' \
354420
> warning-result.json
355421
cat warning-result.json
356422

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ A `Stop` hook (`.claude/settings.json` → [`scripts/lint/lint-changed.sh`](scri
103103
* Ignore `OperationCanceledException`
104104
* Log/report all others via `ReportHub.LogException`
105105
* Use `SuppressToResultAsync()` to simplify exception handling.
106-
* Handle cancellation with `ct.IsCancellationRequested`, never `ThrowIfCancellationRequested()`.
106+
* In exception-free flows (detached UniTask, `.Forget()`, fire-and-forget, returning boolean, returning Result or Result<T>), handle cancellation with `ct.IsCancellationRequested` — do not use `ThrowIfCancellationRequested()`. In flows that already handle exceptions (awaited async methods, `SuppressToResultAsync`), `ThrowIfCancellationRequested()` is acceptable.
107107

108108
### 10. **Testing Systems**
109109

Explorer/Assets/DCL/AuthenticationScreenFlow/States/IdentityVerificationOTPAuthState.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ public override void Exit()
7575
Exception ex => new SpanErrorInfo("Unexpected error during authentication flow", ex),
7676
};
7777

78-
if (loginException is not OperationCanceledException)
78+
if (loginException is not OperationCanceledException && loginException is not InvalidEmailException)
7979
ReportHub.LogException(loginException, new ReportData(ReportCategory.AUTHENTICATION));
8080
}
8181

@@ -100,7 +100,7 @@ private async UniTaskVoid AuthenticateAsync(string email, CancellationToken ct)
100100

101101
// awaits OTP code being entered
102102
IWeb3Identity identity = await compositeWeb3Provider.LoginAsync(LoginPayload.ForOtpFlow(email), ct);
103-
machine.Enter<ProfileFetchingAuthState, ProfileFetchingPayload>(new (email, identity, false, ct));
103+
machine.Enter<ProfileFetchingAuthState, ProfileFetchingPayload>(new ProfileFetchingPayload(email, identity, false, ct));
104104
}
105105
catch (OperationCanceledException e)
106106
{

0 commit comments

Comments
 (0)