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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: auto-release

# Merging a release note is the release.
#
# A pull request that adds docs/release-notes/<version>.md (2.34.1.4.md)
# is stating that the merge it belongs to is a release: the note is hand-written, one per
# version, and nobody writes one by accident.
# So merging it tags the merge commit and starts the ordinary release run. Nothing else about the
# release path changes — the tag is a real tag at a real commit, release.yml resolves the version
# from it exactly as it does for a hand-pushed tag, and the guard job there still proves the
# commit is on the default branch before anything is published.
#
# Triggered on the push to main rather than on `pull_request: closed`, for two reasons: a push
# to the default branch carries a full-permission token whatever the pull request's origin was
# (a fork pull request's token is read-only and could not push the tag), and it sees the merge
# identically whether it arrived as a merge commit, a squash or a rebase.

on:
push:
branches: ['main']
paths:
- 'docs/release-notes/**'

concurrency:
group: auto-release-${{ github.ref_name }}
cancel-in-progress: false

permissions:
# contents: write pushes the tag. actions: write dispatches release.yml, and that dispatch is
# not a stylistic choice: a tag pushed with GITHUB_TOKEN deliberately does not trigger
# `on: push: tags`, so release.yml would sit there and never start. workflow_dispatch is
# documented as an exception which always creates a run, which is why release.yml carries a
# workflow_dispatch trigger alongside its tag trigger.
contents: write
actions: write

jobs:
release:
name: tag and release the notes added here
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
# The whole history, so the diff below can reach the previous commit and so
# `git rev-parse refs/tags/...` can see tags that already exist.
fetch-depth: 0

- name: Tag every release note this push added, and start its release
env:
GH_TOKEN: ${{ github.token }}
BEFORE: ${{ github.event.before }}
run: |
set -euo pipefail

# A brand-new branch reports an all-zero "before" and there is nothing to diff against.
case "${BEFORE}" in
0000000000000000000000000000000000000000|'')
echo "no previous commit to diff against; nothing to do"
exit 0
;;
esac

# --diff-filter=A: added, not modified. Editing an existing note is a correction to a
# release that already happened, and must not tag anything.
added="$(git diff --name-status --diff-filter=A "${BEFORE}" "${GITHUB_SHA}" \
-- 'docs/release-notes/*.md' | cut -f2)"

if [ -z "${added}" ]; then
echo "no release notes added in this push; nothing to do"
exit 0
fi

# An annotated tag needs a tagger, and a runner has no git identity configured — without
# this, `git tag -a` fails with "Committer identity unknown".
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'

count=0
while IFS= read -r file; do
[ -n "${file}" ] || continue
base="$(basename "${file}" .md)"

# README.md documents the folder in several of these repositories.
if [ "${base}" = 'README' ]; then
continue
fi

tag="v${base}"

# Four-part versions only. release.yml's own version job also accepts three parts,
# but every release tag this repository has ever carried is four-part, and the
# three-part notes that exist are series overviews rather than releases — tagging one
# of those would publish something nobody asked for. A genuine three-part release can
# still be tagged by hand; only this automatic path is strict.
if ! printf '%s' "${tag}" | grep -qE '^v[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$'; then
echo "::warning::${file} does not name a release this repository publishes (would be '${tag}'); skipping"
continue
fi

if git rev-parse -q --verify "refs/tags/${tag}" >/dev/null; then
echo "${tag} already exists; skipping"
continue
fi

echo "==> tagging ${GITHUB_SHA} as ${tag} for ${file}"
git tag -a "${tag}" "${GITHUB_SHA}" -m "Release ${tag}"
git push origin "${tag}"

# Dispatched at the tag's own ref, so github.ref_name inside release.yml is the tag
# and its version/track resolution, release-notes lookup and changelog range all
# behave exactly as they do for a hand-pushed tag.
gh workflow run release.yml --ref "${tag}"
echo "==> dispatched release.yml at ${tag}"

{
echo "- \`${tag}\` tagged from ${file} and released"
} >> "$GITHUB_STEP_SUMMARY"
count=$((count + 1))
done <<< "${added}"

if [ "${count}" -eq 0 ]; then
echo "nothing tagged" >> "$GITHUB_STEP_SUMMARY"
fi
112 changes: 111 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@ name: build
on:
workflow_call:
inputs:
verify:
description: >
Whether to run the verification jobs — package validation, the sample builds, the
Release link checks and the e2e suites. Pull requests leave it at true, and are the only
place any of this runs. Releases pass false: the tagged commit was already verified on
its pull request, so a tag packs and publishes and nothing more. The gate is the same
input, with the same name and the same meaning, in every repository.
required: false
default: true
type: boolean
version:
description: NuGet version to stamp on every package.
required: true
Expand Down Expand Up @@ -107,12 +117,14 @@ jobs:
run: ./build/BuildNugets.sh "${{ inputs.version }}" "${{ steps.native.outputs.version }}"

- name: Validate packages
if: ${{ inputs.verify }}
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
if: ${{ inputs.verify }}
run: ./build/ValidatePackageApi.sh

- name: Upload packages
Expand All @@ -126,7 +138,7 @@ jobs:
retention-days: 7

- name: Upload test results
if: always()
if: ${{ inputs.verify && (always()) }}
uses: actions/upload-artifact@v4
with:
name: package-test-results
Expand All @@ -136,6 +148,7 @@ jobs:

sample:
name: build sample app
if: ${{ inputs.verify }}
timeout-minutes: 30
needs: pack
runs-on: macos-15
Expand Down Expand Up @@ -172,8 +185,105 @@ jobs:
-p:RuntimeIdentifier=iossimulator-arm64 \
-p:DatadogPackageVersion="${{ inputs.version }}"

# The Debug sample above restores, resolves and links, but never runs the linker or the AOT
# compiler — and those are what a consumer actually ships. DatadogNet.Mac has carried a Release
# leg for exactly this reason, with the note that "the ILLink/AOT behaviour Release turns on has
# broken binding consumers that built fine in Debug"; this is the same check for iOS.
#
# Affordable here in a way it is not everywhere: the Datadog xcframeworks are small, unlike the
# WebRTC-sized payloads in AntMedia.Net and Red5Pro.Streaming.Net, whose build files record a
# measured 38-minute iOS Release build and are deliberately left in Debug.
link-release:
name: Release link check (device, ${{ matrix.target-framework }})
if: ${{ inputs.verify }}
timeout-minutes: 45
needs: pack
runs-on: macos-15
strategy:
# Both extremes, like the e2e matrix: net9 comes out of the same pack pass as net8, and
# net10 is the band new consumers start on. Run per band because a device link failure can
# be band-specific - and because the regression this job exists for (dd-sdk-ios device
# slices missing _OBJC_CLASS_$_ exports) is invisible to every simulator job.
fail-fast: false
matrix:
target-framework: [net9.0-ios18.0, net10.0-ios26.0]
include:
- target-framework: net9.0-ios18.0
sdk-band: net9
- target-framework: net10.0-ios26.0
sdk-band: net10
steps:
- uses: actions/checkout@v4

- name: Select Xcode
uses: ./.github/actions/select-xcode

- name: Set up .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
9.0.x
10.0.x

- name: Install MAUI workload
run: |
# The example is built for the band under test, so the workload must be installed for
# the SDK that owns it. global.json pins .NET 9; the net10 band needs .NET 10, which is
# steered the same way BuildNugets.sh does it - a scratch directory with its own
# global.json, since the SDK is resolved from the working directory.
if [ "${{ matrix.sdk-band }}" = "net10" ]; then
mkdir -p "${RUNNER_TEMP}/sdk10"
( cd "${RUNNER_TEMP}/sdk10" \
&& dotnet new globaljson --sdk-version "$(dotnet --list-sdks | grep '^10\.' | tail -1 | cut -d' ' -f1)" --force \
&& dotnet workload install maui-ios )
else
dotnet workload install maui-ios
fi

- name: Download packages
uses: actions/download-artifact@v4
with:
name: nuget-packages
path: artifacts

# Code signing off, because a public runner has neither a certificate nor a provisioning
# profile, and neither affects whether the thing links. That leaves the .app itself as the
# assertion: AOT and the native link both run before the bundle is assembled, so a failure
# in either never gets far enough to produce one.
- name: Link the sample for a real device
run: |
tfm="${{ matrix.target-framework }}"
build_dir="$(pwd)"
if [ "${{ matrix.sdk-band }}" = "net10" ]; then
build_dir="${RUNNER_TEMP}/sdk10"
fi
( cd "${build_dir}" && dotnet build "${GITHUB_WORKSPACE}/samples/DatadogNet.iOS.Example/DatadogNetExample.csproj" \
--configuration Release \
--framework "${tfm}" \
-p:DatadogSdkBand="${{ matrix.sdk-band }}" \
-p:RuntimeIdentifier=ios-arm64 \
-p:EnableCodeSigning=false \
-p:DatadogPackageVersion="${{ inputs.version }}" )

app="$(find "samples/DatadogNet.iOS.Example/bin/Release/${tfm}/ios-arm64" -maxdepth 1 -name '*.app' | head -1)"
if [ -z "${app}" ]; then
echo "::error::no .app under samples/DatadogNet.iOS.Example/bin/Release/${tfm}/ios-arm64 — the device build did not produce a bundle"
exit 1
fi

# Reported rather than asserted on a count: how many assemblies survive the linker is
# the linker's business and moves with every SDK update. Zero of them would mean the
# build produced a bundle without AOT compiling anything, which is worth failing on.
images="$(find "${app}" -name '*.aotdata.arm64' | wc -l | tr -d ' ')"
echo "==> ${app}: ${images} AOT images"
if [ "${images}" -eq 0 ]; then
echo "::error::no AOT images in ${app} — the device build did not AOT compile"
exit 1
fi

e2e:
name: simulator smoke test (${{ matrix.target-framework }})
if: ${{ inputs.verify }}
timeout-minutes: 45
needs: pack
runs-on: macos-15
Expand Down
41 changes: 40 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: release

