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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ rustfmt.toml @DataDog/libdatadog-core
scripts/check_cargo_metadata.sh @DataDog/libdatadog-core
scripts/commits-since-release.sh @DataDog/libdatadog-core
scripts/crates-to-package.sh @DataDog/libdatadog-core
scripts/major-bumps-level.sh @DataDog/libdatadog-core
scripts/publication-order.sh @DataDog/libdatadog-core
scripts/reformat_copyright.sh @DataDog/libdatadog-core
scripts/semver-level.sh @DataDog/libdatadog-core
Expand Down
240 changes: 167 additions & 73 deletions .github/workflows/release-proposal-dispatch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,22 @@ on:
required: true
type: choice
options:
- libdd-alloc
- libdd-capabilities
- libdd-capabilities-impl
- libdd-common
- libdd-crashtracker
- libdd-data-pipeline
- libdd-ddsketch
- libdd-dogstatsd-client
- libdd-http-client
- libdd-library-config
- libdd-libunwind-sys
- libdd-log
- libdd-otel-thread-ctx
- libdd-profiling
- libdd-profiling-protobuf
- libdd-shared-runtime
- libdd-telemetry
- libdd-tinybytes
- libdd-trace-normalization
Expand Down Expand Up @@ -388,55 +394,151 @@ jobs:
echo "No previous release tag for $NAME, preparing initial release..."

# Use the version from the crate metadata
LEVEL=$(echo "$crate" | jq -r '.version')

VERSION=$(echo "$crate" | jq -r '.version')
LEVEL="major"
TAG=""
RANGE=""

# fail when the version is not an initial release
if [ "$LEVEL" != "1.0.0" ] && [ "$LEVEL" != "0.1.0" ]; then
echo "Error: $NAME is not a 1.0.0 or 0.1.0 release" >&2
if [ "$VERSION" != "0.1.0" ]; then
echo "Error: $NAME is not a 0.1.0 release" >&2
exit 1
fi

INITIAL_RELEASE=true

echo "Executing cargo release for $NAME with level $LEVEL..."
cargo release version -p "$NAME" -x $LEVEL --no-confirm
cargo release version -p "$NAME" --allow-branch "$BRANCH_NAME" -x $LEVEL --no-confirm
fi

# Commit the changes
cargo release commit --no-confirm -x

NEXT_VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r --arg name "$NAME" '.packages[] | select(.name == $name) | .version')
NEXT_TAG="$TAG_PREFIX$NEXT_VERSION"

# Add to results array
jq --arg name "$NAME" \
--arg level "$LEVEL" \
--arg tag "$NEXT_TAG" \
--arg prev_tag "$TAG" \
--arg version "$NEXT_VERSION" \
--arg range "$RANGE" \
--argjson commits "$COMMITS" \
--arg path "$CRATE_PATH" \
--arg initial_release "$INITIAL_RELEASE" \
'. += [{"name": $name, "level": $level, "tag": $tag, "prev_tag": $prev_tag, "version": $version, "range": $range, "commits": $commits, "path": $path, "initial_release": $initial_release}]' \
/tmp/api-changes.json > /tmp/api-changes.tmp && mv /tmp/api-changes.tmp /tmp/api-changes.json
done

# Check if there are commits to push
if git diff --quiet "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"; then
echo "No changes to push. Cancelling the workflow."
exit 1
fi

# Output the results
echo "API changes summary:"
jq . /tmp/api-changes.json

- name: Update version for crates with libdd-* direct dependency major bumps since last release
run: |
set -euo pipefail
BRANCH_NAME="${{ steps.proposal-branch.outputs.branch_name }}"

# Run the audit in a throwaway worktree so extra worktrees / cargo metadata do not touch the job checkout.
MAJOR_BUMPS_WT=$(mktemp -d "${RUNNER_TEMP:-/tmp}/major-bumps-wt.XXXXXX")
WF_SHA="${{ github.sha }}"

