Skip to content

release: 2026-08-17 - #9771

Open
github-actions[bot] wants to merge 15 commits into
mainfrom
release/2026-08-17
Open

release: 2026-08-17#9771
github-actions[bot] wants to merge 15 commits into
mainfrom
release/2026-08-17

Conversation

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Automated release PR

mikhail-dcl and others added 15 commits August 13, 2026 12:28
…9699)

The type merged as AltTesterFeatureFlagsProbe; the docs still said Alttester...,
which is a working-looking copy-paste that resolves to componentNotFound because
Assembly.GetType is case-sensitive. Records that failure mode too.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…9705 #9706) (#9707)

* opti(avatar-rendering): bound bone-matrix job by each avatar's actual bone count

BoneMatrixCalculationJob recomputed all MAX_BONE_COUNT matrices per avatar every frame regardless of how many ComputeSkinning actually uploads; refresh and pass through the authoritative per-avatar count so the job only computes the uploaded range.

* opti(transforms): assign world cache directly in SetWorldTransform

Transform.SetPositionAndRotation is world-authoritative, so the resulting world pose equals the arguments passed in; skip the native position/rotation readback and assign the cache from them directly.

* opti(landscape): reuse persistent native containers in RenderGroundSystem

RenderGroundInternal allocated and disposed a NativeArray<int> and a NativeList<Matrix4x4> with Allocator.TempJob every frame the ground was visible; hold them as Allocator.Persistent fields instead, clearing them each frame and disposing once in OnDispose.

* opti(web-requests): pre-size PartialDownloadHandler and grow geometrically

ReceiveData rented exactly PartialData.Length + dataLength and copied the full old buffer on every regrow, causing O(N^2) rent+memcpy churn over a chunked download. Pre-size the buffer from the Content-Length header when available, and otherwise double the buffer on regrow, copying only the bytes written so far.

Exercising the growth path requires driving DownloadHandlerScript's native callbacks, which has no stable unit-test seam in headless batch mode.

* opti(comms-profiles): skip the remove-intentions lock on empty frames

RemoteEntitiesExtensions.Remove ran every frame and unconditionally built an OwnedBunch<RemoveIntention>, whose ctor acquires MutexSync even when there is nothing to remove. Add a racy, lock-free NewBunchAvailable() pre-check (mirrors RemoteProfiles.NewBunchAvailable()) so the lock is skipped entirely on empty frames.