on:
# Dispatched as well as pushed to: auto-release.yml creates the tag with GITHUB_TOKEN when a
# release note is merged, and a tag pushed with that token deliberately does not trigger
# `on: push: tags`. workflow_dispatch is documented as an exception that always creates a run.
# Dispatched at the tag's ref, so github.ref_name below is the tag either way.
workflow_dispatch:
push:
tags: ['v*']

Expand All @@ -12,6 +17,37 @@ permissions:
contents: read

jobs:
# The release path packs and publishes without re-running validate/sample/e2e, on the grounds
# that the tagged commit already went through them on its pull request. That reasoning only
# holds if the commit is genuinely on the default branch — a tag cut from an unmerged branch,
# or from a commit force-pushed away since, would ship having been verified by nothing. Two
# cheap ubuntu minutes to make the assumption explicit rather than implicit.
guard:
name: verify the tag is on the default branch
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Refuse a tag that never went through a pull request
env:
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
set -euo pipefail

# A tag push checks out the tag, and the default branch's ref is not necessarily among
# the refs fetched with it, so ask for it by name before testing ancestry.
git fetch --no-tags --quiet origin \
"+refs/heads/${DEFAULT_BRANCH}:refs/remotes/origin/${DEFAULT_BRANCH}"

if ! git merge-base --is-ancestor "${GITHUB_SHA}" "origin/${DEFAULT_BRANCH}"; then
echo "::error::${GITHUB_REF_NAME} points at ${GITHUB_SHA}, which is not an ancestor of ${DEFAULT_BRANCH}. Releases skip the test suites because the tagged commit was verified on its pull request; this commit was not. Merge it first, then re-tag."
exit 1
fi

echo "${GITHUB_REF_NAME} -> ${GITHUB_SHA} is on ${DEFAULT_BRANCH}" >> "$GITHUB_STEP_SUMMARY"

version:
name: resolve release version
runs-on: ubuntu-latest
Expand Down Expand Up @@ -78,11 +114,14 @@ jobs:

build:
name: build
needs: version
needs: [guard, version]
uses: ./.github/workflows/build.yml
with:
version: ${{ needs.version.outputs.version }}
native-version: ${{ needs.version.outputs.native-version }}
# Verification already happened on this commit's pull request, and the guard job above
# proved the tag points at that commit. A release packs and publishes, nothing more.
verify: false

publish:
name: publish to nuget.org and create release
Expand Down
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
impossible to tell which Datadog release a given CrashReporter package belonged to.
-->
<DatadogNativeVersion>3.14.0</DatadogNativeVersion>
<DatadogBindingRevision>4</DatadogBindingRevision>
<DatadogBindingRevision>5</DatadogBindingRevision>
<VersionPrefix>$(DatadogNativeVersion).$(DatadogBindingRevision)</VersionPrefix>

<!--
Expand Down
17 changes: 12 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,8 +103,8 @@ OS-provided Swift runtime, ABI-stable from 12.2.

```xml
<ItemGroup>
<PackageReference Include="DatadogNet.Core.iOS" Version="3.14.0.4" />
<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.4" />
<PackageReference Include="DatadogNet.Core.iOS" Version="3.14.0.5" />
<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.5" />
</ItemGroup>
```

Expand All @@ -113,8 +113,8 @@ restore them:

```xml
<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
<PackageReference Include="DatadogNet.Core.iOS" Version="3.14.0.4" />
<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.4" />
<PackageReference Include="DatadogNet.Core.iOS" Version="3.14.0.5" />
<PackageReference Include="DatadogNet.RUM.iOS" Version="3.14.0.5" />
</ItemGroup>
```

Expand Down Expand Up @@ -502,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.4 net9.0-ios18.0
./.github/scripts/run-simulator-tests.sh 3.14.0.5 net9.0-ios18.0
```

Build and run the sample:
Expand Down Expand Up @@ -592,6 +592,13 @@ alongside Crash Reporting, and dSYMs uploaded for symbolication.
**`This version of .NET for iOS requires Xcode 26.0`.** Only affects `net10.0-ios26.0`. See
[Building locally](#building-locally).

**`Undefined symbols for architecture arm64: "_OBJC_CLASS_$_DD…"` building for a real device.**
Fixed in 3.14.0.5 — update every DatadogNet package to that version or later. The prebuilt
dd-sdk-ios device slices ship without static Objective-C registration for 41 classes (their
deployment target is below iOS 13), which broke every device link while simulator builds worked;
the packages now repair it with generated linker aliases plus up-front class realization. See
`docs/release-notes/3.14.0.5.md` and `build/device-class-aliases/README.md`.

**`ArgumentNullException` passing `null` attributes to `logger.Info(message, attributes)`** — or
any other level. Faithful to upstream: the Objective-C projection declares the dictionary (and the
`NSError`) `_Nonnull`, and the Swift implementation takes a non-optional `[String: Any]`, so a
Expand Down
Loading
Loading