Skip to content

Support the .NET 11 SDK band and net11.0-tizen TFMs - #310

Merged
JoonghyunCho merged 9 commits into
Samsung:net10.0from
Redth:redth-net11-tizen-workload
Aug 28, 2026
Merged

Support the .NET 11 SDK band and net11.0-tizen TFMs#310
JoonghyunCho merged 9 commits into
Samsung:net10.0from
Redth:redth-net11-tizen-workload

Conversation

@Redth

@Redth Redth commented Aug 26, 2026

Copy link
Copy Markdown

Summary

Adds .NET 11 support to the tizen workload alongside .NET 10, and repairs two install scripts that were committed corrupt on this branch.

Base branch

This PR targets net10.0 because that is the active branch and it is already multi-band:
DOTNET_VERSION selects the SDK band at build time, and Versions.props carries per-major
arcade conditions. No net11.0 branch exists today, so net10.0 is the only branch this can
land on.

If you would rather follow the per-major branch convention (net8.0 / net9.0 / net10.0),
create a net11.0 branch and simply retarget this PR to it — no changes to the commits are
required
. The change set is additive and does not modify .NET 10 behaviour, so it applies
cleanly either way. Say the word and I'll switch the base.

Chosen TFM / API mapping

Primary TFM: net11.0-tizen11.0 → TizenFX API level 15Samsung.Tizen.Ref.API15 15.0.0.19396 (already published).

net11.0 combines with every declared platform version, so net11.0-tizen{8.0,9.0,10.0,10.1,11.0} all work.

No new reference or runtime pack is required. Ref packs ship ref/net8.0 assemblies resolved by explicit <File Path=…/> entries in data/FrameworkList.xml, so they're independent of the consuming project's .NET version. Samsung.Tizen.Ref.API16 does not exist and isn't needed.

SDK feature band: 11.0.100-preview.7 (latest .NET 11 SDK is 11.0.100-preview.7.26381.103, 2026-08-11).

Commit 1 — install script repair (independent of .NET 11)

Both installers were committed truncated mid-statement and NUL-padded, so the publicly curl'd installer could never complete:

  • workload-install.sh ended at for DOTNET_SDK in $INSTALLED_DOTNET_SD + 134 NUL bytes
  • workload-install.ps1 ended inside its catch block at Write-Host

validate-version-map.yml didn't catch it because Generate-InstallScripts.ps1 only compares the auto-generated version-map block — which was intact — so it reported OK for a file missing its tail. The generator now also asserts no NUL bytes and an expected final statement.

Also restores the SDK enumeration fix lost in the same regression: --update-all-workloads matched only ^6|^7, silently ignoring every installed .NET 8/9/10/11 SDK.

Commit 2 — .NET 11 support

File Change
build/Versions.props arcade 11.0.0-beta.26426.103 for 11.0 bands
NuGet.config add dotnet11; clear inherited disabledPackageSources
Samsung.Tizen.Sdk.targets add the net11.0 KnownRuntimePack (without it: NETSDK1082)
RuntimeList.xml add the .NET Runtime 11 row
template.json offer net11.0; default stays net10.0 while .NET 11 is preview
TizenApp1.csproj Tizen.UI.Components.Material referenced conditionally on the resolved platform version, with an actionable TIZENTMPL001 instead of an opaque NU1101
test-matrix.sh net11 rows; rows whose .NET major has no installed SDK are skipped, not failed, so the default .NET 10 run stays green
validate-workload-metadata.py new C5/C6 checks tying template/matrix .NET majors to KnownRuntimePack + RuntimeList.xml
test-version-band.sh new — asserts SDK version → feature band, and .sh/.ps1 parity
Makefile validate-metadata, test-version-band, aggregate check (no dotnet install needed)
build-matrix.yml non-blocking .NET 11 preview leg

version-map.json is deliberately not updated — it's a fallback cache of already published manifest versions, so an entry for an unreleased band would make the installer download a 404. Add it after the first release, as 10.0.300 was.

Validation performed

Built the workload against the real .NET 11 preview SDK (11.0.100-preview.7.26381.103):

  • Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7 packs with the correct id, installs into sdk-manifests/11.0.100-preview.7/
  • dotnet new tizen --framework net11.0 + dotnet build produces com.companyname.TizenApp1-1.0.0.tpk for both net11.0-tizen11.0 and net11.0-tizen10.0
  • net11.0-tizen11.0 verified to resolve Samsung.Tizen.Ref.API15/15.0.0.19396
  • make check: 6/6 metadata checks, 36/36 version-band assertions, install-script drift + integrity all green
  • C5/C6 confirmed by negative test (removing the net11 KnownRuntimePack / RuntimeList row fails the build)

Not run locally: Samsung.Tizen.Ref.API13/14/15 packing, which needs Tizen.NET.API* from Samsung's GitHub Packages feed (403 without org membership). CI has secrets.GITHUB_TOKEN for this. The published equivalents were substituted to complete the end-to-end run.

External blockers (not fixable here, not faked)

  1. Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7 is unpublished. Needs a Release Workload run with net_sdk_version = 11.0.100-preview.7.26381.103. Until then workload-install.sh finds no manifest on an 11.x SDK.
  2. Tizen.UIExtensions.NUI (Samsung/Tizen.UIExtensions) — published 0.9.2 ships lib/net6.0-tizen7.0 + lib/tizen10.0. TFM compatibility is fine; the blocker is its dependency group pinning Microsoft.Maui.Graphics 6.0.300-rc.3.1336. Needs a release with lib/net11.0-tizen11.0 built against API15 and refreshed MAUI Graphics deps.
  3. Tizen.UI.Components.Material 1.0.0-rc.8 — ships only lib/net8.0-tizen10.0, never GA. Unusable below tizen10.0; handled in the template as described above.

Details, owners and expected artifact names in workload/docs/net11.md.

Redth and others added 2 commits August 26, 2026 15:09
…ression

Both installers were committed truncated mid-statement and NUL-padded, so the
publicly curl'd installer could never complete. workload-install.sh ended at

    for DOTNET_SDK in $INSTALLED_DOTNET_SD

followed by 134 NUL bytes, and workload-install.ps1 ended inside its catch
block at `Write-Host `. The final lines are restored from the intact copies on
main.

validate-version-map.yml did not catch this because Generate-InstallScripts.ps1
only compares the auto-generated version-map block, and that block was
untouched, so it reported "OK" for a file missing its tail. The generator now
also verifies each installer contains no NUL bytes and ends with its expected
final statement, and fails otherwise.

Also restores the SDK enumeration fix that was lost in the same regression.
--update-all-workloads matched only `^6|^7`, silently ignoring every installed
.NET 8/9/10/11 SDK. It now matches majors 6-9 and any two-or-more digit major,
so future majors need no further edit.

Finally, extracts the SDK-version to feature-band computation into
compute_target_version_band() delimited by BEGIN/END VERSION BAND DETECTION
markers. Behaviour is unchanged - MANIFEST_NAME was already equal to the plain
band in the fall-through case - but the logic is now testable in isolation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Adds .NET 11 support to the tizen workload alongside .NET 10. The primary
target framework is net11.0-tizen11.0, which maps to TizenFX API level 15 and
the already-published Samsung.Tizen.Ref.API15 targeting pack. No new reference
or runtime pack is required: ref packs ship ref/net8.0 assemblies resolved by
explicit paths in FrameworkList.xml, so they are independent of the consuming
project's .NET version.

Workload changes:
* Versions.props: arcade 11.0.0-beta.26426.103 for 11.0 bands.
* NuGet.config: add the dotnet11 source, and clear inherited
  disabledPackageSources so a developer with nuget.org disabled machine-wide
  does not get a confusing NU1101 for a source this file already lists.
* Samsung.Tizen.Sdk.targets: add the net11.0 KnownRuntimePack. Without it the
  SDK cannot resolve Samsung.NETCore.App.Runtime.tizen (NETSDK1082).
* RuntimeList.xml: add the .NET Runtime 11 FileList row.
* template.json: offer net11.0; the default stays net10.0 because .NET 11 is
  still a preview SDK.
* TizenApp1.csproj: Tizen.UI.Components.Material ships assets for the tizen
  10.0 band only, so reference it conditionally on the resolved platform
  version and raise an actionable TIZENTMPL001 below that, instead of an
  opaque NU1101.