* fix: reset stale hand point-at on teleport and scene reload (#9578)

A cached absolute point-at world hit-point survived a teleport or same-realm
scene reload, leaving an avatar's arm pinned at a stale position. Reset the
point-at target from both the PlayerTeleportIntent path and the scene-reload
path (ECSReloadScene, both overloads).

Verified on real Unity 6000.4.0f1 (compile + EditMode/PlayMode regression tests).

* opti(avatar-rendering): hoist frustum-plane extraction out of the outline query

GetAvatarsVisibleWithOutline recalculated the camera's frustum planes and re-fetched the camera component for every avatar; both are identical across a frame, so compute them once in Update and pass the camera in via [Data].

* opti(avatar-animation): skip redundant point-at/rotation layer weight writes

SetPointAtLayerWeight and SetRotationLayerWeight now shadow the last value written and skip the native Animator.SetLayerWeight call when it hasn't changed; ResetState clears the shadow so a rebind isn't skipped.

* refactor(landscape): replace init flag with nullable tuple in RenderGroundSystem

* docs(landscape): explain why persistent ground containers never need reallocation

* Fix formatting (no code changes)

* changed how reload/teleport reset is managed

* opti(avatar-animation): finish indexed animator-layer API and drop string-based lookups

Completes the layer-index scaffolding introduced with the weight guard:

- AnimatorEmoteLayers gains the BASE_LAYER_INDEX constant the scaffolding
  referenced (fixes compilation) and loses GetFromEmoteMask plus the
  unused ALL_LAYERS / NON_BASE_LAYERS arrays.
- GetAnimatorCurrentStateTag(string) and SetLayerWeight(string) resolved
  the layer via a native Animator.GetLayerIndex call on every invocation,
  per frame in HeadIKSystem and HandsIKSystem. All callers now use the
  cached index API (UpperBodyLayerIndex, GetEmoteLayerIndex, int
  overloads) and the string overloads are removed.
- Removes the unused DclAvatarMask using alias.

* Update Explorer/Assets/DCL/Multiplayer/Profiles/RemoveIntentions/IRemoveIntentions.cs

Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
Signed-off-by: Nick Khalow <71646502+NickKhalow@users.noreply.github.com>

* docs(transforms): explain direct world cache write in SetWorldTransform

* expected length "no double calculation" and comments

* refactor(avatar-rendering): drop camera param from IsVisibleInCamera, pass CameraComponent as query data

---------

Signed-off-by: Nick Khalow <71646502+NickKhalow@users.noreply.github.com>
Co-authored-by: Esteban Ordano <esteban@decentraland.org>
Co-authored-by: lorenzo-ranciaffi <lorenzo.ranciaffi@decentraland.org>
Co-authored-by: lorenzo-ranciaffi <41125365+lorenzo-ranciaffi@users.noreply.github.com>
Co-authored-by: Muna <44584806+decentraland-bot@users.noreply.github.com>
* chore: reskin of credits pack purchase

* Cancel any in-flight topup on panel close

* propagate pack picture when succesfully bought
* ci: share the Explorer build resolver between the test dispatchers

visual-regression.yml carried the only copy of "find the Unity Cloud Build
artifact for this commit": the build-workflow run lookup, the event->prefix
table that mirrors build-unitycloud.yml, and the explorer-automation branch
match. A second dispatcher for the InWorld suite needs all three, so move
them into a composite action instead of copying them.

Two things the extracted version does that the inline copy did not:

  - waits, optionally. A caller triggered by a human can require the build to
    already exist; a caller that fires when a PR opens has to wait out the
    build that same event started. `wait-minutes` defaults to 0, so
    visual-regression.yml keeps failing fast.
  - HEAD-checks the artifact. A successful build run does not guarantee a
    macOS zip — build-unitycloud.yml takes a `platforms` input — and the
    same failure inside the suite costs a whole macOS runner slot.

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

* ci: gate release and hotfix PRs on the InWorld suite

Adds a dispatcher for explorer-automation's run-inworld-suite.yml, the
InWorld counterpart to visual-regression.yml. Both share the build resolver,
so this file is a trigger, a call, and a gate.

It runs on pull_request rather than on the build finishing. A workflow_run
reports its check against the default branch's SHA, so it never joins the
PR's checks and can never block a merge; the requirement here is that a red
suite does block one. The cost is that the first job waits out the ~40-minute
build the same event started, which is cheap on a public repo's runners and
shows up honestly as a pending check meanwhile.

`InWorld suite result` is the job to add to main's required status checks. It
is stable across renames inside the reusable workflow, it fails on a
cancelled run rather than going green, and it passes for PRs into main that
are not release or hotfix — so requiring it blocks nothing else.

Drafts are skipped, matching build-unitycloud.yml, which does not build them.

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

* ci: move visual-regression.yml onto the shared build resolver

Same behaviour, minus the 55 lines the composite now owns. The resolve job
gains a sparse checkout, because `uses: ./…` resolves against the workspace
and this job never had one.

Three deliberate differences: an unreachable artifact now fails here in
seconds instead of on the macOS runner; `actions: read` is stated explicitly
rather than relying on GITHUB_TOKEN's read access to a public repo's data;
and the header names the EXPLORER_TEAM_* secrets this repo actually needs.
It listed the DEV_ family, which went stale when explorer-automation#51 made
the reusable pick a family by caller repo.

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

* ci: wait on the macOS build job, not on the whole build run

Measured on run 31582719577: Build (macos) finishes at 09:52:15, Build
(windows64) at 10:01:18, and the run concludes at 10:01:23. Waiting for the
run therefore costs ~9 minutes on every release PR for a Windows build the
InWorld suite never touches.

Watch the macOS matrix leg instead. One extra API call per poll, and the
resolver stops caring about anything else the run does.

This also relaxes /visual-tests in a useful direction: a run whose Windows
leg failed but whose macOS leg is green is now usable, where before the run's
conclusion disqualified it.

Newest-first with a fallback over the five most recent runs, because a commit
can have a skipped run in front of a real one — that is what happens when a
`force-build` label starts a second run after the first declined to build.

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

* ci: keep the InWorld gate on macOS until the Windows leg is callable

explorer-automation#55 added a Windows leg to run-inworld-suite.yml and
defaulted `windows` to true, so pinning @main silently opted this repo in.
It cannot work from here yet, for three separate reasons:

  - the Windows workflow's checkout is bare. In a workflow_call run that
    fetches the caller — this repo — and explorer/ci/*.ps1 is not here. The
    macOS leg names decentraland/explorer-automation explicitly; the Windows
    one has not been given the same treatment.
  - it is passed no build_url, so it resolves the newest dev build instead of
    the commit on the PR. A release gate that reports on a different commit is
    worse than no gate. The artifact is also a different file —
    Decentraland_windows64.zip sits beside the macOS zip under the same
    prefix, so the resolver here can supply it once an input exists.
  - it runs on `win-gpu-t4-explorer`. A called workflow's jobs use runners the
    caller can reach, and this repo registers no self-hosted runners.

Pass `windows: false` explicitly rather than inheriting a default that
changes underneath us.

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

* ci: gate release and hotfix PRs on Windows as well as macOS

explorer-automation#73 made the Windows leg callable from here, so turn it on.
Three parts:

  - the resolver takes `build-legs` and waits for every named leg, not just
    the macOS one. Both suites start together, so both zips have to exist
    first. visual-regression.yml keeps the default of `macos` and is
    unaffected — it still resolves on a run whose Windows leg is unfinished.
  - each requested leg contributes a URL. The two zips share a prefix and
    differ only by Decentraland_<target>.zip, so the Windows one costs a line,
    and an override derives its sibling rather than needing a second input.
  - both are verified reachable before either suite is handed a runner. The
    Windows one is a self-hosted GPU box; a 404 discovered there is expensive.

`windows: true` is stated rather than inherited. It already defaults to true
upstream, and inheriting that default is exactly how this repo silently
acquired a Windows leg it had no way to run.

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

* ci: split the Windows leg across runners on the release gate

explorer-automation#72 moves sharding into the Windows leg itself: callers now
say what to split with `scope`, and one planner builds the matrix. `shards`,
which only PR runs ever passed, is gone.

The gate runs the whole category, so it says ALL — the same thing
inworld-main.yml says. The planner would infer that anyway, since an empty
scope plus a whole-category filter reads as ALL, but that inference is there
to keep callers who resolved nothing from running unsplit. A merge gate
should not take its runtime from a fallback.

Empty scope is left to mean the other thing: a manual run with a narrow
`filter` has nothing to split and belongs on one runner.

Without this the Windows leg ran the whole suite as `1/1` against a 55-minute
cap.

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

* ci: start the InWorld gate on bot-created release PRs

create-release-branch.yml opens the release PR with GITHUB_TOKEN, and events
that token raises start no workflow runs. So a freshly cut release PR has no
runs at all — which is why that workflow already goes hunting for the dev-push
build to link instead of expecting a PR build. A required check on it would
read "waiting to be reported" forever.

Dispatch the gate explicitly, with a token that can. A workflow_dispatch run
attaches its checks to the dispatched ref's tip, and that tip is the PR head,
so the check still lands on the PR.

Deliberately not the other fix — swapping a PAT into the checkout and
`gh pr create`. That would start every PR workflow, including a full rebuild
of a commit dev already built, which is the 40 minutes this workflow's
build-link reuse exists to avoid.

Only dispatched once a build exists, since a dispatched gate resolves this
commit's build and fails fast without one. Same condition as the build-links
comment, and the same remedy: re-run this workflow.

The gate in turn looks up the branch's PR on dispatched runs, so results
comment there rather than reaching only the job summary — on the one path
where the PR is the whole point.

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

* ci: make the build wait decisive and blip-tolerant

Review findings on #9694, in the resolver's poll loop.

A failed leg stopped being decisive when the second leg arrived. `pending`
was written straight to STATE, so `Build (macos)` red followed by
`Build (windows64)` still running overwrote `failed` and the fast-exit never
fired — the job then waited out a build that was already dead, which is the
~9 minutes the leg-level wait exists to save. States are now ranked within a
candidate first, and only the newest candidate's verdict reaches STATE.

`none` conflated "no build was ever queued" with "a build ran and never had a
job for this leg". The second is reachable on a real release PR two ways: a
`windows-only`/`macos-only` label narrows build-unitycloud's targets, or a PR
touching no Explorer/** file leaves the matrix unexpanded. Both used to wait
the full 90 minutes and then recommend `force-build`, which is the wrong
remedy for the label. That case is now `nomatrix`, decided in seconds, and
says which of the two it is.

Both `gh api` calls ran under `set -e` inside a 90-minute loop, so a single
5xx or rate-limit blip ended the wait with a red required check on a release
PR with nothing wrong with it. They now read as "nothing found this poll".
An unreachable API is deliberately not `nomatrix`: that one fast-fails.

`build_url` reached $GITHUB_OUTPUT as a bare key=value. The REST API accepts
newlines in a workflow_dispatch string input and a glob matches across them,
so a payload could clear the host check on its first line and define a second
output on the next.

Also `per_page=50` on the runs query, and the wait line now says how many of
the runs it found were actually examined.

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

* ci: scope the gate's permissions per job

`pull-requests: write` was top-level, so it also landed on the job that only
reads the Actions API and on the five-line gate. Only the reusable-workflow
call needs a write, for the comment it posts.

`resolve` keeps a read on pull-requests because it runs `gh pr list` to find
the branch's PR on dispatched runs. Unlisted scopes resolve to none, and that
only works today because the repo is public — the same assumption
visual-regression.yml's header says not to lean on.

`run-suite` grants exactly what run-inworld-suite.yml declares, which the
superset rule requires; `result` needs nothing at all.

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

* ci: put both gate triggers in one concurrency group, and verify overrides

Review findings on #9694.

The concurrency key was `github.event.pull_request.number || github.ref_name`,
so a `pull_request` run keyed on a number and a `workflow_dispatch` run on a
branch — different groups, unable to cancel each other. That was theoretical
until this branch taught create-release-branch.yml to dispatch the gate: a
release branch can now be covered by both at once, which means two gates on
one commit, four shards competing for the self-hosted GPU pool, and whichever
finishes last owning the required check. `github.head_ref` is the source
branch on `pull_request` and empty elsewhere, so both paths key on the branch.

`verify()` only guarded resolved URLs; the override path returned unchecked.
An override is a human asserting one URL, but the Windows sibling beside it is
*derived*, not asserted — so an override naming a macOS-only build handed the
Windows leg a 404 on the box that check exists to protect. Hoisted above the
override branch; both paths now check what they hand out.

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

* ci: gate the release dispatch on the build legs, not the run

Review findings on #9694.

The dispatch guard accepted `status=success` on a build run — the same blind
spot already fixed in the resolver. build-unitycloud.yml concludes green
having built nothing (no Explorer/** change, matrix never expands) and green
having built one platform (a windows-only/macos-only label narrows targets).
Either dispatched a gate that reds a brand-new release PR on its first poll,
because the dispatch path resolves with wait-minutes 0. It now asks the same
question the gate does: did Build (macos) and Build (windows64) both conclude
success. Not found keeps the existing notice-and-skip.

`gh workflow run` needs `actions: write`, which ORG_ACCESS_TOKEN's other uses
here do not, and a secret's scopes cannot be read from a workflow. A silent
dispatch failure is the worst outcome for a required check — it sits
unreported with no clue why — so the failure is now annotated with the likely
cause and the manual remedy.

Deliberately a warning, not a failure: by that point the branch, PR, labels
and build-links comment have all landed, and a red job invites a re-run of
this workflow, which force-pushes the release branch and quietly re-cuts the
release from a newer dev tip. The release stays blocked regardless, because
the gate's check never reports.

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

* ci: retry the Actions API, and stop a dead listing reading as "no build"

Review finding on #9694: absorbing API failures fixed the 90-minute wait and
broke the two paths that do not get a second poll.

`wait-minutes: 0` decides on one answer. `visual-regression.yml` takes that
path on every /visual-tests, and create-release-branch.yml's dispatched gate
takes it on every release cut — so one 5xx reported a build that is very
likely fine as missing, and reds a required check on a brand-new release PR.
`gh` has no retry of its own; three attempts five seconds apart cover it.

The same absorption also gave `none` back a second meaning one level up: an
empty listing produced TOTAL=0 and no candidate loop, so "no run exists for
this SHA" and "every listing call failed" arrived at the same message — the
one telling the operator to label a fully-built commit `force-build`. The
comment above it asserted that could not happen.

Two flags, not one, because the scenario that reaches this is rate-limit
exhaustion, and this loop is what spends the budget: it starts partway
through a wait, when a listing has already succeeded. `LISTED` alone would
still print the force-build message. `LISTED_NOW` says whether the last poll
answered, `LISTED` whether any poll ever did, and between them the timeout
names which of the three happened.

The jobs call keeps one flag: its failure lands on `pending`, whose message
asks rather than asserts and never points at `force-build`.

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

* ci: keep a fork out of the release gate's concurrency group, and retry the guard

Review findings on #9694.

Keying the group on the branch name alone let a fork PR whose head branch is
named `release/2026-08-10` join the same group as the real release PR. With
cancel-in-progress that fork run kills the legitimate one *before any job's
`if:` is evaluated*, so the fork exclusion in `resolve` never gets to matter
and the gate reports failure on a cancelled run, exactly as designed. A merge
gate an outsider can red at will. The head repo now leads the key:
`head.repo.full_name` is the fork on a `pull_request` and absent otherwise, so
a same-repo PR and a dispatch still agree — which is what put both triggers in
one group in the first place — while a fork gets its own. Everything before
the first `/` is the head owner, so no fork name can collide.

The release-cut guard had the same one-answer shape 1fc33ec fixed in the
resolver, and a worse remedy: both its calls fell back to empty, and "nothing
found" skips the dispatch, so one blip meant the gate silently never started
on a cut nobody is watching. It now shares the resolver's retry, and no longer
reports an unanswered API as "no build".

The comment justifying that retry was wrong even though the code was right: it
named rate-limit exhaustion as the realistic outage, which resets on an hour
boundary and so is not rescued by three attempts ten seconds apart. Corrected
to claim only what is true.

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

* ci: let single-poll callers see why the build was rejected

The wait-minutes: 0 exit sat above the `failed` and `nomatrix` fast-exits, so
a caller that gets one answer always got the generic line — even when the loop
had already decided a leg was red or was never in the matrix. Those are the
two callers that cannot recover by waiting: visual-regression.yml on every
/visual-tests, and create-release-branch.yml's dispatched gate on every
release cut. The specific messages existed precisely so nobody has to guess,
and the ordering kept them from the paths that needed them most.

Hoisted rather than duplicated inside the branch, so each message has one
copy. The outage message still wins where it should: a listing that never
answers leaves TOTAL=0, so no candidate is examined and STATE is necessarily
`none` — neither hoisted check can match. The waiting path is untouched, since
a branch guarded on WAIT_MINUTES -eq 0 cannot fire when it is not.

Also dropped "or did it fail?" from the generic line. With `failed` exiting
above it, only pending/none reach it, so the clause asked about a case that
can no longer arrive and would send an operator to re-check what the loop had
already ruled out.

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

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
* fix smart wearable loading due to missing profile

* improve cancellation token management
* prevent stack of exceptions propagating at HLS synthesis

* remove unused import

* reduce lint warnings
@github-actions

Copy link
Copy Markdown
Contributor Author

@github-actions
github-actions Bot requested a review from anicalbano August 17, 2026 18:18
@github-actions

Copy link
Copy Markdown
Contributor Author

InWorld suite

InWorld suite failed on 3 of 3 legs.

macOS Windows 1/2 Windows 2/2
Result ❌ failed ❌ failed ❌ failed
Tests not reported not reported not reported
Duration 1m12s 2m11s 2m0s
Allure not generated not generated not generated

Commit 1bd63e7 · branch release/2026-08-17 · filter Category=InWorld · run #32054230063

macOS on macos-14, Windows shards on win-gpu-t4-explorer. Updated on every run.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants