diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ce29297..79fde47 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -109,6 +109,12 @@ jobs: - name: Validate packages run: dotnet test tests/DatadogNet.iOS.PackageTests --logger 'trx;LogFileName=package-tests.trx' + # After the merge, on the finished artifact - the per-pass intermediates legitimately lack + # the other band's frameworks, so validating them against the published baseline reports + # every band-to-band difference as a break. See the script header. + - name: Validate package API against the published baseline + run: ./build/ValidatePackageApi.sh + - name: Upload packages uses: actions/upload-artifact@v4 with: 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/.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" diff --git a/Directory.Build.props b/Directory.Build.props index 6e9cfa1..0032cf7 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -16,9 +16,16 @@ impossible to tell which Datadog release a given CrashReporter package belonged to. --> 3.14.0 - 3 + 4 $(DatadogNativeVersion).$(DatadogBindingRevision) + + 3.14.0.3 + s.bokatuk s.bokatuk Copyright © s.bokatuk diff --git a/README.md b/README.md index 16ac598..a710c09 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ OS-provided Swift runtime, ABI-stable from 12.2. ```xml - - + + ``` @@ -113,8 +113,8 @@ restore them: ```xml - - + + ``` @@ -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 @@ -498,7 +502,7 @@ dotnet test tests/DatadogNet.iOS.PackageTests Run the on-simulator smoke tests against the packed packages: ```bash -./.github/scripts/run-simulator-tests.sh 3.14.0.3 net9.0-ios18.0 +./.github/scripts/run-simulator-tests.sh 3.14.0.4 net9.0-ios18.0 ``` Build and run the sample: @@ -510,32 +514,38 @@ 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): - - ```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 - ``` - -2. Bump `DatadogNativeVersion` in [`Directory.Build.props`](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. +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 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. - It is hardcoded, so nothing else will notice when it goes stale. +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 @@ -545,7 +555,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). --- @@ -555,6 +565,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. @@ -590,7 +606,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/`. 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 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/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/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: # 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/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/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: diff --git a/docs/release-notes/3.14.0.4.md b/docs/release-notes/3.14.0.4.md new file mode 100644 index 0000000..bee4253 --- /dev/null +++ b/docs/release-notes/3.14.0.4.md @@ -0,0 +1,83 @@ +## What's changed + +Binding-only release. The native SDK is unchanged — still +[dd-sdk-ios 3.14.0](https://github.com/DataDog/dd-sdk-ios/releases/tag/3.14.0) — and so are the +package ids, namespaces and API. The fourth component advances for the changes below: round two of +the ergonomics review, aimed this time at the packaging metadata, the supply chain around the +native archive, and the release machinery. + +## Descriptions no longer point at an empty package + +Since 3.14.0.1 the RUM, Logs and Trace descriptions ended "The managed DD* API lives in +DatadogNet.Objc.iOS; this package ships the native feature" — carried over from 2.x, where the +DatadogObjc framework really did hold every DD* type, and wrong ever since 3.0 deleted it: each +package binds its own surface (RUM's `ApiDefinitions.cs` alone is 7,400 lines), and +DatadogNet.Objc.iOS is a dependency-only meta-package with no assembly at all. A reader on +nuget.org following that sentence installed an empty package. The descriptions now say where the +API actually is, the stale rationale comment in `src/Datadog.Binding.props` is rewritten to match +reality, and the package tests reject any description that makes the old claim again. + +## The packed README's links now work on nuget.org + +The same README.md ships in every package and is rendered on each package's nuget.org page — where +its eleven repo-relative links (the sample, `build/checksums.txt`, the regeneration guide, the +Flags shim, the licence and friends) resolved against nuget.org and rendered as dead text. Every +repository link is absolute now; in-page anchors stay as they are, since those work everywhere the +file renders. + +## Checksums: trust-on-first-use replaced by GitHub's own digests + +`build/checksums.txt` is the trust anchor for the ~200 MB native archive, but its pins were +recorded by downloading the archive and writing down whatever hash arrived — so a tampered first +download would have minted the poisoned hash as the truth. The new `build/UpdateChecksums.sh` +records the digest the GitHub releases API reports for the asset, computed by GitHub at upload +time, and requires a fresh download to hash to the same value before anything is written. +`FetchXcFrameworks.sh` additionally cross-checks the pin against the live digest before +downloading and hard-fails on a disagreement; when the API is unreachable — offline, +rate-limited — the pinned hash still guards the download, so no build grows a network dependency +it did not have. Run against 3.14.0, the published digest equals the pin recorded under the old +flow: a retroactive check the old flow could not offer. + +## Package validation against the published baseline + +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 + +- **A release tag must agree with `Directory.Build.props`.** The tag alone drives which native + line is bound, so a mistyped tag would have published packages whose version does not describe + their contents. The workflow now fails before building when the tag's native prefix or binding + revision disagrees with the props; releasing a different line on purpose remains possible + through a documented repository variable. +- **Upstream releases are watched.** A weekly workflow compares `DatadogNativeVersion` against + the latest dd-sdk-ios release and opens one issue — idempotently — when upstream is ahead, + linking the release and the upgrade ritual. +- **`build/BumpNativeVersion.sh` starts a native upgrade in one command**: props, digest-anchored + checksum pin, README badge/prose/snippets, the validation baseline, and a scaffolded + release-notes file, finishing with a self-check. The README's upgrade guide now leads with it — + and puts `DiffSwiftHeaders.sh` *before* `FetchXcFrameworks.sh`, since the previous step order + would have diffed the new release against itself. +- **`build/CheckReadmeVersions.sh` now also verifies the dd-sdk-ios badge and the "Built + against" prose** — the two hardcoded spots the upgrade guide used to shrug about ("nothing + else will notice when it goes stale"). Something notices. +- `GenerateBindings.sh`'s usage example no longer names DatadogObjc, a framework deleted + upstream in 3.0. + +## Upgrading from 3.14.0.3 + +```diff +- ++ +``` + +Nothing to change beyond the version: same native SDK, same API, same package set, and the native +xcframeworks are byte-for-byte the same build. All packages move together, as they depend on each +other at an exact version. The corrected descriptions and working README links appear on +nuget.org with this version. 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]