Verification:
* test-matrix.sh: add net11.0-tizen11.0 and net11.0-tizen10.0 rows. Rows whose
  .NET major has no installed SDK are now skipped rather than failed, so the
  default .NET 10 run stays green.
* validate-workload-metadata.py: new checks C5/C6 tying every .NET major
  offered by the template or exercised by the matrix to a KnownRuntimePack and
  a RuntimeList.xml row.
* test-version-band.sh: new test asserting the SDK version to feature band
  mapping (11.0.100-preview.7.26381.103 -> 11.0.100-preview.7) and that
  workload-install.sh and workload-install.ps1 agree.
* Makefile: add validate-metadata, test-version-band and an aggregate check
  target that need no dotnet install.
* build-matrix.yml: add a non-blocking .NET 11 preview leg.

Validated by building the workload against 11.0.100-preview.7.26381.103:
Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7 packs correctly, installs,
and `dotnet new tizen --framework net11.0` builds a .tpk for both
net11.0-tizen11.0 (resolving Samsung.Tizen.Ref.API15/15.0.0.19396) and
net11.0-tizen10.0.

version-map.json is deliberately not updated: it is a fallback cache of
already-published manifest versions, so an entry for an unreleased band would
make the installer download a 404. See workload/docs/net11.md for the mapping
and the external blockers.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Author

Maintainer action required: approve fork workflows

GitHub created all required PR runs, but they are in action_required and can only be approved by a repository administrator:

Please use Approve and run workflows on these runs. Until then, the PR has no CI status even though the .NET 11 manifest pack/install/template/build/TPK chain was validated locally as described in the PR.

Code review:
* test-matrix.sh required an exact SDK/TFM major match, so the .NET 10 job skipped
  every net8/net9/net11 row and built nothing. An SDK builds its own major and all
  earlier ones, so rows are now skipped only when NEWER than the newest installed
  SDK. Pinned by a new `--self-test` mode that needs no dotnet install.
* build-matrix.yml made .NET 11 unconditionally advisory and build-workload.yml
  always used the Versions.props default. Both now switch on a net11.0 branch or a
  PR into one: the .NET 11 leg blocks and the workload builds against
  $(DotNet11SdkVersion), a new SSOT in Versions.props that the workflows grep.
  Check C7 fails if a workflow hardcodes a different .NET 11 SDK.
* The template read $(TargetPlatformVersion) in the project body, which the SDK has
  not yet inferred from the TFM at that point. net11.0-tizen9.0 therefore fell back
  to 10.0, pulled in an incompatible Tizen.UI.Components.Material and never raised
  TIZENTMPL001. The platform version is now parsed from $(TargetFramework), and the
  target re-checks the authoritative value. Verified end to end: tizen9.0 now errors
  with the real resolved version, tizen11.0 builds.
* Both installers swallowed per-SDK failures and exited 0 after printing DONE. They
  now track failures and exit non-zero; --update-all-workloads still visits the
  remaining SDKs but the overall run fails.

MSBuild review:
* workload-install.ps1 built its fallback prefix from a fixed length
  ($ManifestBaseName.Length + 2), so '...Manifest-11.0.100-preview.7' became
  '...Manifest-1' and matched the 10.x entries, installing a .NET 10 manifest into
  an 11.x band. Both installers now constrain the fallback to the same major.minor
  family and fail closed.
* DOTNET_VERSION was cached in .tmp/dotnet-version.config whose only prerequisite
  was Versions.props. The cache was always newer, so make never regenerated it and a
  newly-passed DOTNET_VERSION was ignored - silently building the previous band. It
  is now resolved immediately, and DOTNET_DESTDIR plus the install stamp are
  band-scoped.
* PackageTargetFallback now covers the full (.NET major x platform) cross-product.
  A missing entry makes FixupNuGetReferences leave a package on its netstandard2.x
  assets; net11.0-tizen11.0 was absent. Check C8 keeps it in sync with
  KnownRuntimePack x TizenSdkSupportedTargetPlatformVersion.
* release-workload.yml no longer builds with continue-on-error and no longer pushes
  a glob over whatever is on disk. It cleans first, fails on build error, stages to
  an isolated directory and verifies the expected manifest exists before pushing.
* test-version-band.sh now extracts the real Get-TargetVersionBand from
  workload-install.ps1 instead of reimplementing it, so sh/ps1 drift is actually
  detectable, and also checks Config.mk producer parity.
* Config.mk did not round the feature band for stable non-6 versions: 10.0.404
  produced band '10.0.404' while the installers looked for '10.0.400'. Fixed, with
  producer/consumer parity tests.
* MSI staging copied only Ref.API11/12/13; API14 and API15 were missing and API15 is
  the targeting pack for net11.0-tizen11.0. Now stages every Ref pack.
* workload-install.sh had its shebang below a comment block, so direct execution did
  not reliably use bash. Moved to the first line.
* make check now gates on pwsh with an actionable message instead of failing with
  'command not found'; SKIP_PWSH_CHECKS=1 opts out.

RuntimeList.xml (multiple root elements) is left as-is: proven not to be XML-parsed.
Replacing the installed pack's copy with non-XML text still builds a
net11.0-tizen11.0 project with an explicit RuntimeIdentifier - the path that resolves
runtime pack assets and the one MAUI takes. Documented in workload/docs/net11.md.

Validation on 11.0.100-preview.7.26381.103: full matrix 6/6 pass, 0 skipped
(net8.0-tizen10.0/10.1/11.0, net9.0-tizen10.0, net11.0-tizen11.0, net11.0-tizen10.0).
make check: C1-C8, 11 self-test, 61 version-band, 13 template-condition, 4
install-failure assertions, plus install-script drift and integrity.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Author

Review fixes pushed — 9cdce68

