2323 - libdd-trace-protobuf
2424 - libdd-trace-stats
2525 - libdd-trace-utils
26+ main_start_ref :
27+ description : >
28+ Optional git ref to cut the release from: commit SHA (short or full), branch name,
29+ tag, or refs/... (e.g. main, v1.2.3, origin/main). Leave empty to use latest
30+ origin/main.
31+ required : false
32+ type : string
33+ default : ' '
34+ bypass_standard_checks :
35+ description : >
36+ Skip ongoing-proposal checks. Proposal branches use prefix
37+ release-proposal-testing so they do not collide with normal release-proposal/* runs.
38+ required : false
39+ type : boolean
40+ default : false
2641
2742concurrency :
2843 group : release-proposal-dispatch-group
2944 cancel-in-progress : false
3045
3146env :
3247 MAIN_BRANCH : main
33- PROPOSAL_BRANCH_PREFIX : release-proposal
34- GIT_USER_NAME : " dd-octo-sts[bot]"
35- GIT_USER_EMAIL : " 200755185+dd-octo-sts[bot]@users.noreply.github.com"
48+ RELEASE_BRANCH_PREFIX : ${{ inputs.bypass_standard_checks && 'release-testing' || 'release' }}
49+ PROPOSAL_BRANCH_PREFIX : ${{ inputs.bypass_standard_checks && 'release-proposal-testing' || 'release-proposal' }}
3650
3751jobs :
3852 check-proposal-ongoing :
@@ -44,15 +58,19 @@ jobs:
4458 fetch-tags : true
4559 - name : Check if a release proposal is ongoing
4660 run : |
47- # Check if there are any proposal branches or ephemeral release branches (release/*/*)
48- EXISTING_BRANCHES=$(git branch -r --list "origin/${{ env.PROPOSAL_BRANCH_PREFIX }}/*" "origin/release/*/*")
49- if [ -n "$EXISTING_BRANCHES" ]; then
50- echo "Error: A release proposal is ongoing. Please cancel it or wait for it to be merged." >&2
51- echo "Existing branches:"
52- echo "$EXISTING_BRANCHES"
53- exit 1
61+ if [ "${{ inputs.bypass_standard_checks }}" = "true" ]; then
62+ echo "Skipping ongoing release proposal checks."
63+ else
64+ # Check if there are any proposal branches or ephemeral release branches (release/*/*)
65+ EXISTING_BRANCHES=$(git branch -r --list "origin/${{ env.PROPOSAL_BRANCH_PREFIX }}/*" "origin/${{ env.RELEASE_BRANCH_PREFIX }}/*/*")
66+ if [ -n "$EXISTING_BRANCHES" ]; then
67+ echo "Error: A release proposal is ongoing. Please cancel it or wait for it to be merged." >&2
68+ echo "Existing branches:"
69+ echo "$EXISTING_BRANCHES"
70+ exit 1
71+ fi
72+ echo "No release proposal is ongoing."
5473 fi
55- echo "No release proposal is ongoing."
5674
5775 check-membership :
5876 permissions :
82100 echo "User is not part of apm-common-components-core"
83101 exit 1
84102 fi
85-
103+
86104 cargo-release :
87105 permissions :
88106 id-token : write # Enable OIDC
@@ -120,19 +138,100 @@ jobs:
120138 with :
121139122140
141+ - uses : DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
142+ id : octo-sts
143+ with :
144+ scope : DataDog/libdatadog
145+ policy : self.write.pr
146+
123147 - name : Configure Git for signing
148+ env :
149+ GH_TOKEN : ${{ steps.octo-sts.outputs.token }}
150+ GITHUB_ACTOR : ${{ github.actor }}
124151 run : |
125- git config --global user.name "${{ env.GIT_USER_NAME }}"
126- git config --global user.email "${{ env.GIT_USER_EMAIL }}"
152+ # GET /user is not allowed with installation tokens; use GET /users/ACTOR (who triggered the workflow).
153+ GITHUB_USER_RESPONSE=$(curl -s -H "Authorization: token ${GH_TOKEN}" "https://api.github.com/users/${GITHUB_ACTOR}")
154+ GIT_USER_NAME=$(echo "${GITHUB_USER_RESPONSE}" | jq -r '.login // empty')
155+ GIT_USER_ID=$(echo "${GITHUB_USER_RESPONSE}" | jq -r '.id // empty')
156+ if [[ -z "$GIT_USER_NAME" ]]; then
157+ GIT_USER_NAME="${GITHUB_ACTOR}"
158+ GIT_USER_EMAIL="${GITHUB_ACTOR}@users.noreply.github.com"
159+ else
160+ GIT_USER_EMAIL="${GIT_USER_ID}+${GIT_USER_NAME}@users.noreply.github.com"
161+ fi
162+ echo "GIT_USER_NAME: $GIT_USER_NAME"
163+ echo "GIT_USER_EMAIL: $GIT_USER_EMAIL"
164+ git config --global user.name "$GIT_USER_NAME"
165+ git config --global user.email "$GIT_USER_EMAIL"
127166
167+ - name : Optionally checkout at a specific git ref
168+ env :
169+ MAIN_START_REF : ${{ inputs.main_start_ref }}
170+ run : |
171+ set -euo pipefail
172+ git fetch origin "${{ env.MAIN_BRANCH }}" --tags --prune
173+
174+ resolve_to_commit() {
175+ local r="$1"
176+ local c=""
177+ # Already a commit or resolvable locally
178+ c=$(git rev-parse -q --verify "${r}^{commit}" 2>/dev/null) || true
179+ if [ -n "$c" ]; then
180+ echo "$c"
181+ return 0
182+ fi
183+ # Remote branch: origin/<name>
184+ c=$(git rev-parse -q --verify "origin/${r}^{commit}" 2>/dev/null) || true
185+ if [ -n "$c" ]; then
186+ echo "$c"
187+ return 0
188+ fi
189+ # Tag
190+ c=$(git rev-parse -q --verify "refs/tags/${r}^{commit}" 2>/dev/null) || true
191+ if [ -n "$c" ]; then
192+ echo "$c"
193+ return 0
194+ fi
195+ return 1
196+ }
197+
198+ if [ -n "${MAIN_START_REF// }" ]; then
199+ REF=$(echo "$MAIN_START_REF" | tr -d '[:space:]')
200+ if [ -z "$REF" ]; then
201+ echo "Error: main_start_ref is whitespace-only." >&2
202+ exit 1
203+ fi
204+ # Try to fetch the ref from origin (branches, tags, and SHA objects)
205+ git fetch origin "$REF" 2>/dev/null || true
206+
207+ COMMIT=""
208+ COMMIT=$(resolve_to_commit "$REF") || true
209+ if [ -z "$COMMIT" ]; then
210+ # e.g. short SHA or ref only present after full fetch
211+ git fetch origin "$REF:$REF" 2>/dev/null || true
212+ COMMIT=$(resolve_to_commit "$REF") || true
213+ fi
214+ if [ -z "$COMMIT" ]; then
215+ echo "Error: could not resolve git ref to a commit: $REF" >&2
216+ echo "Try a full SHA, a branch/tag name on origin, or refs/heads/... / refs/tags/..." >&2
217+ exit 1
218+ fi
219+ git checkout "$COMMIT"
220+ echo "Release cut from ref '$REF' -> $COMMIT ($(git log -1 --oneline))"
221+ else
222+ git checkout "${{ env.MAIN_BRANCH }}"
223+ git reset --hard "origin/${{ env.MAIN_BRANCH }}"
224+ echo "Release cut from origin/${{ env.MAIN_BRANCH }} tip ($(git rev-parse --short HEAD))."
225+ fi
226+
128227 - name : Create ephemeral release branch
129228 id : ephemeral-branch
130229 run : |
131230 TIMESTAMP=$(date +%Y%m%d-%H%M%S)
132- EPHEMERAL_BRANCH="release /${{ inputs.crate }}/$TIMESTAMP"
231+ EPHEMERAL_BRANCH="${{ env.RELEASE_BRANCH_PREFIX }} /${{ inputs.crate }}/$TIMESTAMP"
133232 git checkout -b "$EPHEMERAL_BRANCH"
134233 git push origin "$EPHEMERAL_BRANCH"
135- echo "Ephemeral release branch created: $EPHEMERAL_BRANCH from ${{ env.MAIN_BRANCH }} branch "
234+ echo "Ephemeral release branch created: $EPHEMERAL_BRANCH branch ($(git rev-parse --short HEAD)) "
136235 echo "ephemeral_branch=$EPHEMERAL_BRANCH" >> "$GITHUB_OUTPUT"
137236 echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT"
138237
@@ -146,6 +245,7 @@ jobs:
146245 cat /tmp/crates.json
147246
148247 - name : Get commits since last release for each crate
248+ id : commits-since-release
149249 run : |
150250 # Get commits since release for each crate and save to file
151251 ./scripts/commits-since-release.sh "$(cat /tmp/crates.json)" > /tmp/commits-by-crate.json
@@ -154,7 +254,8 @@ jobs:
154254 # so tag/merge-base resolution uses the same ref the script used.
155255 git rev-parse HEAD > /tmp/release_head_sha
156256 echo "Release branch HEAD (saved for later): $(cat /tmp/release_head_sha)"
157-
257+ echo "release_head_sha=$(cat /tmp/release_head_sha)" >> "$GITHUB_OUTPUT"
258+
158259 # Display json output
159260 jq . /tmp/commits-by-crate.json
160261
@@ -264,8 +365,12 @@ jobs:
264365 LATEST_TAG=$(git tag -l "$TAG_PREFIX*" --sort=-v:refname | head -1)
265366 if [ "$LATEST_TAG" != "$TAG" ]; then
266367 echo "Tag $TAG is not the latest. Latest is: $LATEST_TAG. main branch has the latest release for $NAME"
267- echo "Skipping release for $NAME"
268- continue
368+ if [ "${{ inputs.bypass_standard_checks }}" = "false" ]; then
369+ echo "Skipping release for $NAME"
370+ continue
371+ else
372+ echo "Continuing with the release for $NAME because bypass_standard_checks is true"
373+ fi
269374 fi
270375
271376 echo "Executing semver-level.sh for $NAME since $RANGE (tag: $TAG)..."
@@ -360,13 +465,23 @@ jobs:
360465 exit 1
361466 fi
362467
363- echo "Pushing branch $BRANCH_NAME to origin..."
364- git push origin "$BRANCH_NAME"
468+ # Oldest → newest (chronological). Plain `git log` is newest-first; commit-headless should receive
469+ # parent→child order so replays/signing match git history. Space-separated SHAs for the action.
470+ COMMITS=$(git log --reverse "$ORIGINAL_HEAD".. --format='%H' | tr '\n' ' ' | xargs)
471+ echo "commits=$COMMITS" >> $GITHUB_OUTPUT
365472
366473 # Output the results
367474 echo "API changes summary:"
368475 jq . /tmp/api-changes.json
369476
477+ - name : Push commits
478+ uses : DataDog/commit-headless@action/v2.0.3
479+ with :
480+ branch : ${{ steps.proposal-branch.outputs.branch_name }}
481+ head-sha : ${{ steps.commits-since-release.outputs.release_head_sha }}
482+ command : push
483+ commits : " ${{ steps.release-version-bumps.outputs.commits }}"
484+
370485 - name : Upload release data
371486 uses : actions/upload-artifact@v4
372487 with :
@@ -412,7 +527,6 @@ jobs:
412527 name : release-dispatch-data
413528 path : /tmp
414529
415-
416530 - uses : DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
417531 id : octo-sts
418532 with :
@@ -422,9 +536,25 @@ jobs:
422536 - name : Create a PR
423537 env :
424538 GH_TOKEN : ${{ steps.octo-sts.outputs.token }}
539+ MAIN_START_REF : ${{ inputs.main_start_ref }}
540+ MAIN_BRANCH : ${{ env.MAIN_BRANCH }}
541+ BYPASS_STANDARD_CHECKS : ${{ inputs.bypass_standard_checks }}
542+ PROPOSAL_BRANCH_PREFIX : ${{ env.PROPOSAL_BRANCH_PREFIX }}
543+ RELEASE_BRANCH_PREFIX : ${{ env.RELEASE_BRANCH_PREFIX }}
425544 run : |
426545 BRANCH_NAME="${{ needs.cargo-release.outputs.branch_name }}"
427-
546+
547+ NON_DEFAULT=""
548+ if [ -n "$MAIN_START_REF" ]; then
549+ 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'
550+ fi
551+ if [ "$BYPASS_STANDARD_CHECKS" = "true" ]; then
552+ 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'
553+ fi
554+ if [ -n "$NON_DEFAULT" ]; then
555+ NON_DEFAULT="${NON_DEFAULT}"$'\n\n'
556+ fi
557+
428558 # Generate the PR body by merging commits and API changes
429559 # Note: read returns 1 when it reaches EOF, which is expected for heredocs
430560 read -r -d '' JQ_FILTER << 'EOF' || true
@@ -436,7 +566,7 @@ jobs:
436566 [
437567 "## \($crate.name)",
438568 "",
439- (if $api_info.version then "**Next version:** `\($api_info.version)`\n " else null end),
569+ (if $api_info.version then "**Next version:** `\($api_info.version)`" else null end),
440570 "**Semver bump:** `\($api_info.level)`",
441571 (if $api_info.tag then "**Tag:** `\($api_info.tag)`\n" else null end),
442572 (if $api_info.initial_release == "true" then
@@ -446,25 +576,27 @@ jobs:
446576 ] | map(select(. != null and . != "")) | join("\n")
447577 EOF
448578
449- PR_BODY =$(jq -r --slurpfile api /tmp/api-changes.json "$JQ_FILTER" /tmp/commits-by-crate.json)
450-
579+ COMMITS_AND_API_BODY =$(jq -r --slurpfile api /tmp/api-changes.json "$JQ_FILTER" /tmp/commits-by-crate.json)
580+
451581 PR_BODY="# Release proposal for ${{ inputs.crate }} and its dependencies
452582
453583 This PR contains version bumps based on public API changes and commits since last release.
454584
455- ${PR_BODY }"
585+ ${NON_DEFAULT}${COMMITS_AND_API_BODY }"
456586
457- echo "PR_BODY: $PR_BODY"
587+ echo "$PR_BODY" > /tmp/pr-body.md
588+ echo "PR body written to /tmp/pr-body.md (length: $(wc -c < /tmp/pr-body.md) bytes)"
458589
459590 # NOTE: the PR title is used to filter gitlab CI jobs. If you change it, you need to update the gitlab CI job filter.
460591 gh pr create \
461592 --head "$BRANCH_NAME" \
462593 --title "chore(release): proposal for ${{ inputs.crate }}" \
463- --body "$PR_BODY" \
594+ --body-file /tmp/pr-body.md \
464595 --label "release-proposal" \
465596 --label "skip-metadata-check" \
466597 --label "skip-changelog-check" \
467- --base "${{ needs.cargo-release.outputs.ephemeral_branch }}"
598+ --base "${{ needs.cargo-release.outputs.ephemeral_branch }}" \
599+ --draft
468600
469601 - name : Cleanup on failure
470602 if : failure() && (needs.cargo-release.outputs.branch_name != '' || needs.cargo-release.outputs.ephemeral_branch != '')
0 commit comments