Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,3 @@
# Security-sensitive automation and supply-chain config.
/.github/ @twcclegg
/.github/workflows/ @twcclegg
/appveyor.yml @twcclegg
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ concurrency:
cancel-in-progress: false

# Elevated above the read-only default because the update script pushes a commit
# and creates a GitHub release via GITHUB_TOKEN.
# and creates a GitHub release via GITHUB_TOKEN. actions: write lets it dispatch
# publish_nuget.yml, which the release's own push event cannot trigger.
permissions:
contents: write
actions: write

jobs:
create_new_release_on_new_metadata_update:
Expand Down
4 changes: 2 additions & 2 deletions .github/workflows/post_performance_test_comment.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ name: post_performance_test_comment

on:
workflow_run:
workflows: [ "run_performance_tests_windows" ]
workflows: [ "run_performance_tests" ]
types: [ completed ]

permissions:
Expand Down Expand Up @@ -78,7 +78,7 @@ jobs:
'<!-- benchmark-results-marker -->',
'## 📊 Benchmark Results',
'',
`> Commit: \`${sha}\` · [Full run](${runUrl}) · Windows \`windows-latest\``,
`> Commit: \`${sha}\` · [Full run](${runUrl}) · Linux \`ubuntu-24.04-arm\``,
'',
'<details open>',
'<summary><b>PR branch</b></summary>',
Expand Down
83 changes: 83 additions & 0 deletions .github/workflows/publish_nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
name: publish_nuget

# Packs and publishes both packages. Fired by the vX.Y.Z tag that
# create_new_release_on_new_metadata_update.yml pushes; the tag is the version.
# Tests are not re-run here - a tag only points at a commit already on main.

on:
push:
tags: [ 'v*' ]
# Lets a failed publish be retried by dispatching against the tag.
workflow_dispatch:

# Never cancel a publish in flight; --skip-duplicate makes a repeat run harmless.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: read

jobs:
publish_nuget:
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
permissions:
contents: read
# NuGet/login exchanges this workflow's OIDC token for a short-lived
# nuget.org api key, so no push secret is stored in the repo.
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Setup .NET
uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: 10.x

# Fail closed on a non-release ref rather than publishing the csproj placeholder.
- name: Resolve package version from tag
run: |
tag="${GITHUB_REF_NAME}"
if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]
then
echo "error: expected a vX.Y.Z tag, got '${tag}'" >&2
exit 1
fi
echo "VERSION=${tag#v}" >> "${GITHUB_ENV}"
echo "publishing version ${tag#v}" >> "${GITHUB_STEP_SUMMARY}"

- name: Restore dependencies
run: dotnet restore csharp --source https://api.nuget.org/v3/index.json

- name: Pack
run: |
dotnet pack csharp/PhoneNumbers -c Release --no-restore \
-p:VersionPrefix="${VERSION}" -o artifacts
dotnet pack csharp/PhoneNumbers.Extensions -c Release --no-restore \
-p:VersionPrefix="${VERSION}" -o artifacts

- name: Upload packages
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: nuget-packages
path: artifacts/*.nupkg
if-no-files-found: error

- name: NuGet login
id: nuget_login
uses: NuGet/login@8d196754b4036150537f80ac539e15c2f1028841 # v1.2.0
with:
user: twcclegg

# --skip-duplicate so a re-run after a partial failure pushes only what is missing.
- name: Push to nuget.org
env:
NUGET_API_KEY: ${{ steps.nuget_login.outputs.NUGET_API_KEY }}
run: |
dotnet nuget push "artifacts/*.nupkg" \
--api-key "${NUGET_API_KEY}" \
--source https://api.nuget.org/v3/index.json \
--skip-duplicate
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ permissions:

jobs:
run_all_tests_and_upload_code_coverage:
runs-on: windows-latest
runs-on: ubuntu-24.04-arm
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: run_performance_tests_windows
name: run_performance_tests

on:
pull_request:
Expand All @@ -22,8 +22,8 @@ permissions:
contents: read

jobs:
run_performance_tests_windows:
runs-on: windows-latest
run_performance_tests:
runs-on: ubuntu-24.04-arm
timeout-minutes: 30
steps:
- name: Checkout
Expand All @@ -47,9 +47,11 @@ jobs:
# actions/cache/restore requires `key`, but we never expect an exact hit:
# save keys carry a unique -<run_id> suffix, so this is a deliberate near-miss
# and the prefix restore-key below always supplies the most recently created baseline.
key: benchmark-main-
# The prefix names the runner: timings are only comparable against a baseline
# measured on the same hardware, so changing runner must change the key.
key: benchmark-main-linux-arm64-
restore-keys: |
benchmark-main-
benchmark-main-linux-arm64-

- name: Run benchmarks
run: dotnet run -c Release --framework net10.0 -- --filter "*"
Expand All @@ -58,14 +60,14 @@ jobs:
# On push to main (or the keep-warm schedule), publish this run's results as the
# new baseline for future PRs.
- name: Stage main baseline
if: github.event_name != 'pull_request'
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path "main-baseline" | Out-Null
$resultsDir = "csharp/PhoneNumbers.PerformanceTest/BenchmarkDotNet.Artifacts/results"
if (Test-Path $resultsDir) {
Copy-Item "$resultsDir/*-report-github.md" "main-baseline/" -ErrorAction SilentlyContinue
}
mkdir -p main-baseline
results_dir="csharp/PhoneNumbers.PerformanceTest/BenchmarkDotNet.Artifacts/results"
if [ -d "${results_dir}" ]
then
cp "${results_dir}"/*-report-github.md main-baseline/ 2> /dev/null || true
fi
if: github.event_name != 'pull_request'

- name: Save main benchmark baseline
if: github.event_name != 'pull_request'
Expand All @@ -76,33 +78,34 @@ jobs:
# cannot overwrite an existing key, and the keep-warm cron never restores the
# baseline, so reusing benchmark-main-<sha> would let the timer lapse when main
# is unchanged. The PR prefix restore-key picks the most recently created entry.
key: benchmark-main-${{ github.sha }}-${{ github.run_id }}
key: benchmark-main-linux-arm64-${{ github.sha }}-${{ github.run_id }}

# On a PR, bundle the freshly-measured branch results with the restored main
# baseline so the follow-up workflow can post a side-by-side comparison.
- name: Stage benchmark artifact
if: github.event_name == 'pull_request'
shell: pwsh
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
# Head SHA, not the merge SHA in GITHUB_SHA.
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
$stage = "benchmark-artifact"
New-Item -ItemType Directory -Force -Path "$stage/branch-results" | Out-Null
New-Item -ItemType Directory -Force -Path "$stage/main-results" | Out-Null
stage="benchmark-artifact"
mkdir -p "${stage}/branch-results" "${stage}/main-results"

$prDir = "csharp/PhoneNumbers.PerformanceTest/BenchmarkDotNet.Artifacts/results"
if (Test-Path $prDir) {
Copy-Item "$prDir/*-report-github.md" "$stage/branch-results/" -ErrorAction SilentlyContinue
}
if (Test-Path "main-baseline") {
Copy-Item "main-baseline/*-report-github.md" "$stage/main-results/" -ErrorAction SilentlyContinue
}
pr_dir="csharp/PhoneNumbers.PerformanceTest/BenchmarkDotNet.Artifacts/results"
if [ -d "${pr_dir}" ]
then
cp "${pr_dir}"/*-report-github.md "${stage}/branch-results/" 2> /dev/null || true
fi
if [ -d "main-baseline" ]
then
cp main-baseline/*-report-github.md "${stage}/main-results/" 2> /dev/null || true
fi

# PR metadata for the follow-up workflow (use head SHA, not the merge SHA in GITHUB_SHA)
@{
pr_number = ${{ github.event.pull_request.number }}
head_sha = "${{ github.event.pull_request.head.sha }}"
} | ConvertTo-Json | Set-Content "$stage/pr-info.json"
jq -n --argjson pr_number "${PR_NUMBER}" --arg head_sha "${HEAD_SHA}" \
'{pr_number: $pr_number, head_sha: $head_sha}' > "${stage}/pr-info.json"

Get-ChildItem -Recurse $stage | Select-Object FullName
find "${stage}" -type f

- name: Upload benchmark artifact
if: github.event_name == 'pull_request'
Expand Down
12 changes: 7 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ The library tracks upstream metadata releases (~every two weeks) via the `create
- `csharp/PhoneNumbers.PerformanceTest/` — BenchmarkDotNet harness.
- `csharp/PhoneNumbers.MetadataBuilder/` — build-time tool that converts XML metadata + geocoding/timezone text files into per-region binary files. Source-links a small set of files from `PhoneNumbers/` so it doesn't depend on (and can't cycle with) the main library at build time.
- `resources/` — XML metadata (`PhoneNumberMetadata.xml`, `ShortNumberMetadata.xml`, `PhoneNumberAlternateFormats.xml`, `PhoneNumberMetadataForTesting.xml`), plus `geocoding/`, `carrier/`, `timezones/`. **These are copied verbatim from upstream** — do not hand-edit. The library no longer reads them at runtime: the build pipeline emits binary equivalents under `obj/metadata/`, `obj/geocoding/`, `obj/timezones/` which are embedded into the published assembly.
- `lib/update.sh` + `lib/DumpLocale.java` — automation that pulls upstream resources and regenerates `csharp/PhoneNumbers/LocaleData.cs`.
- `lib/github-actions-metadata-update.sh` + `lib/DumpLocale.java` — automation that pulls upstream resources and regenerates `csharp/PhoneNumbers/LocaleData.cs`.

## Common commands

Expand Down Expand Up @@ -47,7 +47,7 @@ dotnet test csharp/PhoneNumbers.Test --filter "FullyQualifiedName~TestPhoneNumbe
dotnet test csharp/PhoneNumbers.Test --filter "FullyQualifiedName~TestPhoneNumberUtil" # whole class
```

Pack the NuGet packages (mirrors AppVeyor):
Pack the NuGet packages (mirrors `publish_nuget.yml`; the workflow adds `-p:VersionPrefix=<tag minus "v">`):

```bash
dotnet pack -c Release csharp/PhoneNumbers
Expand All @@ -65,7 +65,7 @@ dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBench
## Architecture notes that span files

- **Singleton + metadata loading.** `PhoneNumberUtil.GetInstance()` is the entry point. Region/country metadata is lazily loaded via `MetadataSource` + `IMetadataLoader` (default impl: `EmbeddedResourceMetadataLoader`, which reads per-region binary files generated at build time by `PhoneNumbers.MetadataBuilder` and embedded under `PhoneNumbers.metadata.<prefix>_<region-or-cc>`). The XML parser (`BuildMetadataFromXml.cs`) is still used at build time and by the legacy `PhoneNumberUtil(Stream)` constructor for consumers loading custom XML, but is no longer on the default load path.
- **Generated files.** `LocaleData.cs` (~48k lines) and `CountryCodeToRegionCodeMap.cs` are generated. `LocaleData.cs` is regenerated by `javac DumpLocale.java && java DumpLocale > csharp/PhoneNumbers/LocaleData.cs` (see `lib/update.sh`). Don't hand-edit either.
- **Generated files.** `LocaleData.cs` (~48k lines) and `CountryCodeToRegionCodeMap.cs` are generated. `LocaleData.cs` is regenerated by `javac DumpLocale.java && java DumpLocale > csharp/PhoneNumbers/LocaleData.cs` (see `lib/github-actions-metadata-update.sh`). Don't hand-edit either.
- **Partial-class TFM split.** `PhoneNumberUtil.cs` is a `partial class` with framework-specific halves: `PhoneNumberUtil.net.cs` (modern .NET) and `PhoneNumberUtil.netstandard.cs` (netstandard2.0 fallbacks). When adding APIs that use newer BCL features, put the polyfill on the netstandard side.
- **Subsystems and their entry types** (each ports a Java counterpart of the same name):
- `PhoneNumberUtil` — parse / format / validate.
Expand All @@ -85,5 +85,7 @@ dotnet run -c Release --framework net10.0 -- --filter "*PhoneNumberWorkflowBench

## CI and release

- PRs trigger `build_and_run_unit_tests_linux.yml` (Ubuntu, .NET 9, net9.0 target only). AppVeyor (`appveyor.yml`) runs the full multi-TFM matrix on Windows and is the gate for NuGet publishes.
- Releases are tag-driven on AppVeyor; metadata-bump releases are created automatically by `create_new_release_on_new_metadata_update.yml`.
- CI is GitHub Actions only, on `ubuntu-24.04-arm`. There are no Windows runners.
- PRs trigger `build_and_run_unit_tests_linux.yml` (net10.0 only) and `run_all_tests_and_upload_code_coverage.yml` (whole solution, every TFM, uploads to Codecov).
- The test projects' `netframework4.8` target is conditioned on `'$(OS)' == 'Windows_NT'` — it only builds for developers on Windows, and no CI job covers it.
- Releases are tag-driven: a `vX.Y.Z` tag fires `publish_nuget.yml`, which packs both projects at the tag's version and pushes to nuget.org via trusted publishing (GitHub OIDC, `NuGet/login`) — there is no API key secret. Metadata-bump tags are created by `create_new_release_on_new_metadata_update.yml`.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[![Build status](https://ci.appveyor.com/api/projects/status/76abbk0qveot0mbo/branch/main?svg=true)](https://ci.appveyor.com/project/twcclegg/libphonenumber-csharp/branch/main)
[![Build status](https://github.com/twcclegg/libphonenumber-csharp/actions/workflows/run_all_tests_and_upload_code_coverage.yml/badge.svg?branch=main)](https://github.com/twcclegg/libphonenumber-csharp/actions/workflows/run_all_tests_and_upload_code_coverage.yml)
[![codecov](https://codecov.io/gh/twcclegg/libphonenumber-csharp/branch/main/graph/badge.svg)](https://codecov.io/gh/twcclegg/libphonenumber-csharp)
[![NuGet](https://img.shields.io/nuget/dt/libphonenumber-csharp.svg)](https://www.nuget.org/packages/libphonenumber-csharp/)
[![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/twcclegg/libphonenumber-csharp/badge)](https://scorecard.dev/viewer/?uri=github.com/twcclegg/libphonenumber-csharp)
Expand Down
42 changes: 0 additions & 42 deletions appveyor.yml

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netframework4.8;net8.0;net9.0;net10.0</TargetFrameworks>
<!-- netframework4.8 is Windows-only; see PhoneNumbers.Test.csproj. -->
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">netframework4.8;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">net8.0;net9.0;net10.0</TargetFrameworks>
<NoWarn>$(NoWarn);1591;CA1014;CA1062;CA1707;CA1812;CA1852</NoWarn>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
<Title>libphonenumber-csharp.extensions</Title>
<AssemblyName>PhoneNumbers.Extensions</AssemblyName>
<PackageId>libphonenumber-csharp.extensions</PackageId>
<VersionPrefix>$(APPVEYOR_BUILD_VERSION)</VersionPrefix>
<!-- Placeholder only. The real version comes from the release tag: publish_nuget.yml -->
<VersionPrefix>0.0.0</VersionPrefix>
<Authors>Thomas Clegg</Authors>
<TargetFrameworks>netstandard2.0;net8.0;net9.0;net10.0</TargetFrameworks>
<LangVersion>preview</LangVersion>
Expand All @@ -17,7 +18,6 @@
<RepositoryUrl>https://github.com/twcclegg/libphonenumber-csharp</RepositoryUrl>
<RepositoryType>git</RepositoryType>
<RepositoryBranch>main</RepositoryBranch>
<RepositoryCommit>$APPVEYOR_REPO_COMMIT</RepositoryCommit>
<DebugType>embedded</DebugType>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591;CA1014;CA1031;CA1062;CA1707</NoWarn>
Expand Down
7 changes: 5 additions & 2 deletions csharp/PhoneNumbers.Test/PhoneNumbers.Test.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netframework4.8;net8.0;net9.0;net10.0</TargetFrameworks>
<!-- netframework4.8 needs the .NET Framework test host, so it is Windows-only. No CI job covers it. -->
<TargetFrameworks Condition="'$(OS)' == 'Windows_NT'">netframework4.8;net8.0;net9.0;net10.0</TargetFrameworks>
<TargetFrameworks Condition="'$(OS)' != 'Windows_NT'">net8.0;net9.0;net10.0</TargetFrameworks>
<!--
Pin the C# language version so all TFMs (including netframework4.8, which the SDK
otherwise defaults to C# 7.3) accept post-7.3 syntax used by tests: `using var`
Expand Down Expand Up @@ -31,7 +33,8 @@

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<!-- In-box from net8.0 up; referencing it there trips NU1510 (prunable package). -->
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" Condition="'$(TargetFramework)' == 'netframework4.8'" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PrivateAssets>all</PrivateAssets>
Expand Down
1 change: 0 additions & 1 deletion csharp/PhoneNumbers.sln.DotSettings
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateConstants/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="AA_BB" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateInstanceFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/PredefinedNamingRules/=PrivateStaticFields/@EntryIndexedValue">&lt;Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /&gt;</s:String>
<s:String x:Key="/Default/Environment/Hierarchy/Build/SolBuilderDuo/CustomGlobalProperties/=APPVEYOR_005FBUILD_005FVERSION/@EntryIndexedValue">0.0.0</s:String>
<s:Boolean x:Key="/Default/UserDictionary/Words/=anexo/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=benchmarked/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/UserDictionary/Words/=CMDA/@EntryIndexedValue">True</s:Boolean>
Expand Down
Loading