From 065ffa53df6b18fc5d0b54054686e6c19a6884ba Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:41:56 +0300 Subject: [PATCH 01/10] Stop pointing readers at the empty meta-package for the DD* API The RUM, Logs and Trace descriptions ended with "The managed DD* API lives in DatadogNet.Objc.iOS; this package ships the native feature" - true of 2.x, where DatadogObjc held every DD* type, but wrong since 3.0: each package binds its own surface (RUM's ApiDefinitions.cs alone is 7400 lines) and DatadogNet.Objc.iOS is a dependency-only meta-package with no assembly. A reader on nuget.org following that sentence would install a package that contains nothing. Same story for the rationale comment in Datadog.Binding.props, which still described Logs, RUM and Trace as having an empty ApiDefinitions.cs on purpose - it is Flags, Profiling and OpenTelemetryApi that are empty, and for a different reason (no Objective-C projection upstream). The package tests now reject any description that claims the API "lives in DatadogNet.Objc.iOS", so the claim cannot come back with a paste. Co-Authored-By: Claude Fable 5 --- src/Datadog.Binding.props | 10 ++++++---- src/DatadogNet.Logs.iOS/DatadogNet.Logs.iOS.csproj | 2 +- src/DatadogNet.RUM.iOS/DatadogNet.RUM.iOS.csproj | 2 +- src/DatadogNet.Trace.iOS/DatadogNet.Trace.iOS.csproj | 2 +- .../DatadogNet.iOS.PackageTests/PackageLayoutTests.cs | 7 +++++++ 5 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/Datadog.Binding.props b/src/Datadog.Binding.props index 90a1216..1aaf755 100644 --- a/src/Datadog.Binding.props +++ b/src/Datadog.Binding.props @@ -149,10 +149,12 @@ diff --git a/src/DatadogNet.Logs.iOS/DatadogNet.Logs.iOS.csproj b/src/DatadogNet.Logs.iOS/DatadogNet.Logs.iOS.csproj index 9fba91d..6b85c6f 100644 --- a/src/DatadogNet.Logs.iOS/DatadogNet.Logs.iOS.csproj +++ b/src/DatadogNet.Logs.iOS/DatadogNet.Logs.iOS.csproj @@ -3,7 +3,7 @@ DatadogLogs Logs - .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogLogs framework: structured log collection with levels, attributes, tags and error reporting, correlated with RUM sessions. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DD* API lives in DatadogNet.Objc.iOS; this package ships the native feature. + .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogLogs framework: structured log collection with levels, attributes, tags and error reporting, correlated with RUM sessions. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DDLogs/DDLogger API ships in this package; DatadogNet.Objc.iOS remains as a dependency-only compatibility meta-package. datadog-logs;logging diff --git a/src/DatadogNet.RUM.iOS/DatadogNet.RUM.iOS.csproj b/src/DatadogNet.RUM.iOS/DatadogNet.RUM.iOS.csproj index ab180da..45f2eda 100644 --- a/src/DatadogNet.RUM.iOS/DatadogNet.RUM.iOS.csproj +++ b/src/DatadogNet.RUM.iOS/DatadogNet.RUM.iOS.csproj @@ -3,7 +3,7 @@ DatadogRUM RUM - .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogRUM framework: Real User Monitoring - views, actions, resources, errors, frustration signals and mobile vitals, with automatic UIKit instrumentation. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DD* API lives in DatadogNet.Objc.iOS; this package ships the native feature. + .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogRUM framework: Real User Monitoring - views, actions, resources, errors, frustration signals and mobile vitals, with automatic UIKit instrumentation. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DDRUM* API ships in this package; DatadogNet.Objc.iOS remains as a dependency-only compatibility meta-package. datadog-rum;rum;real-user-monitoring diff --git a/src/DatadogNet.Trace.iOS/DatadogNet.Trace.iOS.csproj b/src/DatadogNet.Trace.iOS/DatadogNet.Trace.iOS.csproj index e4f58fd..85eea83 100644 --- a/src/DatadogNet.Trace.iOS/DatadogNet.Trace.iOS.csproj +++ b/src/DatadogNet.Trace.iOS/DatadogNet.Trace.iOS.csproj @@ -3,7 +3,7 @@ DatadogTrace Trace - .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogTrace framework: distributed tracing (APM), spans, and trace-context propagation across Datadog, B3 and W3C tracecontext headers. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DD* API lives in DatadogNet.Objc.iOS; this package ships the native feature. + .NET for iOS / .NET MAUI bindings for the native Datadog iOS SDK's DatadogTrace framework: distributed tracing (APM), spans, and trace-context propagation across Datadog, B3 and W3C tracecontext headers. Built against dd-sdk-ios $(DatadogNativeVersion). The managed DDTrace/DDTracer API ships in this package; DatadogNet.Objc.iOS remains as a dependency-only compatibility meta-package. datadog-trace;apm;tracing;opentelemetry diff --git a/tests/DatadogNet.iOS.PackageTests/PackageLayoutTests.cs b/tests/DatadogNet.iOS.PackageTests/PackageLayoutTests.cs index 945536f..1282eb8 100644 --- a/tests/DatadogNet.iOS.PackageTests/PackageLayoutTests.cs +++ b/tests/DatadogNet.iOS.PackageTests/PackageLayoutTests.cs @@ -252,6 +252,13 @@ string Value(string element) => nuspec.Descendants() { Assert.Contains("meta", Value("description"), StringComparison.OrdinalIgnoreCase); } + + // Until 3.14.0.4 the RUM, Logs and Trace descriptions claimed the managed DD* API "lives + // in DatadogNet.Objc.iOS" - true of the 2.x packages, where DatadogObjc held every DD* + // type, but wrong since 3.0: each package binds its own surface, and DatadogNet.Objc.iOS + // is a dependency-only meta-package with no assembly at all. Sending a reader there for + // the API must not come back. + Assert.DoesNotContain("lives in DatadogNet.Objc.iOS", Value("description"), StringComparison.OrdinalIgnoreCase); } [Theory] From c8056659852f06835eb1915787f0eaaa150effad Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:42:35 +0300 Subject: [PATCH 02/10] README: absolute links, so they survive the trip to nuget.org The same README.md is packed into every .nupkg and rendered on each package's nuget.org page, where its eleven repo-relative links - the sample, checksums.txt, the regeneration guide, the Flags shim, the licence and friends - resolve against nuget.org and render as dead text. Every repository link is now absolute (blob/main for files, tree/main for directories); in-page anchors stay as they are, since those work everywhere the file renders. Co-Authored-By: Claude Fable 5 --- README.md | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 16ac598..d068dff 100644 --- a/README.md +++ b/README.md @@ -127,9 +127,11 @@ restore them: ## Usage The C# names are the Objective-C selectors projected into C#. Each feature is in its own namespace -now — one `using` per module. [`samples/DatadogNet.iOS.Example`](samples/DatadogNet.iOS.Example) is -a working MAUI app that does all of the below; -[`Datadog.cs`](samples/DatadogNet.iOS.Example/Datadog.cs) is the setup in one file. +now — one `using` per module. +[`samples/DatadogNet.iOS.Example`](https://github.com/sbokatuk/DatadogNet.iOS/tree/main/samples/DatadogNet.iOS.Example) +is a working MAUI app that does all of the below; +[`Datadog.cs`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/samples/DatadogNet.iOS.Example/Datadog.cs) +is the setup in one file. ### Initialize @@ -394,7 +396,8 @@ all, so there is nothing for a binding to bind: callable API. They exist so the SDK is mirrored and so a future projection needs no new package. There is a way around this — a hand-written Swift `@objc` wrapper, which we can compile ourselves — -and a working prototype for Flags lives in [`shims/DatadogFlagsObjc/`](shims/DatadogFlagsObjc/). +and a working prototype for Flags lives in +[`shims/DatadogFlagsObjc/`](https://github.com/sbokatuk/DatadogNet.iOS/tree/main/shims/DatadogFlagsObjc). Nothing ships yet. --- @@ -453,13 +456,14 @@ DataDog/dd-sdk-ios release ──► Datadog.xcframework.zip (one archive, el There is no Carthage step and nothing is compiled from source. Datadog publishes the built xcframeworks as a release asset, so the build downloads one archive, verifies it against a hash -pinned in [`build/checksums.txt`](build/checksums.txt), and strips it to the two iOS slices. +pinned in [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt), +and strips it to the two iOS slices. **Why the two-pass build.** Each .NET SDK's iOS workload supports the current target framework and the previous one — the .NET 9 band builds net8 + net9, the .NET 10 band builds net9 + net10. No single SDK builds all three, so `BuildNugets.sh` packs twice and -[`merge-packages.py`](build/merge-packages.py) grafts the net10 assets and dependency groups into -the net9 packages. +[`merge-packages.py`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/merge-packages.py) +grafts the net10 assets and dependency groups into the net9 packages. ### Layout @@ -510,13 +514,14 @@ dotnet build samples/DatadogNet.iOS.Example/DatadogNetExample.csproj -p:RuntimeI > Building anything that targets `net10.0-ios26.0` needs Xcode **26.0** specifically — .NET for iOS > refuses any other version. If your default Xcode is newer, prefix the command: > `DEVELOPER_DIR=/Applications/Xcode_26.0.1.app/Contents/Developer ...`. CI handles this with the -> [`select-xcode`](.github/actions/select-xcode/action.yml) action. +> [`select-xcode`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/.github/actions/select-xcode/action.yml) action. --- ## Upgrading the Datadog SDK -1. Record the new archive's hash in [`build/checksums.txt`](build/checksums.txt): +1. Record the new archive's hash in + [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt): ```bash v=3.15.0 @@ -524,12 +529,14 @@ dotnet build samples/DatadogNet.iOS.Example/DatadogNetExample.csproj -p:RuntimeI shasum -a 256 Datadog.xcframework.zip ``` -2. Bump `DatadogNativeVersion` in [`Directory.Build.props`](Directory.Build.props) and reset - `DatadogBindingRevision` to `1`. +2. Bump `DatadogNativeVersion` in + [`Directory.Build.props`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/Directory.Build.props) + and reset `DatadogBindingRevision` to `1`. 3. `./build/FetchXcFrameworks.sh` 4. `./build/DiffSwiftHeaders.sh ` — writes one `build/.Swift.h.diff` per framework whose generated header changed, which is the porting work list; - [`docs/regenerating-bindings.md`](docs/regenerating-bindings.md) explains how to read one. + [`docs/regenerating-bindings.md`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/docs/regenerating-bindings.md) + explains how to read one. (`./build/GenerateBindings.sh` is the automated alternative for when Objective Sharpie works again — it writes to `Binding/`, **not** over the committed sources, and its header lists the fixes the committed files carry that regenerating would otherwise undo.) @@ -545,7 +552,7 @@ dropping a package — update the `FRAMEWORKS` list, add or remove the binding p > fails on recent iOS SDK module maps. The committed sources were produced by parsing the shipped > `-Swift.h` headers directly; `GenerateBindings.sh` documents the problem and honours a `SHARPIE` > override for when it is fixed. Until then the header diff **is** the upgrade path — see -> [`docs/regenerating-bindings.md`](docs/regenerating-bindings.md). +> [`docs/regenerating-bindings.md`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/docs/regenerating-bindings.md). --- @@ -590,7 +597,8 @@ copies and restore again: `rm -rf ~/.nuget/packages/datadognet.*`. ## Licence -The binding code in this repository is [MIT](LICENSE). The native binaries the packages ship are +The binding code in this repository is +[MIT](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/LICENSE). The native binaries the packages ship are built and published by Datadog and are Apache-2.0 — which covers `dd-sdk-ios`, KSCrash and opentelemetry-swift alike. Every package declares `MIT AND Apache-2.0` and carries both texts under `licenses/`. From e6404f9b9e3558de27b0da487a00928b2f9f0545 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:46:02 +0300 Subject: [PATCH 03/10] Anchor the checksum pins to GitHub's asset digests, not first downloads build/checksums.txt guards ~116 MB of native code, but its pins were recorded trust-on-first-use: the instructions said to download the archive and write down whatever hash you got - so a tampered first download would have minted the poisoned hash as the truth. The releases API reports the digest GitHub computed for every asset at upload time. The new build/UpdateChecksums.sh records that digest as the pin, downloads the asset, and requires the bytes to hash to the same value before anything is written - two independent paths to one number - and warns loudly when a pin CHANGES for an unchanged version, which is the asset-replacement event the pins exist to catch. It also owns the checksums.txt header, so the instructions cannot drift from what it does. (Run against 3.14.0: the digest equals the pin already recorded, which is a nice retroactive check on the old flow.) FetchXcFrameworks.sh now cross-checks the pin against the live digest before downloading anything: a mismatch is a hard failure either way it happened, while an unreachable API - offline, rate-limited - is a one-line note and the pinned hash keeps guarding the download, so the build never grows a network dependency it did not have. Co-Authored-By: Claude Fable 5 --- README.md | 10 +-- build/DiffSwiftHeaders.sh | 10 +-- build/FetchXcFrameworks.sh | 37 ++++++++++ build/UpdateChecksums.sh | 144 +++++++++++++++++++++++++++++++++++++ build/checksums.txt | 21 +++--- 5 files changed, 202 insertions(+), 20 deletions(-) create mode 100755 build/UpdateChecksums.sh diff --git a/README.md b/README.md index d068dff..cf0ee9d 100644 --- a/README.md +++ b/README.md @@ -520,13 +520,13 @@ dotnet build samples/DatadogNet.iOS.Example/DatadogNetExample.csproj -p:RuntimeI ## Upgrading the Datadog SDK -1. Record the new archive's hash in - [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt): +1. Pin the new archive's hash in + [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt) — + recorded from the digest GitHub publishes for the release asset, and verified against a fresh + download of the same bytes: ```bash - v=3.15.0 - curl -fsSL -O "https://github.com/DataDog/dd-sdk-ios/releases/download/$v/Datadog.xcframework.zip" - shasum -a 256 Datadog.xcframework.zip + ./build/UpdateChecksums.sh 3.15.0 ``` 2. Bump `DatadogNativeVersion` in diff --git a/build/DiffSwiftHeaders.sh b/build/DiffSwiftHeaders.sh index d851500..9e24ca6 100755 --- a/build/DiffSwiftHeaders.sh +++ b/build/DiffSwiftHeaders.sh @@ -11,9 +11,9 @@ set -eu # ./DiffSwiftHeaders.sh 3.15.0 # download that release, diff against libs/ # ./DiffSwiftHeaders.sh DIR_OLD DIR_NEW # diff two directories of *.xcframework # -# The download mode also prints the archive's SHA-256, which is the value step 1 of the upgrade -# guide records in build/checksums.txt - so the hash is captured from the same bytes the diff -# was read from. +# The download mode also prints the archive's SHA-256 so it can be compared against the pin +# UpdateChecksums.sh records in build/checksums.txt from GitHub's own asset digest - a third +# reading of the same bytes, independent of both the API and the pinning download. INVOKE_DIR="$(pwd)" cd "$(dirname "$0")" @@ -47,11 +47,11 @@ elif [ "$#" -eq 1 ]; then trap 'rm -rf "$WORK"' EXIT zip="$WORK/Datadog.xcframework.zip" - echo "==> downloading dd-sdk-ios $version (no checksum pinned yet - that is what this run produces)" + echo "==> downloading dd-sdk-ios $version" curl -fSL -o "$zip" \ "https://github.com/DataDog/dd-sdk-ios/releases/download/$version/Datadog.xcframework.zip" - echo "==> SHA-256 for build/checksums.txt:" + echo "==> SHA-256 of this download (must match the UpdateChecksums.sh pin in build/checksums.txt):" shasum -a 256 "$zip" | sed "s|$WORK/||" NEW_DIR="$WORK/extracted" diff --git a/build/FetchXcFrameworks.sh b/build/FetchXcFrameworks.sh index 47658bc..43572f0 100755 --- a/build/FetchXcFrameworks.sh +++ b/build/FetchXcFrameworks.sh @@ -60,6 +60,43 @@ if [ -z "$expected" ]; then exit 1 fi +# Best-effort cross-check of the pin against the digest GitHub publishes for the asset, before +# anything is downloaded. UpdateChecksums.sh derives the pin from this same digest, so a +# disagreement means either the pin was recorded wrongly or the release asset was replaced in +# place - both must stop the build. The API being unreachable (offline, rate-limited) must not: +# the pinned hash still guards the actual download, so that case is a one-line note and the +# fetch continues. +published=$(curl -fsS --max-time 15 \ + "https://api.github.com/repos/DataDog/dd-sdk-ios/releases/tags/$DATADOG_VERSION" 2>/dev/null \ + | python3 -c ' +import json +import sys + +try: + release = json.load(sys.stdin) +except Exception: + raise SystemExit(0) +for asset in release.get("assets", []): + if asset.get("name") == "Datadog.xcframework.zip": + digest = asset.get("digest") or "" + if digest.startswith("sha256:"): + print(digest[len("sha256:"):].lower()) + break +' 2>/dev/null) || published="" +if [ -n "$published" ]; then + if [ "$published" != "$expected" ]; then + echo "error: the pinned SHA-256 for $DATADOG_VERSION disagrees with the digest GitHub publishes" >&2 + echo " pinned $expected ($CHECKSUMS)" >&2 + echo " published $published (releases API asset digest)" >&2 + echo " Either the pin was recorded wrongly - re-run ./build/UpdateChecksums.sh $DATADOG_VERSION -" >&2 + echo " or the release asset was replaced after it was pinned, which wants investigating upstream." >&2 + exit 1 + fi + echo "==> pin matches the digest GitHub publishes for $DATADOG_VERSION" +else + echo "note: could not read the asset digest from the GitHub API - continuing with the pinned hash" +fi + sha256_of() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | cut -d' ' -f1 diff --git a/build/UpdateChecksums.sh b/build/UpdateChecksums.sh new file mode 100755 index 0000000..7c6413e --- /dev/null +++ b/build/UpdateChecksums.sh @@ -0,0 +1,144 @@ +#!/bin/sh +# Records the SHA-256 pin for one dd-sdk-ios release in build/checksums.txt, anchored to the +# digest GitHub itself publishes for the release asset rather than to whatever a first download +# happened to contain. +# +# The releases API reports each asset's digest ("sha256:"), computed by GitHub at upload +# time. Pinning that value closes the trust-on-first-use hole the old "download it and record +# what you got" instructions had: a machine whose download is tampered with - or an asset +# replaced before anyone here pinned it - would have minted the poisoned hash as the new truth. +# The asset is still downloaded and hashed by this script, so the two sources must agree before +# anything is recorded; a download that does not match the published digest is exactly the event +# the pins exist to catch, and stops this script loudly. +# +# Run it after bumping DatadogNativeVersion (build/BumpNativeVersion.sh does), then diff the +# result - a pin that CHANGED for an unchanged version means the release asset was replaced in +# place, and wants investigating rather than committing. +# +# Usage: +# ./UpdateChecksums.sh # version from Directory.Build.props +# ./UpdateChecksums.sh 3.15.0 # pin a different release, e.g. ahead of a bump +# +# FetchXcFrameworks.sh verifies every download against the recorded pin, and cross-checks the +# pin against the live digest whenever the API is reachable. +set -eu + +root="$(cd "$(dirname "$0")/.." && pwd)" +props="$root/Directory.Build.props" +out="$root/build/checksums.txt" +work="$(mktemp -d)" +trap 'rm -rf "$work"' EXIT + +version="${1:-}" +if [ -z "$version" ]; then + version=$(sed -n 's:.*\(.*\).*:\1:p' "$props" | head -1) +fi +if [ -z "$version" ]; then + echo "error: could not read DatadogNativeVersion from $props" >&2 + exit 1 +fi + +# Interpolated into URLs and matched against pin lines, so reject anything exotic up front. +case "$version" in + *[!A-Za-z0-9._-]*) + echo "error: invalid version '$version'" >&2 + exit 1 + ;; +esac + +echo "==> asking the GitHub releases API for the $version asset digest" +digest=$(curl -fsSL "https://api.github.com/repos/DataDog/dd-sdk-ios/releases/tags/$version" | python3 -c ' +import json +import sys + +release = json.load(sys.stdin) +for asset in release.get("assets", []): + if asset.get("name") == "Datadog.xcframework.zip": + digest = asset.get("digest") or "" + if digest.startswith("sha256:"): + print(digest[len("sha256:"):].lower()) + break +') +if [ -z "$digest" ]; then + echo "error: the releases API served no sha256 digest for Datadog.xcframework.zip $version" >&2 + echo " (release missing, asset renamed, or GitHub stopped publishing digests)" >&2 + exit 1 +fi +case "$digest" in + *[!0-9a-f]*) + echo "error: '$digest' does not look like a SHA-256" >&2 + exit 1 + ;; +esac + +# Download the same asset and require it to hash to the published digest: two independent paths +# to one value - what GitHub says it stored, and what it actually serves today. +echo "==> downloading Datadog.xcframework.zip $version to verify the digest against real bytes" +archive="$work/Datadog.xcframework.zip" +curl -fSL -o "$archive" \ + "https://github.com/DataDog/dd-sdk-ios/releases/download/$version/Datadog.xcframework.zip" +if command -v shasum >/dev/null 2>&1; then + actual=$(shasum -a 256 "$archive" | cut -d' ' -f1) +else + actual=$(sha256sum "$archive" | cut -d' ' -f1) +fi +if [ "$actual" != "$digest" ]; then + echo "error: the downloaded archive does not match the digest GitHub publishes for it" >&2 + echo " published $digest" >&2 + echo " download $actual" >&2 + echo " Nothing was recorded. Either the download was corrupted or tampered with in transit," >&2 + echo " or the asset was replaced mid-request; retry, and investigate upstream if it persists." >&2 + exit 1 +fi + +previous=$(sed -n "s/^$version[[:space:]]\{1,\}\([0-9a-f]\{64\}\).*/\1/p" "$out" | head -1) +if [ -n "$previous" ] && [ "$previous" != "$digest" ]; then + # Same version, different hash: for an asset that is supposed to be immutable, this is the + # replacement event the pins exist to catch. Recording it is a deliberate act - the diff of + # checksums.txt is the audit trail - but it must never happen silently. + echo "WARNING: the pin for $version is changing" >&2 + echo " was $previous" >&2 + echo " now $digest" >&2 + echo " The release asset was replaced upstream. Diff the frameworks before committing this." >&2 +fi + +# The whole file is rewritten - canonical header, then the pins in their existing order with this +# version replaced in place or appended - so the instructions above the pins cannot drift from +# what this script actually does. +pins="$work/pins" +awk -v version="$version" -v digest="$digest" ' + /^#/ || /^[[:space:]]*$/ { next } + $1 == version { print version, digest; replaced = 1; next } + { print } + END { if (!replaced) print version, digest } +' "$out" > "$pins" + +{ + echo "# SHA-256 of each dd-sdk-ios release's Datadog.xcframework.zip." + echo "#" + echo "# Upstream publishes no checksum manifest of its own, but the GitHub releases API reports" + echo "# the digest it computed for every asset at upload time. build/UpdateChecksums.sh records" + echo "# that digest here - after downloading the asset and requiring the bytes to hash to the" + echo "# same value - so the pin is anchored to what GitHub stored, not to whatever one machine's" + echo "# first download happened to contain. FetchXcFrameworks.sh refuses an archive that does" + echo "# not match, and cross-checks the pin against the live digest when the API is reachable." + echo "#" + echo "# That makes this file the trust anchor for ~116 MB of native code that gets linked into" + echo "# consumers' apps: a substituted or truncated archive fails the fetch instead of being" + echo "# bound and published. It also makes the build reproducible, since a GitHub release asset" + echo "# can be replaced in place without the tag changing." + echo "#" + echo "# To add or re-pin a version:" + echo "#" + echo "# ./build/UpdateChecksums.sh 3.15.0" + echo "#" + echo "# Format: " + echo "" + cat "$pins" +} > "$out.tmp" && mv "$out.tmp" "$out" + +if [ "$previous" = "$digest" ]; then + echo "==> $version was already pinned at this digest; verified and unchanged" +else + echo "==> pinned $version $digest" +fi diff --git a/build/checksums.txt b/build/checksums.txt index c7bde9d..52f1d54 100644 --- a/build/checksums.txt +++ b/build/checksums.txt @@ -1,19 +1,20 @@ # SHA-256 of each dd-sdk-ios release's Datadog.xcframework.zip. # -# Upstream publishes no checksum manifest of its own - unlike, say, the ffmpeg_kit_flutter -# releases, which ship a checksums.json next to the archives. So the hash is pinned here, in the -# repository, and FetchXcFrameworks.sh refuses to use an archive that does not match. +# Upstream publishes no checksum manifest of its own, but the GitHub releases API reports +# the digest it computed for every asset at upload time. build/UpdateChecksums.sh records +# that digest here - after downloading the asset and requiring the bytes to hash to the +# same value - so the pin is anchored to what GitHub stored, not to whatever one machine's +# first download happened to contain. FetchXcFrameworks.sh refuses an archive that does +# not match, and cross-checks the pin against the live digest when the API is reachable. # # That makes this file the trust anchor for ~116 MB of native code that gets linked into -# consumers' apps: a substituted or truncated archive fails the fetch instead of being bound and -# published. It also makes the build reproducible, since a GitHub release asset can be replaced -# in place without the tag changing. +# consumers' apps: a substituted or truncated archive fails the fetch instead of being +# bound and published. It also makes the build reproducible, since a GitHub release asset +# can be replaced in place without the tag changing. # -# To add a version, download the asset and record its hash: +# To add or re-pin a version: # -# v=2.18.0 -# curl -fsSL -O "https://github.com/DataDog/dd-sdk-ios/releases/download/$v/Datadog.xcframework.zip" -# shasum -a 256 Datadog.xcframework.zip +# ./build/UpdateChecksums.sh 3.15.0 # # Format: From 8d5b391a0b21dc384aa5bcd2a3f77db7204cb55a Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:47:26 +0300 Subject: [PATCH 04/10] CheckReadmeVersions: cover the badge and the "Built against" prose The check guarded the install-snippet pins and the device-check example, but the dd-sdk-ios badge - which names the native version three times over: label, shields.io image path and release link - and the "Built against **dd-sdk-ios 3.14.0**" intro line were exactly the hardcoded spots the README's own upgrade guide warned "nothing else will notice when it goes stale". Now something does: each spot is matched by its own anchored pattern (so the historical "dd-sdk-ios 3.0 deleted that framework" prose stays exempt), required to exist, and verified against DatadogNativeVersion. The upgrade guide's warning is updated to say so. Also retire GenerateBindings.sh's usage example of binding DatadogObjc, a framework dd-sdk-ios 3.0 deleted - the one-framework example is now DatadogRUM, which is in the script's own FRAMEWORKS list. Co-Authored-By: Claude Fable 5 --- README.md | 6 ++++-- build/CheckReadmeVersions.sh | 38 +++++++++++++++++++++++++++++++----- build/GenerateBindings.sh | 4 ++-- 3 files changed, 39 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index cf0ee9d..eb4f54a 100644 --- a/README.md +++ b/README.md @@ -541,8 +541,10 @@ dotnet build samples/DatadogNet.iOS.Example/DatadogNetExample.csproj -p:RuntimeI again — it writes to `Binding/`, **not** over the committed sources, and its header lists the fixes the committed files carry that regenerating would otherwise undo.) 5. `./build/BuildNugets.sh` and run both test suites. -6. Update the `dd-sdk-ios` badge at the top of this file — both its label and its release link. - It is hardcoded, so nothing else will notice when it goes stale. +6. Update the `dd-sdk-ios` badge at the top of this file — both its label and its release link — + and the "Built against" line under the install snippet. `build/CheckReadmeVersions.sh` verifies + them against `DatadogNativeVersion` in CI, so leaving them stale fails the build rather than + shipping. If Datadog adds or removes a framework, `FetchXcFrameworks.sh` fails loudly rather than silently dropping a package — update the `FRAMEWORKS` list, add or remove the binding project, and update diff --git a/build/CheckReadmeVersions.sh b/build/CheckReadmeVersions.sh index 7d37d19..f0e15e6 100755 --- a/build/CheckReadmeVersions.sh +++ b/build/CheckReadmeVersions.sh @@ -4,10 +4,13 @@ # stale silently on every release - 3.14.0.1 sat in the snippets while 3.14.0.2 shipped. Running # this in CI makes the version bump before a release drag the README along with it. # -# What is checked: every pin, and the -# device-check example (run-simulator-tests.sh ...). Prose that explains the version -# *scheme* ("3.14.0.1 is dd-sdk-ios 3.14.0, binding revision 1") is deliberately not checked - -# it describes the format, not the current release. +# What is checked: every pin, the +# device-check example (run-simulator-tests.sh ...), the dd-sdk-ios badge - which +# names the native version three times over: its label, the shields.io image path and the +# release link - and the "Built against **dd-sdk-ios N**" intro prose. Prose that explains the +# version *scheme* ("3.14.0.1 is dd-sdk-ios 3.14.0, binding revision 1") is deliberately not +# checked - it describes the format, not the current release - and the anchored patterns below +# do not reach the historical "dd-sdk-ios 3.0 deleted that framework" style of sentence either. set -eu root="$(cd "$(dirname "$0")/.." && pwd)" @@ -18,7 +21,8 @@ prop() { sed -n "s/.*<$1>\(.*\)<\/$1>.*/\1/p" "$props" | head -1 } -version="$(prop DatadogNativeVersion).$(prop DatadogBindingRevision)" +native="$(prop DatadogNativeVersion)" +version="$native.$(prop DatadogBindingRevision)" bad=0 @@ -40,6 +44,30 @@ for token in $(grep -oE 'run-(simulator|emulator)-tests\.sh +[0-9][0-9.]*' "$rea bad=1 fi done + +# The badge and the prose, each matched by its own anchored pattern so the error names the spot +# that went stale - and each required to exist, so deleting one does not pass as up to date. +check_native() { + pattern=$1 + what=$2 + found=0 + for token in $(grep -oE "$pattern" "$readme" | grep -oE '[0-9][0-9.]*'); do + found=1 + if [ "$token" != "$native" ]; then + echo "README $what says dd-sdk-ios $token, but the bound native version is $native" >&2 + bad=1 + fi + done + if [ "$found" -eq 0 ]; then + echo "README has no $what to check (expected to match: $pattern)" >&2 + bad=1 + fi +} + +check_native '\[!\[dd-sdk-ios [0-9][0-9.]*\]' 'badge label' +check_native 'dd--sdk--ios-[0-9][0-9.]*' 'badge image path' +check_native 'dd-sdk-ios/releases/tag/[0-9][0-9.]*' 'badge release link' +check_native 'Built against \*\*dd-sdk-ios [0-9][0-9.]*\*\*' '"Built against" prose' IFS=$old_ifs if [ "$bad" -ne 0 ]; then diff --git a/build/GenerateBindings.sh b/build/GenerateBindings.sh index 78baf28..e8359ec 100755 --- a/build/GenerateBindings.sh +++ b/build/GenerateBindings.sh @@ -8,8 +8,8 @@ set -e # Run this after bumping DatadogNativeVersion and re-running FetchXcFrameworks.sh. # # Usage: -# ./GenerateBindings.sh # every framework -# ./GenerateBindings.sh DatadogObjc # just one +# ./GenerateBindings.sh # every framework +# ./GenerateBindings.sh DatadogRUM # just one # # Requires Objective Sharpie, which is not on nuget.org and has to be obtained separately: # From dc33e6f4ee4926f86a70bd1d489d396314d8830a Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:48:08 +0300 Subject: [PATCH 05/10] Release: refuse a tag that disagrees with Directory.Build.props The tag alone drives which dd-sdk-ios line a release binds, so a mistyped v3.15.0.1 pushed while the props - and the committed ApiDefinitions.cs - are still on 3.14.0 would have published packages whose version does not describe their contents. The version job now compares the tag's native prefix against DatadogNativeVersion and its fourth component (when present) against DatadogBindingRevision, and fails before anything builds, with the fix spelled out: correct the tag or bump the props. Releasing a different line on purpose remains possible - that is a documented feature - through the RELEASE_ALLOW_VERSION_MISMATCH repository variable, set for the one release and unset after. The README's Releasing section covers both halves. Co-Authored-By: Claude Fable 5 --- .github/workflows/release.yml | 30 ++++++++++++++++++++++++++++++ README.md | 6 ++++++ 2 files changed, 36 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index efb38e4..cdb94c5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -46,6 +46,36 @@ jobs: echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT" echo "Releasing ${version} (prerelease=${prerelease})" >> "$GITHUB_STEP_SUMMARY" + # The tag names the dd-sdk-ios line to bind; Directory.Build.props names the line this + # branch is developed and tested against. When they disagree, the release would publish + # packages binding a native SDK the committed ApiDefinitions.cs were never ported to - or + # simply a mistyped tag - so it stops here, before anything builds. Fix the tag (delete and + # re-push) or bump the props. Releasing a different line on purpose - v2.30.2.3 from a main + # that has moved on to 3.x - is still possible: set the repository variable + # RELEASE_ALLOW_VERSION_MISMATCH to 'true' for that release, and unset it afterwards. + - name: Check the tag against Directory.Build.props + if: vars.RELEASE_ALLOW_VERSION_MISMATCH != 'true' + run: | + version="${{ steps.resolve.outputs.version }}" + native="${{ steps.resolve.outputs.native-version }}" + props_native=$(sed -n 's:.*\(.*\).*:\1:p' Directory.Build.props | head -1) + props_revision=$(sed -n 's:.*\(.*\).*:\1:p' Directory.Build.props | head -1) + + if [ "${native}" != "${props_native}" ]; then + echo "::error::tag ${GITHUB_REF_NAME} binds dd-sdk-ios ${native}, but Directory.Build.props has DatadogNativeVersion ${props_native}. Fix the tag or bump the props; for a deliberate different-line release, set the RELEASE_ALLOW_VERSION_MISMATCH repository variable to 'true'." + exit 1 + fi + + # The fourth component is this repository's binding revision. A tag may omit it, and a + # prerelease carries a suffix; both are stripped before comparing. + revision=$(printf '%s' "${version%%-*}" | cut -s -d. -f4) + if [ -n "${revision}" ] && [ "${revision}" != "${props_revision}" ]; then + echo "::error::tag ${GITHUB_REF_NAME} is binding revision ${revision}, but Directory.Build.props has DatadogBindingRevision ${props_revision}. Fix the tag or bump the props; for a deliberate mismatch, set the RELEASE_ALLOW_VERSION_MISMATCH repository variable to 'true'." + exit 1 + fi + + echo "Tag agrees with Directory.Build.props (${props_native}, revision ${props_revision})" >> "$GITHUB_STEP_SUMMARY" + build: name: build needs: version diff --git a/README.md b/README.md index eb4f54a..4ad8ae3 100644 --- a/README.md +++ b/README.md @@ -564,6 +564,12 @@ Tag it. `v3.14.0.3` builds, tests, publishes every package to nuget.org via trus creates a GitHub release. The tag drives which native SDK is bound, so an older line can be released by tagging it. +The tag must agree with `Directory.Build.props`: pushing `v3.15.0.1` while `DatadogNativeVersion` +still says `3.14.0` — or a fourth component that is not `DatadogBindingRevision` — fails the +workflow before anything builds, since it would publish packages whose version does not describe +their contents. For a deliberate different-line release, set the `RELEASE_ALLOW_VERSION_MISMATCH` +repository variable to `true` for that release, and unset it afterwards. + Pull requests publish a `-beta..` prerelease of the whole set. Curated notes in `docs/release-notes/.md` replace the generated commit list when present. From 2d371cd9fd1538a5f0f54dac848aefa9e81b1478 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:50:01 +0300 Subject: [PATCH 06/10] Watch upstream: open an issue when dd-sdk-ios moves ahead Nothing announced a new dd-sdk-ios release to this repository - the gap between upstream shipping and anyone here noticing was however long it took someone to look. A weekly workflow (plus workflow_dispatch for checking on demand) now compares DatadogNativeVersion against the latest upstream release tag and opens an issue when upstream is newer, linking the release and the upgrade ritual - BumpNativeVersion.sh, UpdateChecksums.sh, DiffSwiftHeaders.sh. Idempotent by exact title match against the open issues, so a pending upgrade collects one issue however many Mondays it stays open, and sort -V ordering means a yanked or re-cut older "latest" never prompts a downgrade. Co-Authored-By: Claude Fable 5 --- .github/workflows/upstream-watch.yml | 66 ++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 .github/workflows/upstream-watch.yml diff --git a/.github/workflows/upstream-watch.yml b/.github/workflows/upstream-watch.yml new file mode 100644 index 0000000..31b7dd8 --- /dev/null +++ b/.github/workflows/upstream-watch.yml @@ -0,0 +1,66 @@ +name: upstream watch + +# Opens an issue when Datadog publishes a dd-sdk-ios release newer than the one these bindings +# are built against, so an upgrade starts as a tracked task rather than a discovery. Weekly is +# plenty for an SDK that releases every few weeks; workflow_dispatch is for checking on demand. + +on: + schedule: + - cron: '30 6 * * 1' + workflow_dispatch: + +permissions: + contents: read + issues: write + +jobs: + check: + name: compare against the latest dd-sdk-ios release + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Open an issue if upstream is ahead + env: + GH_TOKEN: ${{ github.token }} + run: | + current=$(sed -n 's:.*\(.*\).*:\1:p' Directory.Build.props | head -1) + if [ -z "${current}" ]; then + echo "::error::could not read DatadogNativeVersion from Directory.Build.props" + exit 1 + fi + + # Upstream tags releases with the bare version - no v prefix. + latest=$(gh api repos/DataDog/dd-sdk-ios/releases/latest --jq .tag_name) + echo "bound: ${current}, upstream latest: ${latest}" >> "$GITHUB_STEP_SUMMARY" + + if [ "${latest}" = "${current}" ]; then + echo "Up to date." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # sort -V puts the newer of the two last. This also covers upstream being *behind* - + # a yanked or re-cut latest release - which is not an upgrade prompt. + if [ "$(printf '%s\n%s\n' "${current}" "${latest}" | sort -V | tail -1)" != "${latest}" ]; then + echo "Upstream ${latest} is not newer than ${current}; nothing to do." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + # One open issue per upstream version, however many Mondays it stays open. Matched on + # the exact title rather than a search expression, which matches loosely. + title="dd-sdk-ios ${latest} is available" + if gh issue list --state open --limit 100 --json title --jq '.[].title' | grep -Fxq "${title}"; then + echo "An open issue for ${latest} already exists; nothing to do." >> "$GITHUB_STEP_SUMMARY" + exit 0 + fi + + cat > "${RUNNER_TEMP}/issue-body.md" <> "$GITHUB_STEP_SUMMARY" From 5403c484a55b78670e056ac5e6fc7bf1aa64a677 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:50:33 +0300 Subject: [PATCH 07/10] Validate packed packages against the last published release in CI An accidental API break - a regenerated binding quietly dropping a member is the likely shape of it - would today reach nuget.org and be found by a consumer's compiler. EnablePackageValidation with a PackageValidationBaselineVersion catches it at pack time instead, comparing every assembly against the version already published. The baseline lives once, in Directory.Build.props, and advances to the just-shipped version while preparing the next release. Validation is opt-in through RunPackageValidation=true - the baseline restores from nuget.org, and a local offline pack must not depend on that - and the CI pack job opts in via the environment. Only the net9 band validates: the net10-band pass alone packs an intermediate that legitimately lacks the net8/net9 assets the baseline ships (merge-packages.py reassembles the full set afterwards), so the baseline comparison there would fail by design rather than by defect. Co-Authored-By: Claude Fable 5 --- .github/workflows/build.yml | 6 ++++++ Directory.Build.props | 9 +++++++++ src/Datadog.Binding.props | 17 +++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce29297..b03de4d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -104,6 +104,12 @@ jobs: run: ./build/FetchXcFrameworks.sh "${{ steps.native.outputs.version }}" - name: Pack all packages + # RunPackageValidation reaches MSBuild as a property from the environment and switches on + # EnablePackageValidation against the baseline pinned in Directory.Build.props (see + # src/Datadog.Binding.props). CI has the network access the baseline restore needs, which + # is why local packs default to off. + env: + RunPackageValidation: 'true' run: ./build/BuildNugets.sh "${{ inputs.version }}" "${{ steps.native.outputs.version }}" - name: Validate packages diff --git a/Directory.Build.props b/Directory.Build.props index 6e9cfa1..eb867fa 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -19,6 +19,15 @@ 3 $(DatadogNativeVersion).$(DatadogBindingRevision) + + 3.14.0.3 + s.bokatuk s.bokatuk Copyright © s.bokatuk diff --git a/src/Datadog.Binding.props b/src/Datadog.Binding.props index 1aaf755..5ca6e03 100644 --- a/src/Datadog.Binding.props +++ b/src/Datadog.Binding.props @@ -101,6 +101,23 @@ $(PackageTags);datadog;observability;SDK;MAUI;iOS;dotnet-ios;Binding + + + true + $(DatadogPackageValidationBaselineVersion) + + From afdaefa80f502093618c47eb5fc3f267575f4166 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:53:47 +0300 Subject: [PATCH 08/10] BumpNativeVersion.sh: one command starts a native upgrade The mechanical half of a dd-sdk-ios upgrade was six hand-edits across four files, each individually forgettable: the props version and revision, the checksum pin, the README badge and prose and pins, the release-notes file, and now the package-validation baseline. The script does all of it - UpdateChecksums.sh for the digest-anchored pin, the baseline pointed at the version being left behind, the release notes scaffolded with TODOs in the established format - and finishes by running CheckReadmeVersions.sh on its own output. The README's upgrade ritual now leads with it, and puts DiffSwiftHeaders.sh BEFORE FetchXcFrameworks.sh: the diff reads libs/ as the old side, so fetching first - the previous step order - would have diffed the new release against itself and produced an empty porting list. Exercised end to end against a real release (3.13.0 in a scratch copy of the repo): props, pins, badge, prose, scaffold and self-check all came out right. Co-Authored-By: Claude Fable 5 --- README.md | 37 ++++++----- build/BumpNativeVersion.sh | 131 +++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+), 18 deletions(-) create mode 100755 build/BumpNativeVersion.sh diff --git a/README.md b/README.md index 4ad8ae3..271f54a 100644 --- a/README.md +++ b/README.md @@ -520,31 +520,32 @@ dotnet build samples/DatadogNet.iOS.Example/DatadogNetExample.csproj -p:RuntimeI ## Upgrading the Datadog SDK -1. Pin the new archive's hash in - [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt) — - recorded from the digest GitHub publishes for the release asset, and verified against a fresh - download of the same bytes: - - ```bash - ./build/UpdateChecksums.sh 3.15.0 - ``` - -2. Bump `DatadogNativeVersion` in +1. `./build/BumpNativeVersion.sh 3.15.0` does the mechanical part: bumps `DatadogNativeVersion` + in [`Directory.Build.props`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/Directory.Build.props) - and reset `DatadogBindingRevision` to `1`. -3. `./build/FetchXcFrameworks.sh` -4. `./build/DiffSwiftHeaders.sh ` — writes one `build/.Swift.h.diff` per - framework whose generated header changed, which is the porting work list; + and resets `DatadogBindingRevision` to `1`; pins the new archive's hash in + [`build/checksums.txt`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/build/checksums.txt) + from the digest GitHub publishes for the release asset, verified against a fresh download of + the same bytes (`build/UpdateChecksums.sh`); points `PackageValidationBaselineVersion` at the + version being left behind; rewrites this README's badge, prose and install snippets; and + scaffolds `docs/release-notes/3.15.0.1.md`. +2. `./build/DiffSwiftHeaders.sh 3.15.0` — before fetching, while `libs/` still holds the previous + release — writes one `build/.Swift.h.diff` per framework whose generated header + changed, which is the porting work list; [`docs/regenerating-bindings.md`](https://github.com/sbokatuk/DatadogNet.iOS/blob/main/docs/regenerating-bindings.md) explains how to read one. (`./build/GenerateBindings.sh` is the automated alternative for when Objective Sharpie works again — it writes to `Binding/`, **not** over the committed sources, and its header lists the fixes the committed files carry that regenerating would otherwise undo.) +3. `./build/FetchXcFrameworks.sh` — replaces `libs/` with the new release's frameworks. +4. Port the diffs into the committed `ApiDefinitions.cs` files. 5. `./build/BuildNugets.sh` and run both test suites. -6. Update the `dd-sdk-ios` badge at the top of this file — both its label and its release link — - and the "Built against" line under the install snippet. `build/CheckReadmeVersions.sh` verifies - them against `DatadogNativeVersion` in CI, so leaving them stale fails the build rather than - shipping. +6. Finish the scaffolded release notes — its TODOs mark what cannot be generated — then tag (see + [Releasing](#releasing)). + +`build/CheckReadmeVersions.sh` verifies the badge, the "Built against" prose and the install +snippets against `Directory.Build.props` — the bump script runs it last, and CI runs it on every +build, so a stale spot fails loudly rather than shipping. If Datadog adds or removes a framework, `FetchXcFrameworks.sh` fails loudly rather than silently dropping a package — update the `FRAMEWORKS` list, add or remove the binding project, and update diff --git a/build/BumpNativeVersion.sh b/build/BumpNativeVersion.sh new file mode 100755 index 0000000..bc9efac --- /dev/null +++ b/build/BumpNativeVersion.sh @@ -0,0 +1,131 @@ +#!/bin/sh +# Does the mechanical half of a dd-sdk-ios upgrade, so the human half can start at the part that +# needs judgement - porting the header diffs into the committed ApiDefinitions.cs files. +# +# What it does, in order: +# +# * Directory.Build.props: DatadogNativeVersion to the new release, DatadogBindingRevision back +# to 1, and DatadogPackageValidationBaselineVersion to the version being left behind - the +# last one published, which is what the next pack validates against. +# * build/UpdateChecksums.sh: pins the new archive's SHA-256 from the digest GitHub publishes, +# verified against a fresh download of the asset. +# * README.md: the dd-sdk-ios badge (label, image path, release link), the "Built against" +# prose, the install-snippet pins and the device-check example. +# * docs/release-notes/.md: scaffolded with TODOs in the established format, +# unless it already exists. +# * build/CheckReadmeVersions.sh: run at the end, so a rewrite this script missed is caught +# here rather than in CI. +# +# It then prints what remains manual: the header diff, the fetch, the porting, the builds and +# tests, the notes, the tag. See the README's "Upgrading the Datadog SDK" section. +# +# Usage: +# ./BumpNativeVersion.sh 3.15.0 +# +# If a run dies halfway (offline during the checksum pin, say), reset with +# `git checkout -- Directory.Build.props README.md build/checksums.txt` and run it again. +set -eu + +root="$(cd "$(dirname "$0")/.." && pwd)" +props="$root/Directory.Build.props" +readme="$root/README.md" +notes_dir="$root/docs/release-notes" + +new="${1:-}" +if [ -z "$new" ]; then + echo "usage: $0 e.g. $0 3.15.0" >&2 + exit 1 +fi +if ! printf '%s' "$new" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "error: '$new' does not look like a dd-sdk-ios version (expected e.g. 3.15.0)" >&2 + exit 1 +fi + +prop() { + sed -n "s/.*<$1>\(.*\)<\/$1>.*/\1/p" "$props" | head -1 +} + +old_native="$(prop DatadogNativeVersion)" +old_revision="$(prop DatadogBindingRevision)" +if [ -z "$old_native" ] || [ -z "$old_revision" ]; then + echo "error: could not read DatadogNativeVersion/DatadogBindingRevision from $props" >&2 + exit 1 +fi +old_package="$old_native.$old_revision" +new_package="$new.1" + +if [ "$new" = "$old_native" ]; then + echo "error: already bound to dd-sdk-ios $old_native - nothing to bump" >&2 + exit 1 +fi + +# The version being left behind becomes the package-validation baseline, which only works if it +# was actually published. The tag is the best offline evidence; its absence is a note rather +# than a failure, since a shallow clone has no tags at all. +if ! git -C "$root" tag -l "v$old_package" 2>/dev/null | grep -q .; then + echo "note: no v$old_package tag here - if that version was never published, point" + echo " DatadogPackageValidationBaselineVersion at the newest version that was." +fi + +echo "==> $props: $old_native -> $new, revision $old_revision -> 1, baseline -> $old_package" +sed \ + -e "s|$old_native|$new|" \ + -e "s|$old_revision|1|" \ + -e "s|.*|$old_package|" \ + "$props" > "$props.tmp" && mv "$props.tmp" "$props" + +echo "==> pinning the $new archive checksum" +"$root/build/UpdateChecksums.sh" "$new" + +echo "==> rewriting the README badge, prose and version pins" +sed \ + -e "s|\[!\[dd-sdk-ios $old_native\]|[![dd-sdk-ios $new]|" \ + -e "s|dd--sdk--ios-$old_native-|dd--sdk--ios-$new-|" \ + -e "s|dd-sdk-ios/releases/tag/$old_native|dd-sdk-ios/releases/tag/$new|" \ + -e "s|Built against \*\*dd-sdk-ios $old_native\*\*|Built against **dd-sdk-ios $new**|" \ + -e "s|Version=\"$old_package\"|Version=\"$new_package\"|g" \ + -e "s|run-simulator-tests\.sh $old_package|run-simulator-tests.sh $new_package|" \ + "$readme" > "$readme.tmp" && mv "$readme.tmp" "$readme" + +notes="$notes_dir/$new_package.md" +if [ -f "$notes" ]; then + echo "==> $notes already exists; leaving it alone" +else + echo "==> scaffolding $notes" + cat > "$notes" < **Package versions are \`.\`.** \`$new_package\` is dd-sdk-ios +> \`$new\`, binding revision \`1\`. The fourth component belongs to this repository and advances when +> the bindings or packaging change while the native binaries stay put. + +TODO: summarise what the upgrade means for a consumer, in the format of the previous files in +this directory - consumer-visible changes first, each with why it matters, then documentation and +tests. The raw material is upstream's release notes and the \`build/*.Swift.h.diff\` files that +\`./build/DiffSwiftHeaders.sh $new\` writes. + +## Upgrading from $old_package + +TODO: state exactly what a consumer must change - or that it is a version bump alone. +NOTES +fi + +echo "==> checking the README rewrite" +"$root/build/CheckReadmeVersions.sh" + +cat < done. What remains is the part that needs judgement: + + 1. ./build/DiffSwiftHeaders.sh $new - run BEFORE fetching, while libs/ still holds + $old_native: writes one build/.Swift.h.diff per framework whose generated + header changed, which is the porting work list (docs/regenerating-bindings.md). + 2. ./build/FetchXcFrameworks.sh - replaces libs/ with the $new frameworks. + 3. Port the diffs into the committed ApiDefinitions.cs files. + 4. ./build/BuildNugets.sh, then both test suites (PackageTests, simulator). + 5. Finish docs/release-notes/$new_package.md - the TODOs mark what cannot be generated. + 6. Commit, PR, and tag v$new_package once merged (see the README's Releasing section). +REMAINING From 9766a711d04639b5873c1689f3a7fba1d0178b02 Mon Sep 17 00:00:00 2001 From: Siarhei Bakatsiuk Date: Sun, 26 Jul 2026 13:55:01 +0300 Subject: [PATCH 09/10] Prepare 3.14.0.4 Binding revision to 4 - the native SDK stays at 3.14.0 - with curated release notes covering the round: corrected package descriptions, README links that survive nuget.org, digest-anchored checksum pins, package validation against the published baseline, and the release machinery (tag guard, upstream watch, bump script, wider README checks). PackageValidationBaselineVersion stays at 3.14.0.3, the last version actually on nuget.org, and advances only once this one ships. README pins move to 3.14.0.4; CheckReadmeVersions.sh agrees. Co-Authored-By: Claude Fable 5 --- Directory.Build.props | 2 +- README.md | 10 ++--- docs/release-notes/3.14.0.4.md | 80 ++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 6 deletions(-) create mode 100644 docs/release-notes/3.14.0.4.md diff --git a/Directory.Build.props b/Directory.Build.props index eb867fa..6eb302e 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,7 +16,7 @@ impossible to tell which Datadog release a given CrashReporter package belonged to. --> 3.14.0 - 3 + 4 $(DatadogNativeVersion).$(DatadogBindingRevision) 3.14.0.3 diff --git a/build/ValidatePackageApi.sh b/build/ValidatePackageApi.sh new file mode 100755 index 0000000..8cc78fe --- /dev/null +++ b/build/ValidatePackageApi.sh @@ -0,0 +1,77 @@ +#!/bin/sh +# Compares every packed .nupkg in artifacts/ against the last published release, so an accidental +# API break - a regenerated binding quietly dropping a member is the likely shape of it - fails +# here instead of reaching nuget.org and a consumer's compiler. +# +# This deliberately runs AFTER merge-packages.py, on the finished artifact, not at pack time via +# EnablePackageValidation. The two-pass band arrangement makes the MSBuild-time gate structurally +# wrong: each pass packs an intermediate that lacks the other band's target frameworks, so +# ApiCompat maps the baseline's net10 assemblies onto the current net9 ones and reports every +# band-to-band difference as a break. Only the merged package has the baseline's framework set. +# +# Requires network twice over: the baseline packages download from nuget.org, and the apicompat +# tool installs on first use. CI runs it in the pack job; locally it is an explicit invocation, +# never part of an offline build. +# +# Usage: +# ./build/ValidatePackageApi.sh # baseline from Directory.Build.props +# ./build/ValidatePackageApi.sh 3.14.0.3 # explicit baseline version +set -eu + +root="$(cd "$(dirname "$0")/.." && pwd)" +artifacts="$root/artifacts" + +# Pinned like every other tool this repository pulls in. Advance deliberately. +apicompat_version="9.0.316" + +baseline="${1:-$(sed -n 's/.*\(.*\)<\/DatadogPackageValidationBaselineVersion>.*/\1/p' "$root/Directory.Build.props" | head -1)}" +if [ -z "$baseline" ]; then + echo "error: no baseline version - set DatadogPackageValidationBaselineVersion in Directory.Build.props or pass one" >&2 + exit 1 +fi + +packages=$(ls "$artifacts"/*.nupkg 2>/dev/null || true) +if [ -z "$packages" ]; then + echo "error: no packages in $artifacts - run ./build/BuildNugets.sh first" >&2 + exit 1 +fi + +tools="$root/artifacts/.apicompat" +if [ ! -x "$tools/apicompat" ]; then + echo "==> installing Microsoft.DotNet.ApiCompat.Tool $apicompat_version" + dotnet tool install Microsoft.DotNet.ApiCompat.Tool --version "$apicompat_version" --tool-path "$tools" >/dev/null +fi + +baselines="$root/artifacts/.baseline-$baseline" +mkdir -p "$baselines" + +failed=0 +for package in $packages; do + name=$(basename "$package") + id=$(printf '%s' "$name" | sed -E 's/\.[0-9]+\.[0-9]+\.[0-9]+(\.[0-9]+)?(-[0-9A-Za-z.-]+)?\.nupkg$//') + lower=$(printf '%s' "$id" | tr '[:upper:]' '[:lower:]') + + base="$baselines/$lower.$baseline.nupkg" + if [ ! -f "$base" ]; then + url="https://api.nuget.org/v3-flatcontainer/$lower/$baseline/$lower.$baseline.nupkg" + if ! curl -fsSL --max-time 120 -o "$base" "$url"; then + rm -f "$base" + # A package that has never shipped has no baseline to hold it to - the first release IS the + # baseline. Anything else failing to download is a real problem. + echo "==> $id: no $baseline on nuget.org - new package, nothing to validate against" + continue + fi + fi + + echo "==> $id: validating against $baseline" + if ! "$tools/apicompat" package "$package" --baseline-package "$base"; then + failed=1 + fi +done + +if [ "$failed" -ne 0 ]; then + echo "ValidatePackageApi: API breaking changes against $baseline - see above" >&2 + exit 1 +fi + +echo "ValidatePackageApi: every package is API-compatible with $baseline" diff --git a/docs/release-notes/3.14.0.4.md b/docs/release-notes/3.14.0.4.md index cc6bc50..bee4253 100644 --- a/docs/release-notes/3.14.0.4.md +++ b/docs/release-notes/3.14.0.4.md @@ -40,11 +40,14 @@ flow: a retroactive check the old flow could not offer. ## Package validation against the published baseline -CI now packs with `EnablePackageValidation` and a `PackageValidationBaselineVersion` of the last -published release — this one's baseline is `3.14.0.3` — so an accidental API break, most -plausibly a regenerated binding quietly dropping a member, fails the pack instead of surfacing in -a consumer's compiler. Opt-in through `RunPackageValidation=true`, because the baseline restores -from nuget.org and a local offline pack must not depend on that. +CI now holds every packed package's API to the last published release — this one's baseline is +`3.14.0.3` — so an accidental API break, most plausibly a regenerated binding quietly dropping a +member, fails in CI instead of surfacing in a consumer's compiler. It runs as +`build/ValidatePackageApi.sh` on the merged artifacts rather than at pack time: each band's pack +pass produces an intermediate that legitimately lacks the other band's frameworks, and only the +finished package carries the framework set the baseline has. The baseline downloads from +nuget.org, so the check is CI's and an explicit local invocation's, never part of an offline +pack. ## Release machinery diff --git a/src/Datadog.Binding.props b/src/Datadog.Binding.props index 5ca6e03..1aaf755 100644 --- a/src/Datadog.Binding.props +++ b/src/Datadog.Binding.props @@ -101,23 +101,6 @@ $(PackageTags);datadog;observability;SDK;MAUI;iOS;dotnet-ios;Binding - - - true - $(DatadogPackageValidationBaselineVersion) - -