From 3a9ed9b0d374ad6e15ab8752319e86610120688e Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Tue, 23 Jun 2026 23:03:07 +0000
Subject: [PATCH 1/3] Bump the nuget-deps group with 1 update (#188)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Updated [Microsoft.NET.Test.Sdk](https://github.com/microsoft/vstest)
from 18.6.0 to 18.7.0.
Release notes
_Sourced from [Microsoft.NET.Test.Sdk's
releases](https://github.com/microsoft/vstest/releases)._
## 18.7.0
## What's Changed
* Add ARM64 msdia140.dll support to test platform packages by
@jamesmcroft in https://github.com/microsoft/vstest/pull/15689
* Update System.Memory from 4.5.5 to 4.6.3 by @nohwnd in
https://github.com/microsoft/vstest/pull/15706
## New Contributors
* @jamesmcroft made their first contribution in
https://github.com/microsoft/vstest/pull/15689
**Full Changelog**:
https://github.com/microsoft/vstest/compare/v18.6.0...v18.7.0
Commits viewable in [compare
view](https://github.com/microsoft/vstest/compare/v18.6.0...v18.7.0).
[](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)
Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.
[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)
---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore ` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore ` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore ` will
remove the ignore condition of the specified dependency and ignore
conditions
Signed-off-by: dependabot[bot]
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
---
Directory.Packages.props | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4c375a3..242e3e7 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -3,7 +3,7 @@
-
+
From dd19d313460d7ae04c0e892518c18087046b18a3 Mon Sep 17 00:00:00 2001
From: Pieter Viljoen
Date: Tue, 23 Jun 2026 21:37:54 -0700
Subject: [PATCH 2/3] Re-sync from template: artifact cleanup + name-pattern
handoff
Carry the template's artifact-storage hygiene into the workflows:
- Add a terminal `cleanup-artifacts` job to publish-release.yml
(needs setup/publish/date-badge) and test-pull-request.yml
(needs smoke-build) so a run's artifacts are deleted via the REST
API instead of accumulating against the account-wide storage quota.
- Converge the github-release handoff to the canonical name-pattern
form (ptr727/ProjectTemplate#203, decided option a): build-nugetlibrary
uploads `release-asset--nugetlibrary` and drops the `artifact-id`
output/plumbing; build-release downloads by `pattern:` + `merge-multiple:`.
This keeps the github-release job a verbatim carry.
- Document both in AGENTS.md (Workflow YAML Conventions) so this
single-target repo stops drifting back to the id-based variant.
Co-Authored-By: Claude Opus 4.8 (1M context)
---
.github/workflows/build-nugetlibrary-task.yml | 19 +++++--------
.github/workflows/build-release-task.yml | 5 ++--
.github/workflows/publish-release.yml | 26 ++++++++++++++++++
.github/workflows/test-pull-request.yml | 27 +++++++++++++++++++
AGENTS.md | 2 +-
5 files changed, 63 insertions(+), 16 deletions(-)
diff --git a/.github/workflows/build-nugetlibrary-task.yml b/.github/workflows/build-nugetlibrary-task.yml
index ef90d64..f747e58 100644
--- a/.github/workflows/build-nugetlibrary-task.yml
+++ b/.github/workflows/build-nugetlibrary-task.yml
@@ -22,16 +22,12 @@ on:
branch:
required: true
type: string
- # Smoke mode: build for validation only and skip the artifact zip/upload. A PR smoke run has no consumer for
- # the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
+ # Smoke mode: build for validation only and skip the release-asset zip/upload. A PR smoke run has no consumer
+ # for the artifact (the github-release job is gated `!smoke`), so uploading it just burns artifact storage.
smoke:
required: false
type: boolean
default: false
- outputs:
- # Output of the uploaded artifact id
- artifact-id:
- value: ${{ jobs.build-nugetlibrary.outputs.artifact-id }}
jobs:
@@ -45,8 +41,6 @@ jobs:
build-nugetlibrary:
name: Build NuGet library project job
runs-on: ubuntu-latest
- outputs:
- artifact-id: ${{ steps.artifact-upload-step.outputs.artifact-id }}
needs: [get-version]
steps:
@@ -89,15 +83,14 @@ jobs:
set -euo pipefail
7z a -t7z ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }} ${{ runner.temp }}/publish/*
- # Branch-suffixed so the publisher's branch matrix can build both
- # branches in one run without colliding on the artifact name.
+ # GitHub-release asset, uploaded under the `release-asset--*` pattern that the `github-release` job
+ # collects. Branch-suffixed so the publisher can build both branches in one run without colliding on the name.
# Skipped on smoke: the github-release job is `!smoke`, so nothing would consume it.
- name: Upload build artifacts step
if: ${{ !inputs.smoke }}
- id: artifact-upload-step
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
- name: nugetlibrary-build-${{ inputs.branch }}
+ name: release-asset-${{ inputs.branch }}-nugetlibrary
path: ${{ runner.temp }}/${{ env.PROJECT_ARTIFACT }}
- # Intermediate artifact consumed by build-release-task in the same run.
+ # Consumed within this run by the github-release job; minimize artifact storage.
retention-days: 1
diff --git a/.github/workflows/build-release-task.yml b/.github/workflows/build-release-task.yml
index 11f7a3e..264b973 100644
--- a/.github/workflows/build-release-task.yml
+++ b/.github/workflows/build-release-task.yml
@@ -76,10 +76,11 @@ jobs:
with:
ref: ${{ needs.get-version.outputs.GitCommitId }}
- - name: Download library build artifacts step
+ - name: Download release asset artifacts step
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
- artifact-ids: ${{ needs.build-nugetlibrary.outputs.artifact-id }}
+ pattern: release-asset-${{ inputs.branch }}-*
+ merge-multiple: true
path: ./Publish
# The weekly publisher re-runs even with no new commits, so the version may already be released. Skip the release
diff --git a/.github/workflows/publish-release.yml b/.github/workflows/publish-release.yml
index 9c4cb33..df12ef4 100644
--- a/.github/workflows/publish-release.yml
+++ b/.github/workflows/publish-release.yml
@@ -100,3 +100,29 @@ jobs:
with:
# The badge task self-gates to `main`; the develop leg is a no-op.
branch: ${{ matrix.branch }}
+
+ # Delete the run's artifacts (durable copies live on the GitHub release) to keep them off the account storage quota.
+ cleanup-artifacts:
+ name: Delete workflow artifacts job
+ needs: [setup, publish, date-badge]
+ if: ${{ always() && needs.setup.outputs.publish == 'true' }}
+ runs-on: ubuntu-latest
+ permissions:
+ actions: write
+ steps:
+ - name: Delete workflow artifacts step
+ # Best-effort housekeeping must never fail the run.
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
+ --jq '.artifacts[].id'); then
+ echo "::warning::Could not list run artifacts; skipping cleanup."
+ ids=""
+ fi
+ for artifact_id in $ids; do
+ gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
+ || echo "::warning::Failed to delete artifact $artifact_id; continuing."
+ done
diff --git a/.github/workflows/test-pull-request.yml b/.github/workflows/test-pull-request.yml
index 72becce..3d418b2 100644
--- a/.github/workflows/test-pull-request.yml
+++ b/.github/workflows/test-pull-request.yml
@@ -115,3 +115,30 @@ jobs:
# smoke-build may be legitimately skipped (library unchanged); only failure/cancelled blocks.
exit_on_result "unit-test" "${{ needs.unit-test.result }}"
exit_on_result "smoke-build" "${{ needs.smoke-build.result }}"
+
+ # Delete any incidental artifacts a build step emitted to keep them off the account storage quota. Kept out of
+ # `check-workflow-status`'s needs so housekeeping never gates the required merge check.
+ cleanup-artifacts:
+ name: Delete workflow artifacts job
+ needs: [smoke-build]
+ if: always()
+ runs-on: ubuntu-latest
+ permissions:
+ actions: write
+ steps:
+ - name: Delete workflow artifacts step
+ # Best-effort housekeeping must never fail the run.
+ continue-on-error: true
+ env:
+ GH_TOKEN: ${{ github.token }}
+ run: |
+ set -euo pipefail
+ if ! ids=$(gh api repos/${{ github.repository }}/actions/runs/${{ github.run_id }}/artifacts --paginate \
+ --jq '.artifacts[].id'); then
+ echo "::warning::Could not list run artifacts; skipping cleanup."
+ ids=""
+ fi
+ for artifact_id in $ids; do
+ gh api --method DELETE "repos/${{ github.repository }}/actions/artifacts/$artifact_id" \
+ || echo "::warning::Failed to delete artifact $artifact_id; continuing."
+ done
diff --git a/AGENTS.md b/AGENTS.md
index 28a2184..60e2e0e 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -173,7 +173,7 @@ These conventions describe the target state. New and modified workflows must res
- **Boolean inputs**: workflows triggered both via `workflow_call` and `workflow_dispatch` must declare each boolean input in *both* trigger blocks - one definition does not propagate to the other. `workflow_call` delivers booleans as actual booleans; `workflow_dispatch` delivers them as the *strings* `"true"`/`"false"`. Any `if:` consuming a boolean input must compare against both forms - `if: ${{ inputs.foo == true || inputs.foo == 'true' }}`.
- **Reusable workflows**: job-level `permissions:` are validated *before* the `if:` evaluates, so even a skipped job needs valid permissions declared. A `release` job with `permissions: contents: write` and `if: ${{ inputs.publish }}` will still cause `startup_failure` on a caller that doesn't grant `contents: write`. Either declare permissions at the call site, or omit the inner block and inherit.
- **Allowlist `success` and `skipped` explicitly** when chaining jobs across optional dependencies - `!= 'failure'` lets `cancelled` through (timeout, runner failure, manual cancel). Use `(needs.X.result == 'success' || needs.X.result == 'skipped')`.
-- **Artifact retention**: intermediate build artifacts (`actions/upload-artifact`) are consumed by a later job in the same run, so set `retention-days: 1` - the default 90-day retention otherwise piles up against the account-wide artifact-storage quota. The durable copies live on the GitHub release, not in workflow artifacts.
+- **Artifact handoff and cleanup**: a build job contributes files to the GitHub release by uploading an artifact named `release-asset--`; the verbatim `github-release` job collects every `release-asset--*` by `pattern:` + `merge-multiple:` and never names a build job. **This name-pattern handoff is canonical even for this single-target repo** - do not switch to an `artifact-id` output plus `download-artifact` `artifact-ids:`, which looks tidier for 1:1 but forks the `github-release` download and breaks its verbatim carry. Artifacts are an intra-run handoff (durable copies live on the GitHub release, not in workflow artifacts), so every artifact-producing workflow ends with a terminal `cleanup-artifacts` job that deletes the run's artifacts via the REST API - `permissions: actions: write`, an `if:` that includes `always()`, `continue-on-error: true` on the delete step, kept out of any required status check so housekeeping never gates a merge; both [`publish-release.yml`](./.github/workflows/publish-release.yml) and [`test-pull-request.yml`](./.github/workflows/test-pull-request.yml) carry one. Set `retention-days: 1` on explicit uploads as a backstop.
- **Tag pinning on releases**: when using `softprops/action-gh-release` (or any tag-creating action), pass `target_commitish` explicitly - without it, GitHub's REST API defaults the new tag to the repository's default branch instead of the commit that built the artifact. Pin it to the **exact built commit's SHA** (the publisher uses NBGV's `GitCommitId` output), not `github.sha` (wrong branch in the publisher's branch matrix - a `develop` leg runs with `github.sha` = main's tip) and not a branch name (a moving ref that a mid-run commit could advance past the built tree).
## Project Structure
From e104dbb5eee5ba5509b8799c21d1dc8858153311 Mon Sep 17 00:00:00 2001
From: Pieter Viljoen
Date: Tue, 23 Jun 2026 21:38:05 -0700
Subject: [PATCH 3/3] Remove noise comments from parser and lookup
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Strip inline comments that only restate self-evident code (`// Done`,
`// Sort variants`, `// Add … tag`, the SetCase casing labels, etc.)
per the AGENTS.md "comment only when the code does not explain itself"
rule. RFC 5646 section URLs, ABNF grammar blocks, fallback-chain labels,
and any "why" notes are kept.
Removing a trailing `// Done` unblocked analyzer IDE0046 at the end of
Validate(), so the final `if (…) return false; return true;` collapses
to a single `return !string.IsNullOrEmpty(…);` (required: the husky gate
runs `dotnet format style --severity=info`).
Co-Authored-By: Claude Opus 4.8 (1M context)
---
LanguageTags/LanguageLookup.cs | 7 ------
LanguageTags/LanguageTagParser.cs | 41 +------------------------------
2 files changed, 1 insertion(+), 47 deletions(-)
diff --git a/LanguageTags/LanguageLookup.cs b/LanguageTags/LanguageLookup.cs
index 421f77c..3600cc1 100644
--- a/LanguageTags/LanguageLookup.cs
+++ b/LanguageTags/LanguageLookup.cs
@@ -27,7 +27,6 @@ public sealed class LanguageLookup
try
{
- // Get a CultureInfo representation
CultureInfo cultureInfo = CultureInfo.GetCultureInfo(languageTag, true);
// Make sure the culture was not custom created
@@ -173,7 +172,6 @@ public string GetIsoFromIetf(string languageTag)
Iso6393Record? iso6393 = _iso6393.Find(languageTag, false);
if (iso6393 != null)
{
- // Return the Part 2B code
return iso6393.Part2B!;
}
@@ -181,7 +179,6 @@ public string GetIsoFromIetf(string languageTag)
Iso6392Record? iso6392 = _iso6392.Find(languageTag, false);
if (iso6392 != null)
{
- // Return the Part 2B code
return iso6392.Part2B!;
}
@@ -197,7 +194,6 @@ public string GetIsoFromIetf(string languageTag)
iso6393 = _iso6393.Find(cultureInfo.ThreeLetterISOLanguageName, false);
if (iso6393 != null)
{
- // Return the Part 2B code
return iso6393.Part2B!;
}
@@ -232,7 +228,6 @@ public bool IsMatch(string prefix, string languageTag)
// The tag matches the prefix exactly
if (languageTag.Equals(prefix, StringComparison.OrdinalIgnoreCase))
{
- // Exact match
return true;
}
@@ -242,7 +237,6 @@ public bool IsMatch(string prefix, string languageTag)
&& languageTag[prefix.Length..].StartsWith('-')
)
{
- // Prefix match
return true;
}
@@ -258,7 +252,6 @@ public bool IsMatch(string prefix, string languageTag)
!string.Equals(languageTag, subtag.TagValue, StringComparison.OrdinalIgnoreCase)
)
{
- // Rematch
languageTag = subtag.TagValue;
continue;
}
diff --git a/LanguageTags/LanguageTagParser.cs b/LanguageTags/LanguageTagParser.cs
index 909b638..7b0ddbb 100644
--- a/LanguageTags/LanguageTagParser.cs
+++ b/LanguageTags/LanguageTagParser.cs
@@ -30,7 +30,6 @@ private string ParseGrandfathered(string languageTag)
// Grandfathered and Redundant Registrations
// https://www.rfc-editor.org/rfc/rfc5646#section-2.2.8
- // Search tag registry
// Type = Grandfathered, Tag = i-navajo, PreferredValue = nv
List recordList =
[
@@ -47,25 +46,21 @@ .. _rfc5646.RecordList.Where(record =>
return recordList[0].PreferredValue!;
}
- // No match
return languageTag;
}
private static void SetCase(LanguageTag languageTag)
{
- // Language lowercase
if (!string.IsNullOrEmpty(languageTag.Language))
{
languageTag.Language = languageTag.Language.ToLowerInvariant();
}
- // Extended language lowercase
if (!string.IsNullOrEmpty(languageTag.ExtendedLanguage))
{
languageTag.ExtendedLanguage = languageTag.ExtendedLanguage.ToLowerInvariant();
}
- // Script title case
if (!string.IsNullOrEmpty(languageTag.Script))
{
languageTag.Script = CultureInfo.InvariantCulture.TextInfo.ToTitleCase(
@@ -73,34 +68,28 @@ private static void SetCase(LanguageTag languageTag)
);
}
- // Region uppercase
if (!string.IsNullOrEmpty(languageTag.Region))
{
languageTag.Region = languageTag.Region.ToUpperInvariant();
}
- // Variants lowercase
for (int i = 0; i < languageTag._variants.Count; i++)
{
languageTag._variants[i] = languageTag._variants[i].ToLowerInvariant();
}
- // Extensions lowercase and normalize
for (int i = 0; i < languageTag._extensions.Count; i++)
{
languageTag._extensions[i] = languageTag._extensions[i].Normalize();
}
- // Private use lowercase and normalize
languageTag.PrivateUse = languageTag.PrivateUse.Normalize();
}
private static void Sort(LanguageTag languageTag)
{
- // Sort variants
languageTag._variants.Sort();
- // Sort extensions by prefix
languageTag._extensions.Sort((x, y) => x.Prefix.CompareTo(y.Prefix));
// Note: Extension tags and private use tags are already sorted by Normalize()
@@ -136,7 +125,6 @@ private bool ParseLanguage()
_languageTag.Language = _tagList[0];
_tagList.RemoveAt(0);
- // Done
return true;
}
@@ -159,14 +147,12 @@ private bool ParseExtendedLanguage()
// Language is 2 or 3 chars
if (!ValidateExtendedLanguage(_tagList[0]) || _languageTag.Language.Length is < 2 or > 3)
{
- // Done
return true;
}
_languageTag.ExtendedLanguage = _tagList[0];
_tagList.RemoveAt(0);
- // Done
return true;
}
@@ -188,14 +174,12 @@ private bool ParseScript()
// Qaaa - Qabx reserved private use
if (!ValidateScript(_tagList[0]))
{
- // Done
return true;
}
_languageTag.Script = _tagList[0];
_tagList.RemoveAt(0);
- // Done
return true;
}
@@ -225,14 +209,12 @@ private bool ParseRegion()
// 3 digit UN M.49
if (!ValidateRegion(_tagList[0]))
{
- // Done
return true;
}
_languageTag.Region = _tagList[0];
_tagList.RemoveAt(0);
- // Done
return true;
}
@@ -263,7 +245,6 @@ private bool ParseVariant()
// begin with digit 4 = 8 chars
if (!ValidateVariant(_tagList[0]))
{
- // Done
return true;
}
@@ -273,12 +254,10 @@ private bool ParseVariant()
return false;
}
- // Add variant tag
_languageTag._variants.Add(_tagList[0]);
_tagList.RemoveAt(0);
}
- // Done
return true;
}
@@ -310,7 +289,6 @@ private bool ParseExtension()
// 1 char (not x)
if (!ValidateExtensionPrefix(_tagList[0]))
{
- // Done
return true;
}
@@ -342,7 +320,6 @@ private bool ParseExtension()
return false;
}
- // Add extension tag
extensionTags.Add(_tagList[0]);
_tagList.RemoveAt(0);
}
@@ -353,11 +330,9 @@ private bool ParseExtension()
return false;
}
- // Add extension tag
_languageTag._extensions.Add(new ExtensionTag(prefix, extensionTags));
}
- // Done
return true;
}
@@ -381,7 +356,6 @@ private bool ParsePrivateUse()
// x-[private]-[private]
if (!ValidatePrivateUsePrefix(_tagList[0]))
{
- // Done
return true;
}
@@ -403,7 +377,6 @@ private bool ParsePrivateUse()
// Collect all private use tags
List privateTags = [];
- // Read all tags
while (_tagList.Count > 0)
{
// 1 to 8 chars
@@ -419,7 +392,6 @@ private bool ParsePrivateUse()
return false;
}
- // Add private use tag
privateTags.Add(_tagList[0]);
_tagList.RemoveAt(0);
}
@@ -430,10 +402,8 @@ private bool ParsePrivateUse()
return false;
}
- // Create private use tag
_languageTag.PrivateUse = new PrivateUseTag(privateTags);
- // Done
return true;
}
@@ -456,7 +426,6 @@ private bool ParsePrivateUse()
["-" privateuse]
*/
- // Init
_languageTag = new();
_tagList.Clear();
string originalTag = languageTag;
@@ -478,7 +447,6 @@ private bool ParsePrivateUse()
// Grandfathered
languageTag = ParseGrandfathered(languageTag);
- // Split by -
_tagList.AddRange([.. languageTag.Split('-')]);
if (_tagList.Count == 0)
{
@@ -744,7 +712,6 @@ .. _rfc5646.RecordList.Where(record =>
Log.LogNormalizedTag(originalTag, normalizedTag);
}
- // Done
return normalizeTag;
}
@@ -827,12 +794,6 @@ internal static bool Validate(LanguageTag languageTag)
}
// No empty tags
- if (string.IsNullOrEmpty(languageTag.ToString()))
- {
- return false;
- }
-
- // Done
- return true;
+ return !string.IsNullOrEmpty(languageTag.ToString());
}
}