All four code-review findings and all ten MSBuild-review findings are addressed. One item (#14) is reported as provably non-applicable with evidence rather than changed.

Code review

# Fix
1 test-matrix.sh required an exact SDK/TFM major match, so the .NET 10 job skipped every net8/net9/net11 row and built nothing. Rows are now skipped only when newer than the newest installed SDK. New --self-test mode pins it with no dotnet install (11 assertions).
2 .NET 11 was unconditionally advisory and build-workload.yml always used the .NET 10 default. Both now switch on a net11.0 branch / PR-into-one: the leg blocks and the build uses $(DotNet11SdkVersion) — a new SSOT in Versions.props that the workflows grep. Check C7 fails if a workflow hardcodes a different version.
3 Confirmed and fixed. The template read $(TargetPlatformVersion) in the project body, before the SDK infers it from the TFM. Probe output was TPV='' resolved='10.0' material='True' for net11.0-tizen9.0 — exactly as reported. Now parsed from $(TargetFramework), with the target re-checking the authoritative value. Verified: net11.0-tizen9.0error TIZENTMPL001 ... (resolved TargetPlatformVersion: 9.0), net11.0-tizen11.0 builds. 13 evaluation regression tests added.
4 Both installers now track per-SDK failures and exit non-zero; --update-all-workloads still visits remaining SDKs but the run fails. 4 tests, including "must not print DONE on failure".

MSBuild review

# Fix
5 Fixed. $ManifestBaseName.Length + 2 truncated ...Manifest-11.0.100-preview.7 to ...Manifest-1, matching 10.x entries. Both installers now constrain fallback to the same major.minor family and fail closed. Parity-tested.
6 Root cause was deeper than paths: DOTNET_VERSION was cached in .tmp/dotnet-version.config whose only prerequisite was Versions.props. The cache was always newer, so make never regenerated it and a newly-passed DOTNET_VERSION was ignored. Now resolved immediately; DOTNET_DESTDIR and the install stamp are band-scoped. Band-switching in one tree is tested.
7 PackageTargetFallback now covers the full 30-entry (.NET major × platform) cross-product. Confirmed via FixupNuGetReferences: a missing entry leaves a package on its netstandard2.x assets — net11.0-tizen11.0 was absent. Check C8 keeps it in sync with KnownRuntimePack × TizenSdkSupportedTargetPlatformVersion.
8 release-workload.yml: no continue-on-error, make clean first, stage to an isolated dir, verify the expected manifest exists, push explicit staged paths. Band is computed by reusing the installer's own function so the release can't disagree with it.
9 test-version-band.sh now extracts the real Get-TargetVersionBand from workload-install.ps1 (same BEGIN/END markers as bash) instead of reimplementing it, so drift is genuinely detectable.
10 Confirmed: Config.mk did not round stable non-6 bands — 10.0.40410.0.404 while installers looked for 10.0.400. Fixed; producer/consumer parity now asserted across all 18 version cases.
11 MSI staging copied only Ref.API11/12/13. Now stages every Ref.API* — API15 is the targeting pack for net11.0-tizen11.0.
12 Shebang moved to line 1.
13 make check gates on pwsh with an actionable message; SKIP_PWSH_CHECKS=1 opts out.

#14RuntimeList.xml: proven not XML-parsed, left unchanged

The file does have multiple root elements. I checked the consumer contract empirically rather than assuming:

  1. Microsoft.NET.Build.Tasks.dll does contain RuntimeListNotFound / absoluteRuntimeListPath, so the SDK can read a runtime pack's RuntimeList.xml — a static grep alone would have been misleading.
  2. So I tested the path that actually resolves runtime pack assets: a net11.0-tizen11.0 build with an explicit RuntimeIdentifier=tizen-x86 (what MAUI takes via EnableImplicitRuntimeIdentifiers). Built clean.
  3. Decisive test: replaced the installed pack's RuntimeList.xml with the literal text <<< THIS IS NOT XML AT ALL &&& >>> and rebuilt the same RID-specific project → Build succeeded. 0 Error(s).

Samsung.NETCore.App.Runtime.tizen is a placeholder pack whose only payload is lib/net6.0-tizen/_._, so nothing reads the file. Making it well-formed would need a non-standard wrapper root or splitting the pack per .NET version — more risk than the malformed file carries while unread. C6 parses it line-wise, matching how it's produced. Documented in workload/docs/net11.md with the revisit criteria.

Validation on 11.0.100-preview.7.26381.103

Full matrix 6/6 pass, 0 skippednet8.0-tizen10.0, net8.0-tizen10.1, net8.0-tizen11.0, net9.0-tizen10.0, net11.0-tizen11.0, net11.0-tizen10.0.

make check: C1–C8, 11 self-test, 61 version-band (bash ↔ PowerShell ↔ Config.mk, fallback family, band isolation), 13 template-condition, 4 install-failure assertions, plus version-map drift and install-script integrity. Docs updated to describe the corrected behaviour.

Unrelated observation

tizen.myget.org now returns HTTP 401 to anonymous clients (.../api/v3/index.json). It remains a push target in build-workload.yml's deploy job. I have not touched it — removing a publishing destination is a maintainer call — but it's noted in workload/docs/net11.md in case any docs still point consumers there for restore.

@Redth

Redth commented Aug 26, 2026

Copy link
Copy Markdown
Author

Updated workflow approval request for head 9cdce68

The review-fix push created replacement runs, all currently action_required:

Please approve these newer runs; the earlier links target superseded head b847341.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

Merge-readiness validation at head 9cdce68 (clean-tree re-run)

Re-validated from a fully cleaned tree. No code changes were needed — head is unchanged at 9cdce68 and the existing approval request still points at the current run IDs.

Manifest ID / version — .NET 11 supported, old bands unaffected

Built the manifest package for three bands from the clean tree:

DOTNET_VERSION Produced package
11.0.100-preview.7.26381.103 Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7.10.0.123.nupkg
10.0.100 Samsung.NET.Sdk.Tizen.Manifest-10.0.100.10.0.123.nupkg

The .NET 11 manifest declares Samsung.Tizen.Ref.API1515.0.0.19396, the targeting pack for net11.0-tizen11.0.

Diffed the 10.0 build against the actually-published Samsung.NET.Sdk.Tizen.Manifest-10.0.100 10.0.123 from nuget.org:

  • package id: identical
  • WorkloadManifest.targets: byte-identical
  • WorkloadManifest.json: differs only by the additive Samsung.Tizen.Ref.API14 / API15 pack entries — introduced by base-branch commit 44d7bdf, which predates this branch point. git diff 5e13077 HEAD -- .../WorkloadManifest.in.json is empty; this PR does not touch that file.

Feature-band change: proven non-regressive

The Config.mk rounding fix (finding #10) changes the manifest package ID, so I verified it against every one of the 37 bands in version-map.json, comparing the pre-change logic (reconstructed from base commit 5e13077) with the current one:

identical: 37   differing: 0

It differs only for non-round patch versions, where the new value is the correct one:

input old (producer) new (producer) installer (consumer) expects
9.0.304 9.0.304 9.0.300 9.0.300
10.0.404 10.0.404 10.0.400 10.0.400
8.0.421 8.0.421 8.0.400 8.0.400

End-to-end, both bands, one tree

Band dotnet workload list Matrix
11.0.100-preview.7 tizen 10.0.123/11.0.100-preview.7 6/6 pass, 0 skipped
10.0.100 tizen 10.0.123/10.0.100 4 pass, 0 fail, 2 skipped (net11 rows only)

The 10.0 run is the direct refutation of finding #1: net8/net9 rows now build on a .NET 10 SDK and only the genuinely-unbuildable net11 rows skip. Previously every row skipped and the job built nothing.

Both SDKs coexist under out/dotnet-10.0.100 and out/dotnet-11.0.100-preview.7, confirming the band-isolation fix (#6).

make check on the clean tree: C1–C8 plus 89 assertions (11 self-test, 61 version-band, 13 template-condition, 4 install-failure), version-map drift and install-script integrity — all green.

Pre-existing issue found (NOT introduced here, NOT fixed here)

The .NET 9 band cannot be built on this branch, independent of these changes:

error: Unable to find package Microsoft.DotNet.SharedFramework.Sdk with version (= 9.0.0-beta.25065.2)

Evidence it is pre-existing:

  • the pin was added by commit 86fcc19 (2025-11-19), long before this branch;
  • git diff 5e13077 HEAD -- workload/build/Versions.props shows my edits are purely additive (the 11.0 block + DotNet11SdkVersion) and do not touch the 9.0 pin;
  • 9.0.0-beta.25065.2 exists for Microsoft.DotNet.Arcade.Sdk but not for Microsoft.DotNet.SharedFramework.Sdk on dotnet-eng (nearest real versions present in both: 9.0.0-beta.25058.5, 9.0.0-beta.25077.4).

Only the 9.0 pin is affected — 8.0, 10.0 and 11.0 all resolve.

I have not changed it: it is outside this PR's scope, and I cannot validate a 9.0 pack-output comparison precisely because the band doesn't build today. Flagging it because one consequence of finding #8's fix is relevant: with continue-on-error removed from release-workload.yml, a release run for the 9.0 band will now fail loudly instead of silently proceeding to the push step. That is the intended behaviour, but it means this latent breakage becomes visible.

Remaining blocker

Only maintainer approval of the fork workflow runs. All three remain action_required at the run IDs listed in the previous comment; no new runs were created because the head did not change.

Installers
* getLatestVersion / Get-LatestVersion now return "<packageId>=<version>". Returning
  only a version made the caller download that version under the ORIGINAL, unpublished
  manifest id: a request for '...manifest-10.0.400' resolves to 10.0.127, which exists
  only under '...manifest-10.0.300', so the download 404'd. Verified live against
  nuget.org: old path HTTP 404, new path HTTP 200, and an end-to-end run installs the
  10.0.300 package into the 10.0.400 band directory.
* Removed $global:FallbackId from workload-install.ps1. It was never cleared, so an
  -UpdateAllWorkloads run could carry one SDK's fallback package into the next SDK's
  install. The resolved id is now function-local. Verified with a live two-SDK run
  (10.0.404 -> fallback install, then 11.0.100-preview.7 -> fails closed, no 11.x
  directory created).
* Replaced the ${var,,} lowercase expansion. It is bash 4+, and macOS ships bash 3.2 -
  where it raised "bad substitution", left the version empty and silently skipped the
  fallback entirely, making the fix above unreachable. macOS is a supported target
  (DOTNET_DEFAULT_PATH_MACOS). An empty or failed version query now takes the same
  fallback path as an explicit BlobNotFound, and a failed download fails closed.
* Quoted install/temp paths so a directory containing spaces works.

NuGet fallbacks
* Samsung.Tizen.Sdk.NuGet.targets no longer passes an unfiltered 30-entry cross product.
  FixupNuGetReferences matches lib/<name>/ directories by NAME ONLY, so the list has to
  be filtered: a net6.0-tizen8.0 build could otherwise consume net6.0-tizen11.0 or
  net11.0-tizen8.0 assets. Candidates are emitted only when both their .NET version and
  their platform version are <= the project's, highest-first so the best compatible
  match wins. Built from conditional properties, not items - MSBuild evaluates all
  top-level properties before any items, so an @(item) reference there expands to
  nothing.

Runtime metadata
* RuntimeList.xml is now well-formed (single <FileList> root). It IS parsed:
  Microsoft.NET.Build.Tasks carries the literal alongside the runtime-pack manifest
  fields ResolveRuntimePackAssets reads. A previous note claiming otherwise was based on
  a framework-dependent RID build, which never reaches that task; that claim is
  retracted in the docs.
* Self-contained Tizen publishing is rejected with TIZENSDK001. The pack ships no
  runtime binaries, so a self-contained app cannot work; previously this surfaced as an
  opaque NETSDK1083. Note: with the guard bypassed and the malformed file restored, this
  configuration still failed at NETSDK1083 before reaching ResolveRuntimePackAssets, so
  a raw XmlException was not reproducible here - the malformed file was nonetheless a
  latent hazard on any path that does reach it.

Release
* Release notes reuse the staging step's verified band and manifest id. Recomputing
  ${SDK%%-*} produced 11.0.100 for 11.0.100-preview.7.* and 10.0.404 for a servicing
  band, linking to packages that were never published.
* The version bump is committed and pushed, and the tag targets that commit. Previously
  the bump was runner-local, so the released tag pointed at sources still carrying the
  previous version.

Coverage
* Matrix gains explicit net10.0 rows (10.0/10.1/11.0) plus net11.0-tizen10.0, and a
  self-contained disposition assertion.
* The .NET 11 leg now blocks by default; set repo variable TIZEN_NET11_ADVISORY=true to
  downgrade it temporarily.
* New scripts/test-package-fallback.sh pins the fallback filtering with negative
  cross-platform/cross-version cases. C6 now parses RuntimeList.xml with a real XML
  parser and requires the TIZENSDK001 guard; C8 requires every fallback candidate to be
  compatibility-gated; C7 ignores YAML comments.

Validation on 11.0.100-preview.7.26381.103: matrix 9/9 TFM rows plus the self-contained
assertion, 0 skipped. make check: C1-C8 plus 110 assertions (11 self-test, 61
version-band, 13 template-condition, 6 package-fallback, 19 install-failure) - including
a real install into a space-containing path and an unreachable-feed case, all under
bash 3.2.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

New head 6fffd09 — all code + MSBuild review blockers addressed

Maintainer action required: approve fork workflows for the NEW head

The previous runs were superseded. All three replacement runs are action_required:

Installers

Fallback now resolves the package ID, not just a version. getLatestVersion /
Get-LatestVersion return "<packageId>=<version>". Verified live against nuget.org using the
reviewer's exact example:

requested (unpublished) samsung.net.sdk.tizen.manifest-10.0.400
resolved samsung.net.sdk.tizen.manifest-10.0.300 = 10.0.127
old download path …manifest-10.0.400/10.0.127HTTP 404
new download path …manifest-10.0.300/10.0.127HTTP 200

End-to-end run installs the 10.0.300 package into the 10.0.400 band directory, as intended.

$global:FallbackId removed. Verified with a live two-SDK -UpdateAllWorkloads run:
10.0.404 → falls back and installs; then 11.0.100-preview.7fails closed (Wrong Id),
overall exit 1, and no 11.x manifest directory is created. Previously the 10.x fallback id
persisted into the next iteration.

bash 3.2 (macOS). ${MANIFEST_NAME,,} is bash 4+; on macOS it raised bad substitution,
left the version empty and silently skipped the fallback entirely — making the fix above
unreachable there. Replaced with tr. An empty/failed query now takes the same path as
BlobNotFound, and a failed download fails closed. Install/temp paths are quoted; a real
install into dir with spaces/dotnet sdk is now a test.

NuGet fallbacks

FixupNuGetReferences matches lib/<name>/ by name only, so the list must be filtered.
Candidates are emitted only when both .NET version and platform version are <= the
project's, appended highest-first. Negative assertions now guard exactly the leaks called out:

building must NOT admit
net6.0-tizen8.0 net6.0-tizen9.0/10.0/11.0, net8.0-tizen8.0, tizen90, tizen10.0
net11.0-tizen8.0 net11.0-tizen9.0, net6.0-tizen11.0, tizen90
net6.0-tizen11.0 net8.0-tizen11.0, net11.0-tizen11.0, net9.0-tizen10.0

Deleting the filter makes the test fail with explicit LEAKED: diagnostics.

Built from conditional properties, not items — MSBuild evaluates all top-level properties
before items, so an @(item) reference there silently expands to nothing (this bit me first
attempt).

Runtime metadata — correcting my earlier claim

You are right that RuntimeList.xml is parsed, and my earlier "never parsed" note was wrong.
It is retracted in the docs.
Microsoft.NET.Build.Tasks carries the literal RuntimeList.xml
next to the runtime-pack manifest fields ResolveRuntimePackAssets reads (Managed, Native,
PgoData, AssemblyVersion, PublicKeyToken). My earlier test used a framework-dependent
RID build, which never reaches that task.

Both remediations applied:

  1. The file is well-formed (single <FileList> root, legitimately empty — the pack ships
    only lib/net6.0-tizen/_._).
  2. Self-contained is rejected with TIZENSDK001 and an actionable message.

One precise scope note: with the guard bypassed and the malformed file restored, this
configuration failed at NETSDK1083 (RID resolution) before reaching
ResolveRuntimePackAssets, so I could not reproduce a raw XmlException here. I'm reporting
that rather than claiming a repro I didn't get. The malformed file was still a latent hazard on
any path that does reach the task, and both fixes stand regardless of which error surfaces first.

Release

  • Notes reuse the staging step's verified band and manifest id. ${SDK%%-*} produced
    11.0.100 for 11.0.100-preview.7.* and 10.0.404 for a servicing band — both linking to
    packages that were never published.
  • The version bump is now committed and pushed, and the tag targets that commit. Previously
    the bump was runner-local, so the tag pointed at sources still carrying the previous version.

Coverage

  • Matrix gains explicit net10.0 rows (10.0/10.1/11.0) plus net11.0-tizen10.0 → 9 TFM
    rows, plus a self-contained disposition assertion.
  • .NET 11 now blocks by default (TIZEN_NET11_ADVISORY=true downgrades it). The SDK version
    is pinned exactly, so the leg is deterministic — and a branch whose purpose is .NET 11 support
    gains nothing from an advisory-only .NET 11 gate.

Validation on 11.0.100-preview.7.26381.103

Matrix 9/9 TFM rows + self-contained assertion, 0 skipped:
net8.0-tizen{10.0,10.1,11.0}, net9.0-tizen10.0, net10.0-tizen{10.0,10.1,11.0},
net11.0-tizen{10.0,11.0}, and PASS self-contained rejected with TIZENSDK001.

make check: C1–C8 plus 110 assertions — 11 self-test, 61 version-band
(bash ↔ PowerShell ↔ Config.mk), 13 template-condition, 6 package-fallback, 19 install-failure
— all under bash 3.2, including the space-path install and an unreachable-feed case.

Environmental note (not a repo defect)

On this machine the freshly extracted .NET 11 preview SDK cannot run RoslynCodeTaskFactory
(MSB3755 for mscorlib/netstandard), so make packs fails under it. The same sources
pack cleanly
under .NET 10.0.400, producing Samsung.NET.Sdk.Tizen.Manifest-11.0.100-preview.7
correctly, and the resulting workload installs into and builds with the .NET 11 SDK. Flagging in
case CI hits it; I could not attribute it to anything in this branch.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

Approval links are already current for 6fffd09 — plus which runs to ignore

No refreshed request is needed: the three links in my previous comment already point at the runs created for the current head.

✅ Approve these three — they are for head 6fffd09 (current):

Workflow Run
Build Workload 33070345680
Build Matrix 33070345658
Validate Version Map 33070345715

⚠️ Do NOT approve these — superseded heads, still sitting in action_required:

  • 9cdce68 — Build Workload / Build Matrix / Validate Version Map
  • b847341 — Build Workload / Build Matrix / Validate Version Map

Because every push from a non-member fork queues a new set awaiting approval, older sets linger in the Actions tab and look identical apart from the commit SHA. Approving one of those would run CI against superseded code and report a result that does not describe the current head — worth knowing before clicking, since the older heads lack every fix from 6fffd09.

The GitHub Actions run list can be filtered to the current head with:

https://github.com/Samsung/Tizen.NET/actions?query=branch%3Aredth-net11-tizen-workload

then matching the commit SHA column against 6fffd09.

Head is being held stable at 6fffd09 pending the exact-head reviews; I will not push unless a reviewer reports a real blocker.

1. PowerShell empty versions
   An empty or all-blank versions[] made `Select-Object -Last 1` yield $null, so
   Get-LatestVersion returned "<id>=" - a TRUTHY string. The caller then split off an
   empty version, and the NuGet v2 package endpoint serves the LATEST package when the
   URL has no version segment, silently installing an arbitrary version. Blank entries
   are now filtered, an empty result falls through to the retry/version-map path, and the
   caller rejects an empty id or version outright. The shell installer gained the same
   guard before the download.

2. Shell SDK pinning
   install_tizenworkload is invoked under `if !`, which disables errexit for everything it
   calls, so the unchecked `dotnet new globaljson` let the install proceed against whatever
   SDK PATH resolved. The pin now uses the dotnet under test, its exit status is checked,
   and the EFFECTIVE `dotnet --version` and feature band are re-verified against the
   requested ones before any pack is installed. Verified with a stub whose pin silently
   does not take effect: the run aborts non-zero and never reaches the install.

3. Release versioning / resume
   The bump was committed and pushed BEFORE the build, so any later failure left the branch
   already bumped and the retry computed OLD == NEW and aborted - an unretryable release.
   Persistence now happens only after the build succeeds and the expected artifacts are
   verified on disk. OLD == NEW resumes instead of aborting, since next-workload-version.py
   derives from what is published on NuGet and the branch may legitimately already carry the
   intended unpublished version. Reference-only runs are fully non-mutating (no bump, no
   commit, no tag), and re-creating an existing tag is a no-op.

4. Fallback priority and atomicity
   PackageTargetFallback is an ordered preference list, but FixupNuGetReferences collected
   every matching directory into an unordered HashSet populated in filesystem-enumeration
   order and then took assemblies first-wins across all of them. That could ignore the
   declared priority and mix assemblies from different TFMs within one package. Candidates
   are now ranked by their position in the list, exactly ONE fallback TFM is selected per
   package, and every substituted assembly comes from that single directory.

5. Self-contained matrix outcome
   An unexpected diagnostic warned and passed. Self-contained has exactly one supported
   outcome, so anything other than TIZENSDK001 - including NETSDK1083 or an accidental
   success - now fails CI, as does being unable to prepare the fixture.

Tests: new scripts/test-release-workflow.sh (14 assertions covering ordering, retryability
and non-mutating reference-only runs); test-package-fallback.sh gains priority/atomicity
cases built in BOTH directory-creation orders so the assertion does not depend on
filesystem enumeration; test-install-failure.sh gains SDK-pin verification (effective and
ineffective) and empty-response guards for both installers.

Validation: make check green - C1-C8 plus 135 assertions (11 self-test, 61 version-band,
13 template-condition, 12 package-fallback, 14 release-workflow, 24 install-failure),
all under bash 3.2, including installs into a space-containing path, an unreachable feed,
and a pin that does not take effect.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

New head c26d236 — all five exact-head blockers fixed

Maintainer action required: approve fork workflows for head c26d236 ONLY

Workflow Run
Build Workload 33077331106
Build Matrix 33077331324
Validate Version Map 33077331204

⚠️ Runs for 6fffd09, 9cdce68 and b847341 are also queued in action_required and must not be approved — they would test superseded code.

1. PowerShell empty versions

An empty/all-blank versions[] made Select-Object -Last 1 yield $null, so the function returned "<id>="truthy. The caller split off an empty version, and the NuGet v2 endpoint serves the latest package for a versionless URL. Blank entries are now filtered, an empty result falls through to the retry/version-map path, and the caller rejects an empty id or version. The shell installer gained the same guard before the download.

2. Shell SDK pinning

install_tizenworkload runs under if !, which disables errexit for everything it calls — so the unchecked dotnet new globaljson let the install proceed on the wrong SDK. The pin now uses the dotnet under test, its exit status is checked, and the effective dotnet --version and feature band are re-verified before any pack is installed.

Verified with a stub whose pin silently doesn't take effect:

SDK pin did not take effect: requested 10.0.100, active 9.0.100.
FAILED to install Tizen workload for sdk(s): 10.0.100
exit=1

REACHED_INSTALL never printed. The matching effective-pin case still installs normally.

3. Release versioning / resume

Persistence now happens only after the build succeeds and artifacts are verified. OLD == NEW resumes instead of aborting — next-workload-version.py derives from what's published on NuGet, so the branch may legitimately already carry the intended unpublished version. Reference-only runs are fully non-mutating; re-creating an existing tag is a no-op. Order is pinned by 14 assertions.

4. Fallback priority and atomicity

FixupNuGetReferences collected every match into an unordered HashSet (populated in filesystem-enumeration order) and took assemblies first-wins across all of them. It now ranks candidates by their position in PackageTargetFallback, selects exactly one fallback TFM per package, and takes every substituted assembly from that single directory.

One thing I want to be straight about: the new tests build the candidate directories in both creation orders so the assertion can't depend on filesystem enumeration, and they pass. But I could not construct a case where the old implementation demonstrably failed on this machine — directory enumeration here happened to favour the correct answer in every variant I tried, including reversed creation order and a package where only the lower-priority TFM carried the second assembly. So the tests lock in the correct contract going forward, but I can't claim to have empirically reproduced the mis-selection; the fix rests on the code path being order-dependent by construction, which the diff makes explicit.

5. Self-contained matrix outcome

An unexpected diagnostic previously warned and passed. Anything other than TIZENSDK001 — including NETSDK1083 or an accidental success — now fails CI, as does being unable to prepare the fixture.

Validation

make check green: C1–C8 + 135 assertions — 11 self-test, 61 version-band, 13 template-condition, 12 package-fallback, 14 release-workflow, 24 install-failure — all under bash 3.2, including installs into a space-containing path, an unreachable feed, and a pin that doesn't take effect.

The 9-row TFM matrix and self-contained assertion were verified on 11.0.100-preview.7.26381.103 at 6fffd09; this head changes the installers, the fallback task, the release workflow and tests, and make check covers all of them. Full-matrix re-verification will come from the CI runs above once approved.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

Closing my own caveat on blocker #4 — now reproduced on Linux

In my previous comment I flagged that I could not empirically reproduce the fallback mis-selection, only reason about it. Since the re-review is exercising randomized fallback selection on Linux, I reproduced it there. No code change — head remains c26d236.

Root cause of my earlier failure to reproduce: directory enumeration order differs by OS.

Platform Directory.EnumerateDirectories order
macOS (APFS) reverse creation order
Linux (ext4 / overlayfs) creation order

Every variant I built on macOS happened to enumerate the higher-priority TFM first, so the pre-fix task produced the right answer by luck.

Running the identical fixture in mcr.microsoft.com/dotnet/sdk:8.0, with the low-priority directory created first and PackageTargetFallback="net9.0-tizen9.0;net6.0-tizen8.0":

dirent order: netstandard2.0 net6.0-tizen8.0 net9.0-tizen9.0
[old] RESULT|/tmp/pkg/lib/net6.0-tizen8.0/Contoso.dll;/tmp/pkg/lib/net6.0-tizen8.0/Contoso.Extras.dll   <-- WRONG TFM
[new] RESULT|/tmp/pkg/lib/net9.0-tizen9.0/Contoso.dll

And the sharper case — only the low-priority TFM carries the second assembly:

[old] TFMs used: net6.0-tizen8.0 net9.0-tizen9.0     <-- ONE PACKAGE, TWO TFMs
      RESULT|.../net6.0-tizen8.0/B.dll;.../net9.0-tizen9.0/A.dll
[new] TFMs used: net9.0-tizen9.0

So both reported failure modes are real and now demonstrated: priority ignored and assets mixed across TFMs within a single package.

The committed test suite catches it. Running scripts/test-package-fallback.sh unchanged inside the Linux container against both task versions:

Task Result
current (fixed) 12 passed, 0 failed
pre-fix (6fffd09) 8 passed, 4 failed — incl. no mixing across TFMs [forward] ... saw: net9.0-tizen9.0 net6.0-tizen8.0

The [forward]/[reverse] pairing added at c26d236 is what makes this platform-independent: whichever way the filesystem enumerates, one of the two runs presents a lower-priority directory first. On Linux the [reverse] runs fail on the old task; the [forward] mixing case fails too.

I have not pushed anything. I have a one-line comment improvement for test-package-fallback.sh recording the per-OS enumeration finding, but committing it would move the head and invalidate the current approval links, so I'm holding it until after the re-review. Say the word and I'll fold it in.

Approval links remain unchanged — head c26d236: Build Workload 33077331106 · Build Matrix 33077331324 · Validate Version Map 33077331204

Redth and others added 2 commits August 27, 2026 13:28
1. Partial publication resume
   The companion packs, the manifest, the tag and the GitHub release are separate
   steps. When one failed partway, version V was left partially published - and
   because next-workload-version.py derives the next version from what exists on
   NuGet, it then answered V+1. The previous logic saw OLD != NEW, advanced, and
   stranded V permanently with no tag, no release and a missing pack set.

   scripts/release-state.py now reports whether a version is unpublished, partial or
   published, and the workflow resumes V unless it is provably complete:
     * partial                      -> always resume V, never advance
     * published but no tag/release -> resume V to finish the tag/release
     * unpublished, branch has it   -> resume V
     * published AND released       -> safe to compute V+1
   A feed transport failure exits non-zero instead of reading as "unpublished", which
   would otherwise let a network blip advance the version and strand the release.

   Two supporting changes make resumption sound. The manifest is now pushed LAST,
   after its companion packs: it is what next-workload-version.py keys off, so an
   interruption can no longer leave a version that looks advanceable while its packs
   are missing. And publication completeness is re-verified against the feed (with
   retries for propagation lag) before the tag is created, so a release is never
   created for an incomplete publication. Every push already used --skip-duplicate,
   so re-running publishes only the missing artifacts.

2. Explicit empty -Version
   An explicit -Version "" (or whitespace) does not equal "<latest>", so it bypassed
   the resolution-and-validation block entirely, reached the manifest removal, and
   built a versionless NuGet URL - and the v2 package endpoint serves the LATEST
   package for such a URL. Verified against the pre-fix script: it exited 0, REMOVED
   the installed manifest, and reported "Done installing Tizen workload " with an
   empty version. The gate is now unconditional, before any removal, URL construction
   or download.

Tests: test-release-workflow.sh gains resume/ordering assertions plus failure
injection at each publication step against the real release-state.py over a local
file:// feed (interrupted after the manifest, after one companion pack, before the
last pack, and an unreachable feed). test-install-failure.sh gains an explicit
empty/whitespace -Version regression asserting no download, no dotnet invocation and
no removal of the installed manifest. line_of() in the workflow test now fails loudly
on a missing step anchor - it caught a stale reference left by the push-step split.

Validation: make check green - C1-C8 plus 150 assertions (11 self-test, 61
version-band, 13 template-condition, 12 package-fallback, 27 release-workflow, 26
install-failure).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Two issues found reviewing the resume machinery added in the previous commit.

* The staging step recomputed the SDK feature band instead of using the value the
  resolve step had already derived, leaving two places that could drift. Staging now
  consumes steps.bump.outputs.band and fails if it is absent, so
  compute_target_version_band is invoked in exactly one place in the workflow.

* Nothing tied release-state.py's expected package set to the packs the workflow
  actually pushes. Adding a pack to the push steps without adding it there would make
  a version that is missing it report "published", get tagged, and never be completed
  - precisely the failure the resume machinery exists to prevent. A new assertion
  extracts both lists and compares them; removing Samsung.Tizen.Templates from
  release-state.py fails it with "untracked: Samsung.Tizen.Templates".

Validation: make check green - C1-C8 plus 152 assertions (11 self-test, 61
version-band, 13 template-condition, 12 package-fallback, 29 release-workflow, 26
install-failure).

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

New head 47b34b2 — both release blockers fixed

Maintainer action required: approve fork workflows for 47b34b2 ONLY

Workflow Run
Build Workload 33117630487
Build Matrix 33117630598
Validate Version Map 33117630671

⚠️ Sets for c26d236, 6fffd09, 9cdce68 and b847341 are also queued in action_required — approving one would test superseded code.

1. Idempotent resume of a partially published version

Publication is not atomic. Once V's manifest is on NuGet, next-workload-version.py answers V+1, so the old logic advanced and stranded V with no tag, no release and a missing pack set.

New workload/scripts/release-state.py classifies V as unpublished / partial / published, and the workflow resumes V unless it is provably complete:

State of the branch's version Action
partial always resume V — never advance
published, no tag/release resume V to finish the tag/release
unpublished, branch already carries it resume V
published and released safe to compute V+1

Three properties make the resume sound rather than cosmetic:

  • The manifest is published LAST, after its companion packs. It is what the version derivation keys off, so an interruption can no longer leave a version that looks advanceable while its packs are missing.
  • Every push is idempotent (--skip-duplicate). NuGet packages are immutable, so an id+version that already exists is the intended artifact and is skipped; a resumed run publishes only what's missing.
  • Completeness is re-verified against the feed before tagging, with retries for propagation lag. A transport failure exits non-zero rather than reading as unpublished — otherwise a network blip would advance the version and strand the very release this protects.

Failure injection against the real script over a local file:// feed: interrupted after the manifest → partial; after one companion pack → partial; before the last pack → partial; all present → published; an unrelated version unaffected; unreachable feed → exit 2.

2. Explicit empty/whitespace -Version

An explicit -Version "" does not equal "<latest>", so it bypassed resolution and validation entirely. The gate is now unconditional, before any removal, URL construction or download.

The pre-fix behaviour was worse than "reaches a versionless URL" — verified directly against the c26d236 script:

FAIL  -Version ''    exit=0 (expected rejection with no side effects)
      | installed manifest was REMOVED
      | Done installing Tizen workload          <-- empty version, reported SUCCESS

It exited 0, deleted the installed manifest, and completed an "install" of whatever the versionless endpoint served. Now exit 1, no dotnet invocation, sentinel manifest intact, for both "" and " ".

From my own re-review (not reported by either reviewer)

  • The band was computed in two places — the staging step recomputed it instead of using the resolve step's value. Now single-sourced via steps.bump.outputs.band; compute_target_version_band appears exactly once in the workflow, asserted by a test.
  • Nothing tied release-state.py's expected package set to what the workflow pushes. Adding a pack to the push steps without adding it there would make a version missing that pack report published, get tagged, and never complete — exactly the failure this machinery prevents. A new assertion extracts both lists and compares them; removing Samsung.Tizen.Templates from release-state.py fails it with untracked: Samsung.Tizen.Templates.
  • An earlier iteration's stale step anchor was caught by the workflow test itself; line_of() now fails loudly on a missing anchor instead of silently yielding an empty value that turns ordering assertions into confusing failures.

Validation

make check green: C1–C8 + 152 assertions — 11 self-test, 61 version-band, 13 template-condition, 12 package-fallback, 29 release-workflow, 26 install-failure. All shell tests run under bash 3.2.

Requesting exact-head code + MSBuild review at 47b34b2.

…t replacement

Addresses the ten blockers from the exact-head code and MSBuild reviews of 47b34b2.

Release provenance and ownership
 1. Packages embed the current Git SHA, so building before the version-bump commit
    meant an initial run and its retry produced packages from different commits, and
    skip-duplicate preserved the mixed set under a tag pointing at only one of them.
    The release commit is now created and reserved BEFORE any artifact is built, and
    a resume rebuilds from that exact SHA only. Every staged package is verified to
    carry the reserved SHA before publish or tag. This deliberately reverses the
    earlier 'commit after a verified build' ordering: retryability now comes from the
    reservation rather than from deferring the commit.
 2. The next version was not atomically reserved, so concurrent branches or bands
    could select the same version and let skip-duplicate silently accept one owner's
    packs. Added a repository-wide concurrency group, an atomic reservation ref
    refs/tizen-release/v<version>, and an ownership check that aborts on a foreign
    reservation. Retries must prove they own V.
 5. workflow_dispatch re-runs check out the original event SHA, not the pushed bump
    commit. Every step now checks out the reserved SHA explicitly and rejects drift.
 6. NEW was selected and then re-queried during --apply, so a concurrent release
    could change it. next-workload-version.py gained --set-version so one immutable
    candidate is computed once and passed through explicitly.
 3. Per-package feed query failures were treated as empty, letting the derived
    version silently drop below the true maximum. Any transport, status or parse
    failure now aborts.

Installer band selection
 9. The fallback selected the closest band ABOVE the request (10.0.200 -> 10.0.300),
    skipping the published 10.0.200 manifest. Both installers now sort bands
    numerically and pick the closest compatible band <= the request.
 7. -t / -Tizen is documented as supporting cross-band installation, but the
    'target band must equal the active SDK band' rule was applied to explicit
    requests too, and only after the manifest had been copied. Equality now applies
    only to an auto-derived band, and all inputs are validated before any mutation.
 8. install_tizenworkload runs under 'if !', which suppresses errexit, so an
    unchecked SDK pin could let the install proceed against the wrong SDK, and an
    unchecked copy could destroy a working manifest while leaving a directory that
    satisfied the later existence check. The SDK is now validated first, the payload
    is staged and verified, the destination is replaced atomically, and the previous
    manifest is rolled back on any failure.
10. DOTNET_DESTDIR and the install stamp were keyed by feature band, so
    make install reused a stale SDK that merely shared a band. Both are now keyed by
    the full SDK version; manifest paths remain band-based.

NuGet fallback
 4. FixupNuGetReference removed only filename-matching assets when applying a
    fallback, leaving extra netstandard DLLs mixed with the selected group. The
    original asset group is now replaced atomically by the selected fallback group's
    complete asset set.

Validation: make check passes C1-C8 plus 162 assertions (11 self-test, 61
version-band, 13 template-condition, 14 package-fallback, 37 release-workflow,
26 install-failure). Fallback tests randomize directory enumeration order and run
with -nodereuse:false, without which MSBuild caches a stale task assembly and the
negative cases pass against the pre-fix binary. Cross-band install, auto-band
equality, and pre-mutation failure with manifest rollback verified end to end
against a stub SDK. Shell tests run under Bash 3.2.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

New head 8080bb3 — all ten review blockers addressed

Supersedes 47b34b24. Both exact-head reviews (code + MSBuild) of 47b34b24 are covered in this single commit.

MSBuild review

# Fix
1 Release SHA provenance. The Makefile embeds the current Git SHA in every package, so building before the bump commit meant an initial run and its retry produced packages from different commits — and skip-duplicate preserved that mixed set under a tag pointing at only one. The release commit is now created and reserved before any artifact is built; a resume rebuilds from that exact SHA only, and every staged package is verified to carry it before publish or tag.
⚠️ This deliberately reverses the earlier "commit only after a verified build" ordering. That ordering can't survive a resume that has to rebuild. Retryability now comes from the reservation, not from deferring the commit.
2 Atomic version reservation. Repo-wide concurrency group (cancel-in-progress: false), atomic claim of refs/tizen-release/v<V>, and an ownership check — a retry must prove it owns V; a foreign reservation aborts.
3 Fail-closed feed queries. Any transport/status/parse failure for any authoritative package aborts instead of being read as empty and lowering the derived max.
4 Atomic asset-group replacement. FixupNuGetReference removed only filename-matching assets, leaving extra netstandard DLLs mixed in with the selected fallback group. The original group is now replaced wholesale by the selected group's complete asset set.

Code review

# Fix
5 Re-runs check out the original event SHA, not the pushed bump commit. Every step now checks out the reserved SHA explicitly and rejects source drift.
6 NEW was re-queried during --apply and could change under a concurrent release. next-workload-version.py --set-version passes one immutable candidate through explicitly.
7 -t / -Tizen is documented as cross-band, but equality with the active SDK band was enforced for explicit requests too — and only after the manifest had been copied. Equality now applies only to an auto-derived band, and all inputs are validated before any mutation.
8 install_tizenworkload runs under if !, which suppresses errexit: an unchecked SDK pin could install against the wrong SDK, and an unchecked copy could destroy a working manifest while leaving a directory that satisfied the later existence check. SDK is validated first; payload staged and verified; destination replaced atomically; previous manifest rolled back on any failure.
9 Fallback selected the closest band above the request (10.0.200 → 10.0.300), skipping the published 10.0.200. Both installers now sort bands numerically and pick the closest compatible band the request.
10 DOTNET_DESTDIR and the install stamp were keyed by feature band, so make install reused a stale SDK sharing a band. Both are now keyed by the full SDK version; manifest paths stay band-based.

Validation at 8080bb3

make checkC1–C8 + 162 assertions, 0 failures
11 self-test · 61 version-band · 13 template-condition · 14 package-fallback · 37 release-workflow · 26 install-failure. All shell tests run under stock Bash 3.2.

Additionally verified end-to-end against a stub SDK:

  • explicit -t 9.0.300 on a 10.0.100 SDK installs into 9.0.300 (exit 0), while an auto-derived band still enforces equality;
  • a failing SDK pin aborts before any mutation — pre-existing manifest and its sibling files intact, no temp residue.

Worth flagging for anyone re-running these tests: MSBuild node reuse caches the loaded task assembly across invocations. Without -nodereuse:false plus a forced rebuild, the fallback negative tests happily "pass" against the pre-fix binary. Several of my earlier negative-test conclusions were invalidated by this; the suite now disables node reuse everywhere and randomizes directory enumeration order (macOS returns reverse-creation order, Linux returns creation order — which is why the original mis-selection reproduced only under Docker).

Fork-CI approval — please approve only these three

All runs for 47b34b2, c26d236, 6fffd09, 9cdce68 and b847341 are superseded — please do not approve them.

Samsung admin approval of the three runs above remains the only external gate. Requesting exact-head code + MSBuild re-review at 8080bb3.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

Current-head workflow approval required

Please approve only the workflow runs for current head 8080bb3b8ddd8464bb0bffbd5b6b016f12621d14:

Older action_required runs target superseded heads and should remain unapproved.

@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

Do not approve the 8080bb3 workflow runs yet

Exact-head review found release-reservation/provenance and installer rollback blockers on 8080bb3b8ddd8464bb0bffbd5b6b016f12621d14. Please leave runs 33120183523, 33120183717, and 33120183755 unapproved. A new head and replacement run IDs will be posted after the fixes.

…ng and atomicity

Addresses the eight blockers and five additional findings from the exact-head
MSBuild/release review of 8080bb3.

Release resume and reservation state
 1. Re-runs could never resume their own release. 'Re-run jobs' checks out the SHA
    of the ORIGINAL event - the commit before the version bump - so the version read
    from Versions.props was the previous, already-released one. The re-run concluded
    there was nothing to resume, recomputed the same candidate, found its own
    reservation ref and aborted as though a stranger held it, leaving an interrupted
    release permanently unfinishable. Resume is now decided by ENUMERATING the
    reservation refs and matching them against this run's inputs; the checked-out
    SHA is not used for that decision at all.
 2. A reservation was only a ref pointing at a commit, so nothing tied it to the
    inputs it was made for and a redispatch with a different net_sdk_version could
    adopt it. refs/tizen-release/v<V> is now an annotated tag object carrying the
    complete input set - version, reserved SHA, SDK, band, manifest id, package set
    and branch (scripts/release-reservation.py). Any mismatch is refused, and a
    reservation predating the payload format cannot be validated so it is refused
    rather than trusted.
 3. An existing tag was treated as 'already done' on mere existence, which would
    bless a release whose tag targets a different tree than the published packages.
    The tag must now resolve to the reserved commit.

Feed queries
 4. The NuGet search index was read one page deep with no totalHits validation, so a
    band whose entry fell on a later page was invisible and the derived version
    collided with one already published. Pagination now runs to completion and any
    short, malformed or non-integer-totalHits response aborts. release-state.py read
    payload.get('versions', []), so a '{}' body made a published package look
    missing; only a genuine 404 now means 'never published'.
 5. --set-version equal to the value on disk returned non-zero. It is called outside
    a condition under set -e, so the step died and the documented OLD == NEW
    'reserve the current commit' recovery below it was unreachable. It is now an
    idempotent no-op; writing a lower version is still refused.

Installers
 6. The manifest package is named after the band it is FOR, but the id was derived
    from the running SDK and only corrected on the auto-derived path. '-t 10.0.200'
    on a 10.0.100 SDK downloaded manifest-10.0.100 and installed it into
    sdk-manifests/10.0.200 - the wrong band's manifest in the right band's
    directory, reported as success.
 7. Installing is now one transaction in both installers. The shell installer left
    global.json pinned when a download failed, so the next iteration of an
    --update-all-workloads run overwrote and destroyed the user's real
    global.json.bak; and a failed pack install left a new manifest advertising packs
    that were not on disk. PowerShell was worse: it removed the old manifest and
    packs BEFORE fetching the new ones, so a mid-way failure left no workload at
    all. Both now restore the previous state exactly, global.json included.

Self-contained
 8. The TIZENSDK001 guard keyed off the SelfContained property, which the SDK
    infers from the
    implicit Tizen RID for TFMs below 8.0 (Microsoft.NET.RuntimeIdentifierInference
    .targets: SelfContained is inferred when a RID is present and the TFM version is
    less than 8.0). Ordinary framework-dependent net6.0-tizen and net7.0-tizen
    builds - what every existing MAUI Tizen app does - therefore failed to build.
    The guard now also requires _SelfContainedWasSpecified, which the SDK sets only
    when self-contained was explicitly requested, and which the SDK itself uses for
    the same distinction.

Also
 - Package-set drift is checked in BOTH directions across the push loop,
   release-state.py and the reservation payload. The old one-way check let a package
   be tracked but never pushed, so the completeness gate could never be satisfied
   and the version could never be tagged.
 - The fallback tests re-implemented the family-prefix rule with their own sed, so
   they agreed with themselves regardless of what the installer did. The shipped
   resolver is now extracted via BEGIN/END FALLBACK RESOLVER markers and invoked
   directly; regressing band_sort_key makes the suite fail.
 - band_sort_key appended the pre-release verbatim and compared as a string, so
   preview.10 sorted below preview.9 and a preview.10 request fell back to
   preview.9. Identifiers are now normalised for SemVer precedence, sh/ps1 keys
   asserted byte-identical.
 - SDK bootstrap directories and install stamps are keyed by the FULL SDK version,
   so 10.0.100 and 10.0.101 do not share a tree; manifest paths stay band-based.
 - Two docstring headings were underlined with exactly seven '=' characters, which
   git reads as a leftover conflict marker; 'git diff --check' exited 2.

Validation: make check passes C1-C8 plus 224 assertions (11 self-test, 86
version-band, 22 template-condition, 14 package-fallback, 65 release-workflow, 26
install-failure), up from 162. Full matrices: .NET 11 SDK 10/10 passed 0 skipped
including net11.0-tizen11.0 and the self-contained guard; .NET 10 SDK 8 passed with
only the two net11 rows skipped. Search pagination, malformed-feed, OLD == NEW,
reservation-mismatch and SemVer-ordering fixes are each verified against a
reconstructed pre-fix implementation to confirm the tests detect the regression.
Cross-band install, pack-install rollback and download-failure global.json
restoration verified end to end against a stub SDK. git diff --check is clean.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@Redth

Redth commented Aug 27, 2026

Copy link
Copy Markdown
Author

New head 38aabff — all eight blockers + five additional findings

Supersedes 8080bb3. Every item from the exact-head MSBuild/release review is addressed.

Release resume & reservation state

# Fix
1 Re-runs could never resume their own release. Re-run jobs checks out the SHA of the original event — the commit before the bump — so the version read from Versions.props was the previous, already-released one. The re-run concluded there was nothing to resume, recomputed the same candidate, found its own reservation ref, and aborted as though a stranger held it. An interrupted release was permanently unfinishable. Resume is now decided by enumerating reservation refs and matching them against this run's inputs; the checked-out SHA is no longer used for that decision.
2 Reservations now bind the complete input set. refs/tizen-release/v<V> is an annotated tag object carrying version, reserved SHA, SDK, band, manifest id, package set and branch (release-reservation.py). Any mismatch is refused; a reservation predating the payload format can't be validated, so it's refused rather than trusted. Two matching unfinished reservations is an error, not a guess.
3 Tag provenance. An existing tag was accepted on mere existence, which would bless a release whose tag targets a different tree than the published packages. It must now resolve to the reserved commit.

Feed queries

# Fix
4 Search is paginated and validated. One page deep with no totalHits check meant a band on a later page was invisible and the derived version collided with one already published. Short/malformed/non-integer-totalHits responses now abort. Separately, release-state.py used payload.get("versions", []), so a {} body made a published package look missing — only a genuine 404 now means "never published".
5 OLD == NEW recovery was dead code. --set-version equal to the current value returned non-zero, and it's called outside a condition under set -e — so the step died before reaching the documented "reserve the current commit" branch. Now an idempotent no-op; writing a lower version is still refused.

Installers

# Fix
6 Manifest id now follows the target band. It was derived from the running SDK and only corrected on the auto-derived path, so -t 10.0.200 on a 10.0.100 SDK downloaded manifest-10.0.100 into sdk-manifests/10.0.200 — wrong band's manifest, right band's directory, reported as success. (This is visible in my own round-5 output; I read it as a pass. Thanks for catching it.)
7 Installing is one transaction. The shell installer left global.json pinned on a download failure — so the next --update-all-workloads iteration overwrote and destroyed the user's real global.json.bak — and a failed pack install left a manifest advertising packs not on disk. PowerShell was worse: it removed the old manifest and packs before fetching new ones, so a mid-way failure left no workload at all. Both now restore prior state exactly.
8 TIZENSDK001 no longer fires on ordinary builds. The guard keyed off SelfContained, which the SDK infers from the implicit Tizen RID for TFMs < 8.0 (Microsoft.NET.RuntimeIdentifierInference.targets L207 + L211-216). Framework-dependent net6.0-tizen/net7.0-tizen — what every existing MAUI Tizen app does — failed to build. Now also requires _SelfContainedWasSpecified, which the SDK sets only on explicit request and uses for this same distinction itself (L301).

Additional findings

  • Package-set drift is now bidirectional across the push loop, release-state.py and the reservation payload. The one-way check let a package be tracked but never pushed — the completeness gate could then never be satisfied and the version could never be tagged.
  • Fallback tests now drive the shipped resolver. They re-implemented the family-prefix rule with their own sed, so they agreed with themselves regardless of the installer. Extracted via BEGIN/END FALLBACK RESOLVER; regressing band_sort_key now makes the suite fail (verified).
  • SemVer ordering: the pre-release was appended verbatim and string-compared, so preview.10 sorted below preview.9 and a preview.10 request fell back to preview.9. Identifiers normalised; sh/ps1 keys asserted byte-identical.
  • Full-SDK cache isolation: SDK dirs/stamps keyed by full version (10.0.100 vs 10.0.101); manifest paths stay band-based.
  • git diff --check exited 2 — two docstring headings underlined with exactly seven = read as leftover conflict markers.

Validation at 38aabff

make checkC1–C8 + 224 assertions, 0 failures (up from 162): 11 self-test · 86 version-band · 22 template-condition · 14 package-fallback · 65 release-workflow · 26 install-failure.

Full matrices, both bands:

  • .NET 11 SDK — 10 passed, 0 skipped, including net11.0-tizen11.0 and the self-contained guard
  • .NET 10 SDK — 8 passed, only the two net11 rows skipped (correctly)

Pagination, malformed-feed, OLD == NEW, reservation-mismatch and SemVer ordering are each verified against a reconstructed pre-fix implementation to confirm the tests actually detect the regression. Cross-band install, pack-install rollback and download-failure global.json restoration verified end-to-end against a stub SDK. git diff --check clean.

Fork-CI approval — please approve only these three

Runs for 8080bb3, 47b34b2, c26d236, 6fffd09, 9cdce68 and b847341 are superseded — please do not approve them.

Requesting exact-head code + MSBuild re-review at 38aabff.

@JoonghyunCho
JoonghyunCho merged commit 26f5e1a into Samsung:net10.0 Aug 28, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants