Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
31 changes: 31 additions & 0 deletions .github/workflows/build_and_run_unit_tests_linux.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,34 @@ jobs:
- name: Test solution targeting dotnet10.0 only
run: dotnet test --no-build --verbosity normal -p:TargetFrameworks=net10.0
working-directory: ./csharp

# Mirrors publish_nuget.yml so a broken pack fails here instead of during a release.
# The dependency assertion matters most: the extensions package must pin the exact
# same libphonenumber-csharp version, which only happens if VersionPrefix reaches
# the ProjectReference.
- name: Verify release packaging
run: |
version=9.9.9
dotnet pack csharp/PhoneNumbers -c Release --no-restore \
-p:VersionPrefix="${version}" -o packtest
dotnet pack csharp/PhoneNumbers.Extensions -c Release --no-restore \
-p:VersionPrefix="${version}" -o packtest
ls -l packtest

for id in libphonenumber-csharp libphonenumber-csharp.extensions
do
if [ ! -f "packtest/${id}.${version}.nupkg" ]
then
echo "error: expected packtest/${id}.${version}.nupkg" >&2
exit 1
fi
done

nuspec=$(unzip -p "packtest/libphonenumber-csharp.extensions.${version}.nupkg" '*.nuspec')
if ! grep -qE "id=\"libphonenumber-csharp\"[^>]*version=\"${version}\"" <<< "${nuspec}"
then
echo "error: extensions package does not depend on libphonenumber-csharp ${version}" >&2
printf '%s\n' "${nuspec}" >&2
exit 1
fi
echo "packaging ok: both packages at ${version}, extensions dependency pinned"
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
2 changes: 1 addition & 1 deletion csharp/PhoneNumbers.PerformanceTest/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
## Performance testing history

See [Github Actions](https://github.com/twcclegg/libphonenumber-csharp/actions/workflows/run_performance_tests_windows.yml) for a history of previous runs, in the logs, you can see the performance results for each method being tested
See [Github Actions](https://github.com/twcclegg/libphonenumber-csharp/actions/workflows/run_performance_tests.yml) for a history of previous runs, in the logs, you can see the performance results for each method being tested

## Running locally

Expand Down
Loading