git worktree add --detach "$MAJOR_BUMPS_WT" "$WF_SHA"
set +e
( cd "$MAJOR_BUMPS_WT" && ./scripts/major-bumps-level.sh /tmp/api-changes.json ) \
> /tmp/api-changes-with-major-bumps-pre-commit.json
MB_RC=$?
git worktree remove --force "$MAJOR_BUMPS_WT" || true
set -e
if [[ "$MB_RC" -ne 0 ]]; then
echo "Major bumps level script failed with code $MB_RC"
echo "Major bumps level script output:"
cat /tmp/api-changes-with-major-bumps-pre-commit.json
exit "$MB_RC"
fi

# Full same crate list as api-changes.json; rows updated in place when a major bump is applied.
cp /tmp/api-changes-with-major-bumps-pre-commit.json /tmp/api-changes-with-major-bumps.json

# iterate over the major bumps and check the major bumps and update the version
jq -c '.[]' /tmp/api-changes-with-major-bumps-pre-commit.json | while read -r bump; do
NAME=$(echo "$bump" | jq -r '.name')
PREV_TAG=$(echo "$bump" | jq -r '.prev_tag')
TAG=$(echo "$bump" | jq -r '.tag')
VERSION=$(echo "$bump" | jq -r '.version')
MAJOR_BUMPS=$(echo "$bump" | jq -r '.major_bumps')

if [ "$MAJOR_BUMPS" != "[]" ]; then
echo "Updating version for $NAME with major bumps: $MAJOR_BUMPS"
cargo release version -p "$NAME" --prev-tag-name "$PREV_TAG" --allow-branch "$BRANCH_NAME" -x major --no-confirm

git commit -am "chore(release): update version for $NAME with major bumps"

NEXT_VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r --arg name "$NAME" '.packages[] | select(.name == $name) | .version')
NEXT_TAG="$NAME-v$NEXT_VERSION"

echo "Updating tag $TAG to $NEXT_TAG and version $VERSION to $NEXT_VERSION for $NAME"

jq --arg name "$NAME" \
--arg nl "major" \
--arg version "$NEXT_VERSION" \
--arg tag "$NEXT_TAG" \
'map(if .name == $name then . + {level: $nl, version: $version, tag: $tag} else . end)' \
/tmp/api-changes-with-major-bumps.json > /tmp/api-changes-with-major-bumps.tmp \
&& mv /tmp/api-changes-with-major-bumps.tmp /tmp/api-changes-with-major-bumps.json
fi
done

# Output the results
echo "API changes with major bumps summary:"
jq . /tmp/api-changes-with-major-bumps.json

- name: Generate CHANGELOGS
id: generate-changelogs
run: |
set -euo pipefail
ORIGINAL_HEAD=$(cat /tmp/release_head_sha)

echo "Generating CHANGELOGS"

jq -c '.[]' /tmp/api-changes-with-major-bumps.json | while read -r bump; do
COMMITS=$(echo "$bump" | jq -r '.commits')
RANGE=$(echo "$bump" | jq -r '.range')
NAME=$(echo "$bump" | jq -r '.name')
TAG=$(echo "$bump" | jq -r '.prev_tag')
NEXT_TAG=$(echo "$bump" | jq -r '.tag')
CRATE_PATH=$(echo "$bump" | jq -r '.path')
INITIAL_RELEASE=$(echo "$bump" | jq -r '.initial_release')

if [ "$INITIAL_RELEASE" = "true" ]; then
echo "Initial release for $NAME"

# Use the existing CHANGELOG.md if present, otherwise create a minimal one
if [ ! -f "$CRATE_PATH/CHANGELOG.md" ]; then
echo "Creating CHANGELOG.md for $NAME..."
RELEASE_DATE=$(date +%Y-%m-%d)
printf '# Changelog\n\n\n## %s - %s\n\nInitial release.\n' "$LEVEL" "$RELEASE_DATE" > "$CRATE_PATH/CHANGELOG.md"

