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).
[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=Microsoft.NET.Test.Sdk&package-manager=nuget&previous-version=18.6.0&new-version=18.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()); } }