git add "$CRATE_PATH/CHANGELOG.md"
git commit -m "chore(release): update CHANGELOG.md for $NAME"
else
echo "Using existing CHANGELOG.md for $NAME..."
fi
continue
fi

git add "$CRATE_PATH/CHANGELOG.md"
if git diff --cached --quiet; then
# Nothing changed (version already correct, CHANGELOG already present).
# An empty commit is required so the proposal branch is ahead of the
# ephemeral branch — GitHub rejects PRs between branches at the same commit.
echo "Nothing to commit for $NAME (version and CHANGELOG already up to date), creating empty release marker commit..."
git commit --allow-empty -m "chore: Release $NAME $LEVEL"
else
cargo release commit --no-confirm -x
fi

# Add to results array
jq --arg name "$NAME" \
--arg level "$LEVEL" \
--arg tag "$TAG_PREFIX$LEVEL" \
--arg version "$LEVEL" \
--arg initial_release "$INITIAL_RELEASE" \
'. += [{"name": $name, "level": $level, "tag": $tag, "version": $version, "initial_release": $initial_release}]' \
/tmp/api-changes.json > /tmp/api-changes.tmp && mv /tmp/api-changes.tmp /tmp/api-changes.json

# Skip the git-cliff changelog update below — CHANGELOG is already handled above
# FIXME: $COMMITS could be empty if there are no commits since last release
if [ "$COMMITS" = "[]" ]; then
echo "No commits since last release for $NAME, skipping CHANGELOG generation"
continue
fi

# Update the CHANGELOG.md
NEXT_VERSION=$(cargo metadata --format-version=1 --no-deps | jq -r --arg name "$NAME" '.packages[] | select(.name == $name) | .version')
NEXT_TAG="$TAG_PREFIX$NEXT_VERSION"

# Build a tight range from commits already found by commits-since-release.sh.
# This will save some time analising unnecessary commits and prevent unrelated commits
Expand Down Expand Up @@ -471,44 +573,29 @@ jobs:
"$CLIFF_CONTEXT_FILE" > "$CLIFF_FILTERED_FILE"
git cliff --from-context "$CLIFF_FILTERED_FILE" -u -v --prepend "$CRATE_PATH/CHANGELOG.md"
rm -f "$CLIFF_CONTEXT_FILE" "$CLIFF_HASHES_FILE" "$CLIFF_FILTERED_FILE"
git add "$CRATE_PATH/CHANGELOG.md"

# Commit the changes
cargo release commit --no-confirm -x

# Add to results array
jq --arg name "$NAME" \
--arg level "$LEVEL" \
--arg tag "$NEXT_TAG" \
--arg version "$NEXT_VERSION" \
--arg initial_release "$INITIAL_RELEASE" \
'. += [{"name": $name, "level": $level, "tag": $tag, "version": $version, "initial_release": $initial_release}]' \
/tmp/api-changes.json > /tmp/api-changes.tmp && mv /tmp/api-changes.tmp /tmp/api-changes.json
git add "$CRATE_PATH/CHANGELOG.md"
git commit -m "chore(release): update CHANGELOG.md for $NAME"
done

# Check if any crates are being released
if [ "$(jq 'length' /tmp/api-changes.json)" -eq 0 ]; then
echo "No crates to release. Cancelling the workflow."
# Check if there are commits to push
if git diff --quiet "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"; then
echo "No changes to push. Cancelling the workflow."
exit 1
fi

# Oldest → newest (chronological). Plain `git log` is newest-first; commit-headless should receive
# parentchild order so replays/signing match git history. Space-separated SHAs for the action.
# parentchild order so replays/signing match git history. Space-separated SHAs for the action.
COMMITS=$(git log --reverse "$ORIGINAL_HEAD".. --format='%H' | tr '\n' ' ' | xargs)
echo "commits=$COMMITS" >> $GITHUB_OUTPUT

# Output the results
echo "API changes summary:"
jq . /tmp/api-changes.json

- name: Push commits
if: steps.release-version-bumps.outputs.commits != ''
uses: DataDog/commit-headless@action/v2.0.3
with:
branch: ${{ steps.proposal-branch.outputs.branch_name }}
head-sha: ${{ steps.commits-since-release.outputs.release_head_sha }}
command: push
commits: "${{ steps.release-version-bumps.outputs.commits }}"
commits: "${{ steps.generate-changelogs.outputs.commits }}"

- name: Upload release data
uses: actions/upload-artifact@v4
Expand All @@ -517,6 +604,7 @@ jobs:
path: |
/tmp/commits-by-crate.json
/tmp/api-changes.json
/tmp/api-changes-with-major-bumps.json
retention-days: 1

- name: Cleanup on failure
Expand All @@ -536,7 +624,7 @@ jobs:
outputs:
branch_name: ${{ steps.proposal-branch.outputs.branch_name }}
ephemeral_branch: ${{ steps.ephemeral-branch.outputs.ephemeral_branch }}

create-pr:
needs: cargo-release
runs-on: ubuntu-latest
Expand Down Expand Up @@ -574,37 +662,42 @@ jobs:

NON_DEFAULT=""
if [ -n "$MAIN_START_REF" ]; then
NON_DEFAULT="${NON_DEFAULT}"$'\n### Cut from non-default ref\n\n'"This proposal was generated from \`$MAIN_START_REF\` instead of the default latest \`origin/$MAIN_BRANCH\`."$'\n'
NON_DEFAULT="${NON_DEFAULT}"$'\n### :exclamation: Cut from non-default ref\n\n'"This proposal was generated from \`$MAIN_START_REF\` instead of the default latest \`origin/$MAIN_BRANCH\`."$'\n'
fi
if [ "$BYPASS_STANDARD_CHECKS" = "true" ]; then
NON_DEFAULT="${NON_DEFAULT}"$'\n### Non-default workflow options\n\n'"**bypass_standard_checks** was enabled: the ongoing-proposal branch guard was skipped; branches use proposal prefix \`$PROPOSAL_BRANCH_PREFIX\` and release prefix \`$RELEASE_BRANCH_PREFIX\`. Crates whose resolved git tag is not the latest SemVer tag for that crate are still included (normally skipped)."$'\n'
NON_DEFAULT="${NON_DEFAULT}"$'\n### :test_tube: Non-default workflow options\n\n'"**bypass_standard_checks** was enabled: the ongoing-proposal branch guard was skipped; branches use proposal prefix \`$PROPOSAL_BRANCH_PREFIX\` and release prefix \`$RELEASE_BRANCH_PREFIX\`. Crates whose resolved git tag is not the latest SemVer tag for that crate are still included (normally skipped)."$'\n'
fi
if [ -n "$NON_DEFAULT" ]; then
NON_DEFAULT="${NON_DEFAULT}"$'\n\n'
fi

# Generate the PR body by merging commits and API changes
# PR body from api-changes-with-major-bumps.json (same crates as api-changes.json; tags/versions updated after libdd major bumps).
# Note: read returns 1 when it reaches EOF, which is expected for heredocs
read -r -d '' JQ_FILTER << 'EOF' || true
.[] | select((.commits | length) > 0) |
. as $crate |
($api[0] | map(select(.name == $crate.name)) | first // {
level: "skipped because there were no changes to the public API"
}) as $api_info |
[
"## \($crate.name)",
"",
(if $api_info.version then "**Next version:** `\($api_info.version)`" else null end),
"**Semver bump:** `\($api_info.level)`",
(if $api_info.tag then "**Tag:** `\($api_info.tag)`\n" else null end),
(if $api_info.initial_release == "true" then
"**Warning:** this is an initial release. Please verify that the version and commits included are correct.\n"
else null end),
(if ($crate.commits | length) > 0 then "### Commits\n\n" + ($crate.commits | map("- \(.subject)") | join("\n")) else null end)
] | map(select(. != null and . != "")) | join("\n")
[ $api[0][]
| [
"## \(.name)",
"",
(if .version then "**Next version:** `\(.version)`" else null end),
"**Semver bump:** `\(.level)`",
(if .tag then "**Tag:** `\(.tag)`\n" else null end),
(if (.major_bumps // [] | length) > 0 then
"### :warning: major bump forced due to:\n\n"
+ ((.major_bumps // []) | map("- `\(.dependency)`: \(.previous_req) → \(.current_req)") | join("\n"))
+ "\n"
else null end),
(if .initial_release == "true" then
"**Warning:** this is an initial release. Please verify that the version and commits included are correct.\n"
else null end),
(if (.commits | length) > 0 then "### Commits\n\n" + (.commits | map("- \(.subject)") | join("\n")) else null end)
]
| map(select(. != null and . != ""))
| join("\n")
]
| join("\n\n")
EOF

COMMITS_AND_API_BODY=$(jq -r --slurpfile api /tmp/api-changes.json "$JQ_FILTER" /tmp/commits-by-crate.json)
COMMITS_AND_API_BODY=$(jq -nr --slurpfile api /tmp/api-changes-with-major-bumps.json "$JQ_FILTER")

PR_BODY="# Release proposal for ${{ inputs.crate }} and its dependencies

Expand All @@ -623,6 +716,7 @@ jobs:
--label "release-proposal" \
--label "skip-metadata-check" \
--label "skip-changelog-check" \
--label "skip-pr-title-semver-check" \
--base "${{ needs.cargo-release.outputs.ephemeral_branch }}" \
--draft

Expand Down
29 changes: 24 additions & 5 deletions .github/workflows/release-proposal-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,21 +19,40 @@ jobs:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
with:
fetch-depth: 0
# Use PR head so git diff origin/main...HEAD matches commits on the proposal branch (not the merge commit).
# Checkout PR head (not the synthetic merge commit) so version comparison matches the proposal branch.
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: 1.92.0

- name: Fetch main for three-dot diff
run: git fetch origin main
# origin/main is needed for merge-base on push; PR base SHA must be fetched for git show.
- name: Fetch refs for version comparison
run: |
git fetch origin main
if [ "${{ github.event_name }}" = "pull_request" ]; then
git fetch --no-tags origin "${{ github.event.pull_request.base.sha }}"
fi

# Every workspace crate: compare effective [package] version at origin/main vs HEAD (see script).
# Compare [package] versions between PR base and head (or merge-base(main,HEAD) vs push on direct push).
# Dependency-only manifest edits are ignored; excludes match the old cargo package --exclude list.
- name: Package crates changed vs main
- name: Package crates with version bumps vs comparison base
id: package-crates
run: |
set -euo pipefail
if [ "${{ github.event_name }}" = "pull_request" ]; then
GIT_VERSION_BASE="${{ github.event.pull_request.base.sha }}"
export GIT_VERSION_BASE
GIT_VERSION_HEAD="${{ github.event.pull_request.head.sha }}"
export GIT_VERSION_HEAD
else
# Direct push: compare from fork point with main, not origin/main's current tip (main may have advanced).
GIT_VERSION_BASE="$(git merge-base origin/main HEAD)"
export GIT_VERSION_BASE
GIT_VERSION_HEAD="${GITHUB_SHA}"
export GIT_VERSION_HEAD
fi

echo "Comparing Cargo.toml package versions: GIT_VERSION_BASE=${GIT_VERSION_BASE} GIT_VERSION_HEAD=${GIT_VERSION_HEAD}"
mapfile -t PACKAGES < <(./scripts/crates-to-package.sh)
echo "Packages to run cargo package on (version bump only):"
printf '%s\n' "${PACKAGES[@]}"
Expand Down
Loading
Loading