-
Notifications
You must be signed in to change notification settings - Fork 21
1070 lines (952 loc) · 52.1 KB
/
Copy pathrelease-proposal-dispatch.yml
File metadata and controls
1070 lines (952 loc) · 52.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
name: Release - Open a release proposal PR
on:
workflow_dispatch:
inputs:
crates:
description: >
Crate(s) to release, comma-separated (e.g. "libdd-common" or "libdd-common, libdd-telemetry"). Trailing and repeated commas are ignored.
Each selected crate is released along with its workspace dependencies (other libdd-* crates it depends on).
Hotfix releases (main_start_ref matching hotfix/<crate>/<N>.x.x) accept a single crate only.
required: true
type: string
default: ''
main_start_ref:
description: >
Optional git ref to cut the release from: commit SHA (short or full), branch name,
tag, or refs/... (e.g. main, v1.2.3, origin/main). Leave empty to use latest
origin/main.
required: false
type: string
default: ''
bypass_standard_checks:
description: >
Skip ongoing-proposal checks. Proposal branches use prefix
release-proposal-testing so they do not collide with normal release-proposal/* runs.
required: false
type: boolean
default: false
concurrency:
group: release-proposal-dispatch-group
cancel-in-progress: false
env:
MAIN_BRANCH: main
RELEASE_BRANCH_PREFIX: ${{ inputs.bypass_standard_checks && 'release-testing' || 'release' }}
PROPOSAL_BRANCH_PREFIX: ${{ inputs.bypass_standard_checks && 'release-proposal-testing' || 'release-proposal' }}
HOTFIX_REF_PATTERN: '^hotfix/[^/]+/[0-9]+\.x\.x$'
jobs:
validate-inputs:
runs-on: ubuntu-latest
outputs:
crates: ${{ steps.normalize.outputs.crates }}
crates_display: ${{ steps.normalize.outputs.crates_display }}
crates_branch: ${{ steps.normalize.outputs.crates_branch }}
count: ${{ steps.normalize.outputs.count }}
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
- name: Exclude from Green CI
env:
DATADOG_SITE: datadoghq.com
DATADOG_API_KEY: ${{ secrets.DATADOG_API_KEY }}
run: ./scripts/exclude-from-green-ci.sh
- name: Normalize and validate crate list
id: normalize
env:
RAW_CRATES: ${{ inputs.crates }}
RAW_MAIN_START_REF: ${{ inputs.main_start_ref }}
HOTFIX_REF_PATTERN: ${{ env.HOTFIX_REF_PATTERN }}
run: |
set -euo pipefail
# Comma-separated list; strip per-element whitespace, drop empties (handles
# trailing/repeated commas), dedupe, sort for stable downstream order. Crate names
# have no internal whitespace, so per-line whitespace removal is safe.
# Use command substitution (not process substitution) so set -e/pipefail catches failures.
NORMALIZED=$(printf '%s\n' "$RAW_CRATES" | tr ',' '\n' | sed 's/[[:space:]]//g; /^$/d' | sort -u)
mapfile -t CRATES <<< "$NORMALIZED"
# An empty NORMALIZED produces a single empty-string element via <<<; drop it.
if [ "${#CRATES[@]}" -eq 1 ] && [ -z "${CRATES[0]}" ]; then
CRATES=()
fi
if [ "${#CRATES[@]}" -eq 0 ]; then
echo "Error: 'crates' input is empty after normalization." >&2
exit 1
fi
METADATA=$(cargo metadata --no-deps --format-version=1)
AVAILABLE_LIST=$(jq -r '
.packages[]
| select(.publish == null or (.publish | type == "array" and length > 0))
| .name
' <<< "$METADATA" | sort -u)
mapfile -t AVAILABLE <<< "$AVAILABLE_LIST"
UNKNOWN=()
for c in "${CRATES[@]}"; do
if ! printf '%s\n' "${AVAILABLE[@]}" | grep -qxF "$c"; then
UNKNOWN+=("$c")
fi
done
if [ "${#UNKNOWN[@]}" -gt 0 ]; then
echo "Error: unknown or unpublishable crate(s): ${UNKNOWN[*]}" >&2
echo "Available crates:" >&2
printf ' - %s\n' "${AVAILABLE[@]}" >&2
exit 1
fi
REF="$(echo "$RAW_MAIN_START_REF" | tr -d '[:space:]')"
if [[ -n "$REF" && "$REF" =~ $HOTFIX_REF_PATTERN ]] && [ "${#CRATES[@]}" -gt 1 ]; then
echo "Error: hotfix releases (main_start_ref=$REF) accept only a single crate; got ${#CRATES[@]}: ${CRATES[*]}" >&2
exit 1
fi
CRATES_SPACE="${CRATES[*]}"
CRATES_DISPLAY=$(IFS=,; echo "${CRATES[*]}")
CRATES_DISPLAY=${CRATES_DISPLAY//,/, }
# Branch segment: keep the path concise when many crates are bundled.
if [ "${#CRATES[@]}" -eq 1 ]; then
CRATES_BRANCH="${CRATES[0]}"
else
CRATES_BRANCH="${CRATES[0]}-$(( ${#CRATES[@]} - 1 ))-more"
fi
echo "Normalized crates: $CRATES_SPACE"
echo "Display: $CRATES_DISPLAY"
echo "Branch segment: $CRATES_BRANCH"
echo "Count: ${#CRATES[@]}"
{
echo "crates=$CRATES_SPACE"
echo "crates_display=$CRATES_DISPLAY"
echo "crates_branch=$CRATES_BRANCH"
echo "count=${#CRATES[@]}"
} >> "$GITHUB_OUTPUT"
check-proposal-ongoing:
runs-on: ubuntu-latest
needs: validate-inputs
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
with:
fetch-depth: 0
fetch-tags: true
- name: Check if a release proposal is ongoing
run: |
if [ "${{ inputs.bypass_standard_checks }}" = "true" ]; then
echo "Skipping ongoing release proposal checks."
else
# Check if there are any proposal branches or ephemeral release branches (release/*/*)
EXISTING_BRANCHES=$(git branch -r --list "origin/${{ env.PROPOSAL_BRANCH_PREFIX }}/*" "origin/${{ env.RELEASE_BRANCH_PREFIX }}/*/*")
if [ -n "$EXISTING_BRANCHES" ]; then
echo "Error: A release proposal is ongoing. Please cancel it or wait for it to be merged." >&2
echo "Existing branches:"
echo "$EXISTING_BRANCHES"
exit 1
fi
echo "No release proposal is ongoing."
fi
check-membership:
if: ${{ !inputs.bypass_standard_checks }}
permissions:
id-token: write # Enable OIDC
runs-on: ubuntu-latest
needs: check-proposal-ongoing
steps:
- uses: DataDog/dd-octo-sts-action@08f2144903ced3254a3dafec2592563409ba2aa0 # v1.0.1
id: octo-sts
with:
scope: DataDog/libdatadog # target repository
policy: self.read.members # trust policy in target repo, without the .sts.yaml extension
- name: Check if user is in the team allowed to make crate releases
id: check
uses: TheModdingInquisition/actions-team-membership@057d91bb80f2976a1bc6dfab5b4ae1da9aebbd89 #v1.0.1
with:
team: 'apm-common-components-core'
organization: 'Datadog'
token: ${{ steps.octo-sts.outputs.token }} # Needs 'read:org' scope
exit: false
- name: Check output
run: |
permitted=${{ steps.check.outputs.permitted }}
if [[ "$permitted" != "true" ]]; then
echo "User is not part of apm-common-components-core"
exit 1
fi
cargo-release:
# Run only when the upstream guards actually passed. check-membership is skipped on bypass runs
# (its own `if`), so accept a skipped membership ONLY when bypassing. A membership skipped for
# any other reason (e.g. check-proposal-ongoing failing) must NOT let the release through, so we
# also require check-proposal-ongoing to have succeeded.
if: ${{ !cancelled() && needs.validate-inputs.result == 'success' && needs.check-proposal-ongoing.result == 'success' && (needs.check-membership.result == 'success' || (inputs.bypass_standard_checks && needs.check-membership.result == 'skipped')) }}
permissions:
id-token: write # Enable OIDC
pull-requests: write
contents: write
needs: [check-proposal-ongoing, check-membership, validate-inputs]
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.92.0
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
with:
ref: ${{ env.MAIN_BRANCH }}
fetch-depth: 0
fetch-tags: true
- name: Read nightly version from nightly-toolchain.toml
id: nightly-version
run: echo "version=$(grep -Po '^channel = "\K[^"]+' nightly-toolchain.toml)" >> $GITHUB_OUTPUT
- uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1
with:
cache-targets: true
- uses: dtolnay/rust-toolchain@nightly
with:
toolchain: ${{ steps.nightly-version.outputs.version }}
- name: Link nightly toolchain for cargo-public-api
run: ln -sf ~/.rustup/toolchains/${{ steps.nightly-version.outputs.version }}-x86_64-unknown-linux-gnu ~/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu
- uses: dtolnay/rust-toolchain@stable
with:
toolchain: ${{ env.RUSTUP_TOOLCHAIN }}
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: cargo-public-api@0.52.0
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: cargo-release
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: git-cliff
- uses: taiki-e/cache-cargo-install-action@7447f04c51f2ba27ca35e7f1e28fab848c5b3ba7 # 2.3.1
with:
tool: cargo-semver-checks@0.48.0
- uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
id: octo-sts
if: ${{ !inputs.bypass_standard_checks }}
with:
scope: DataDog/libdatadog
policy: self.write.pr
- name: Configure Git for signing
env:
GH_TOKEN: ${{ inputs.bypass_standard_checks && github.token || steps.octo-sts.outputs.token }}
GITHUB_ACTOR: ${{ github.actor }}
run: |
# GET /user is not allowed with installation tokens; use GET /users/ACTOR (who triggered the workflow).
GITHUB_USER_RESPONSE=$(curl -s -H "Authorization: token ${GH_TOKEN}" "https://api.github.com/users/${GITHUB_ACTOR}")
GIT_USER_NAME=$(echo "${GITHUB_USER_RESPONSE}" | jq -r '.login // empty')
GIT_USER_ID=$(echo "${GITHUB_USER_RESPONSE}" | jq -r '.id // empty')
if [[ -z "$GIT_USER_NAME" ]]; then
GIT_USER_NAME="${GITHUB_ACTOR}"
GIT_USER_EMAIL="${GITHUB_ACTOR}@users.noreply.github.com"
else
GIT_USER_EMAIL="${GIT_USER_ID}+${GIT_USER_NAME}@users.noreply.github.com"
fi
echo "GIT_USER_NAME: $GIT_USER_NAME"
echo "GIT_USER_EMAIL: $GIT_USER_EMAIL"
git config --global user.name "$GIT_USER_NAME"
git config --global user.email "$GIT_USER_EMAIL"
- name: Snapshot scripts from workflow revision
run: |
set -euo pipefail
# workflow_dispatch runs the workflow file from the branch selected in the UI; github.sha is that commit.
# main_start_ref may be older and not contain scripts/ — always run release tooling from the workflow revision.
WF_SHA="${{ github.sha }}"
git fetch --no-tags origin "$WF_SHA" 2>/dev/null || true
if ! git rev-parse -q --verify "${WF_SHA}^{commit}" >/dev/null; then
echo "Error: could not resolve workflow revision ${WF_SHA}." >&2
exit 1
fi
DEST="/tmp/workflow-scripts-libdatadog"
rm -rf "$DEST"
mkdir -p "$DEST"
git archive "$WF_SHA" scripts | tar -x -C "$DEST"
echo "WORKFLOW_SCRIPTS_ROOT=${DEST}/scripts" >> "$GITHUB_ENV"
echo "Release scripts pinned to workflow revision $(git rev-parse --short "$WF_SHA") (${WF_SHA})"
- name: Optionally checkout at a specific git ref
env:
MAIN_START_REF: ${{ inputs.main_start_ref }}
run: |
set -euo pipefail
git fetch origin "${{ env.MAIN_BRANCH }}" --tags --prune
resolve_to_commit() {
local r="$1"
local c=""
# Already a commit or resolvable locally
c=$(git rev-parse -q --verify "${r}^{commit}" 2>/dev/null) || true
if [ -n "$c" ]; then
echo "$c"
return 0
fi
# Remote branch: origin/<name>
c=$(git rev-parse -q --verify "origin/${r}^{commit}" 2>/dev/null) || true
if [ -n "$c" ]; then
echo "$c"
return 0
fi
# Tag
c=$(git rev-parse -q --verify "refs/tags/${r}^{commit}" 2>/dev/null) || true
if [ -n "$c" ]; then
echo "$c"
return 0
fi
return 1
}
if [ -n "${MAIN_START_REF// }" ]; then
REF=$(echo "$MAIN_START_REF" | tr -d '[:space:]')
if [ -z "$REF" ]; then
echo "Error: main_start_ref is whitespace-only." >&2
exit 1
fi
# Try to fetch the ref from origin (branches, tags, and SHA objects)
git fetch origin "$REF" 2>/dev/null || true
COMMIT=""
COMMIT=$(resolve_to_commit "$REF") || true
if [ -z "$COMMIT" ]; then
# e.g. short SHA or ref only present after full fetch
git fetch origin "$REF:$REF" 2>/dev/null || true
COMMIT=$(resolve_to_commit "$REF") || true
fi
if [ -z "$COMMIT" ]; then
echo "Error: could not resolve git ref to a commit: $REF" >&2
echo "Try a full SHA, a branch/tag name on origin, or refs/heads/... / refs/tags/..." >&2
exit 1
fi
# Reject pull-request refs outright: they can be pushed by anyone with a fork.
case "$REF" in
refs/pull/*|pull/*)
echo "Error: refs/pull/* refs are not allowed as main_start_ref." >&2
exit 1
;;
esac
# Verify the resolved commit is reachable from a trusted ref before checking it out.
# Trusted: origin/${{ env.MAIN_BRANCH }}, or the matching origin/hotfix/<crate>/N.x.x branch.
TRUSTED=false
if git merge-base --is-ancestor "$COMMIT" "origin/${{ env.MAIN_BRANCH }}" 2>/dev/null; then
TRUSTED=true
elif [[ "$REF" =~ $HOTFIX_REF_PATTERN ]] \
&& git rev-parse -q --verify "origin/${REF}^{commit}" >/dev/null 2>&1 \
&& git merge-base --is-ancestor "$COMMIT" "origin/${REF}" 2>/dev/null; then
TRUSTED=true
fi
if [ "$TRUSTED" != "true" ]; then
echo "Error: resolved commit ${COMMIT} is not reachable from origin/${{ env.MAIN_BRANCH }} or a recognised origin/hotfix/<crate>/N.x.x branch." >&2
echo "main_start_ref must point at a commit on a trusted branch." >&2
exit 1
fi
# Hotfix releases use the hotfix branch itself as the ephemeral branch.
# If a hotfix branch name was provided and exists on origin, check it out as a branch
# (not a detached commit) so later steps can use it as a PR base.
if [[ "$REF" =~ $HOTFIX_REF_PATTERN ]] && git rev-parse -q --verify "origin/$REF^{commit}" >/dev/null 2>&1; then
git checkout -B "$REF" "origin/$REF"
echo "Release cut from hotfix branch '$REF' -> $(git rev-parse --short HEAD) ($(git log -1 --oneline))"
else
git checkout "$COMMIT"
echo "Release cut from ref '$REF' -> $COMMIT ($(git log -1 --oneline))"
fi
else
git checkout "${{ env.MAIN_BRANCH }}"
git reset --hard "origin/${{ env.MAIN_BRANCH }}"
echo "Release cut from origin/${{ env.MAIN_BRANCH }} tip ($(git rev-parse --short HEAD))."
fi
- name: Reject untrusted cargo-release configuration
run: |
set -euo pipefail
# cargo-release supports pre-release-hook (arbitrary command execution) and
# pre-release-replacements (arbitrary file rewriting). Neither is used by this repo,
# so reject any tree that mentions them. This stops a malicious main_start_ref from
# executing code with the job's OIDC mint capability via the cargo-release step.
#
# The match is intentionally broad: it catches every TOML key form cargo-release
# accepts — bare `pre-release-hook = ...`, dotted `package.metadata.release.pre-release-hook = ...`,
# quoted `"pre-release-hook" = ...`, inline-table `{ pre-release-hook = ... }`, and
# array-of-table `[[package.metadata.release.pre-release-replacements]]`. `^[^#]*`
# excludes the substring when it only appears after a `#` comment marker.
FORBIDDEN=$(grep -rEn '^[^#]*(pre-release-hook|pre-release-replacements)' \
--include='Cargo.toml' --include='release.toml' . || true)
if [ -n "$FORBIDDEN" ]; then
echo "Error: forbidden cargo-release configuration detected in the checked-out tree:" >&2
echo "$FORBIDDEN" >&2
exit 1
fi
- name: Create ephemeral release branch
id: ephemeral-branch
env:
MAIN_START_REF: ${{ inputs.main_start_ref }}
run: |
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
REF="$(echo "${MAIN_START_REF:-}" | tr -d '[:space:]')"
if [[ -n "$REF" && "$REF" =~ $HOTFIX_REF_PATTERN ]]; then
# Hotfix: use the hotfix branch itself as the ephemeral branch (no new branch created).
if ! git rev-parse -q --verify "origin/$REF^{commit}" >/dev/null 2>&1; then
echo "Error: hotfix branch does not exist on origin: $REF" >&2
exit 1
fi
EPHEMERAL_BRANCH="$REF"
echo "Hotfix mode: using existing branch as ephemeral base: $EPHEMERAL_BRANCH"
echo "ephemeral_branch=$EPHEMERAL_BRANCH" >> "$GITHUB_OUTPUT"
echo "is_hotfix=true" >> "$GITHUB_OUTPUT"
else
EPHEMERAL_BRANCH="${{ env.RELEASE_BRANCH_PREFIX }}/${{ needs.validate-inputs.outputs.crates_branch }}/$TIMESTAMP"
git checkout -b "$EPHEMERAL_BRANCH"
git push origin "$EPHEMERAL_BRANCH"
echo "Ephemeral release branch created: $EPHEMERAL_BRANCH branch ($(git rev-parse --short HEAD))"
echo "ephemeral_branch=$EPHEMERAL_BRANCH" >> "$GITHUB_OUTPUT"
echo "is_hotfix=false" >> "$GITHUB_OUTPUT"
fi
echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT"
- name: Get publication order for crate and dependencies
env:
CRATES: ${{ needs.validate-inputs.outputs.crates }}
run: |
echo "Getting publication order for ${{ needs.validate-inputs.outputs.crates_display }}..."
# CRATES is a space-separated list validated upstream; word-splitting is intentional.
# shellcheck disable=SC2086
"${WORKFLOW_SCRIPTS_ROOT}/publication-order.sh" --format=json $CRATES > /tmp/crates.json
echo "Publication order:"
cat /tmp/crates.json
- name: Get commits since last release for each crate
id: commits-since-release
run: |
# Get commits since release for each crate and save to file
"${WORKFLOW_SCRIPTS_ROOT}/commits-since-release.sh" "$(cat /tmp/crates.json)" > /tmp/commits-by-crate.json
# Capture ephemeral release branch tip now. Use this in Release version bumps
# so tag/merge-base resolution uses the same ref the script used.
git rev-parse HEAD > /tmp/release_head_sha
echo "Release branch HEAD (saved for later): $(cat /tmp/release_head_sha)"
echo "release_head_sha=$(cat /tmp/release_head_sha)" >> "$GITHUB_OUTPUT"
# Display json output
jq . /tmp/commits-by-crate.json
- name: Create a branch for the release proposal
id: proposal-branch
run: |
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
echo "Repository is shallow, fetching full history..."
git fetch --unshallow
fi
# TODO: check if this is really necessary, we should have the full history from
# previous steps.
# Assure we have the full history, 2147483647 is the highest integer number that
# git accepts.
git fetch --depth=2147483647 --tags origin "${{ env.MAIN_BRANCH }}" "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"
git checkout "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"
TIMESTAMP="${{ steps.ephemeral-branch.outputs.timestamp }}"
BRANCH_NAME="${{ env.PROPOSAL_BRANCH_PREFIX }}/${{ needs.validate-inputs.outputs.crates_branch }}/$TIMESTAMP"
git checkout -b "$BRANCH_NAME"
git push origin "$BRANCH_NAME" --tags
echo "Branch created: $BRANCH_NAME from ${{ steps.ephemeral-branch.outputs.ephemeral_branch }} branch"
echo "branch_name=$BRANCH_NAME" >> "$GITHUB_OUTPUT"
- name: Release version bumps
id: release-version-bumps
run: |
echo "Release version bumps..."
# TODO: check if this is really necessary, we should have the full history from
# previous steps.
if [ "$(git rev-parse --is-shallow-repository)" = "true" ]; then
echo "Repository is shallow, fetching full history..."
git fetch --unshallow
fi
# Initialize results array
echo "[]" > /tmp/api-changes.json
# Crates with no commits of their own are not released here, but recorded as candidates:
echo "[]" > /tmp/pending-major-only.json
# Use release branch tip from when we ran commits-since-release (same ref the script used).
# Avoids tag/merge-base resolution failures after switching to the new proposal branch.
ORIGINAL_HEAD=$(cat /tmp/release_head_sha)
echo "ORIGINAL_HEAD: $ORIGINAL_HEAD"
BRANCH_NAME="${{ steps.proposal-branch.outputs.branch_name }}"
# iterate over the commits and execute cargo release for each crate
jq -c '.[]' /tmp/commits-by-crate.json | while read -r crate; do
NAME=$(echo "$crate" | jq -r '.name')
TAG=$(echo "$crate" | jq -r '.tag')
TAG_PREFIX="$NAME-v"
CRATE_PATH=$(echo "$crate" | jq -r '.path')
TAG_EXISTS=$(echo "$crate" | jq -r '.tag_exists')
COMMITS=$(echo "$crate" | jq -r '.commits')
INITIAL_RELEASE=false
# if there are no commits and there is an existing tag, do not release the crate here.
# but record it as a pending candidate
if [ "$COMMITS" = "[]" ] && [ "$TAG_EXISTS" = "true" ]; then
VERSION=$(echo "$crate" | jq -r '.version')
echo "No commits since last release for $NAME; deferring to the libdd-* major-bump check"
jq --arg name "$NAME" \
--arg tag "$TAG" \
--arg version "$VERSION" \
--arg path "$CRATE_PATH" \
'. += [{"name": $name, "level": "none", "tag": $tag, "prev_tag": $tag, "version": $version, "range": "", "commits": [], "path": $path, "initial_release": "false", "pending_release": "true"}]' \
/tmp/pending-major-only.json > /tmp/pending-major-only.tmp && mv /tmp/pending-major-only.tmp /tmp/pending-major-only.json
continue
fi
if [ "$TAG_EXISTS" = "true" ]; then
# Explicitly dereference annotated tags to their underlying commit.
# Several git commands do not consistently dereference annotated tag objects
# across all git versions.
TAG_COMMIT=$(git rev-parse "${TAG}^{}" 2>/dev/null || echo "")
if [ -z "$TAG_COMMIT" ]; then
echo "ERROR: Could not dereference tag $TAG to a commit" >&2
exit 1
fi
RANGE="$TAG_COMMIT..$ORIGINAL_HEAD"
echo "Using $RANGE as range (tag: $TAG)"
if git merge-base --is-ancestor "$TAG_COMMIT" "$ORIGINAL_HEAD" 2>/dev/null; then
echo " Tag $TAG is ancestor of HEAD"
else
MERGE_BASE=$(git merge-base "$TAG_COMMIT" "$ORIGINAL_HEAD" 2>/dev/null || echo "")
if [ -n "$MERGE_BASE" ]; then
RANGE="$MERGE_BASE..$ORIGINAL_HEAD"
echo " Tag $TAG is NOT ancestor of HEAD, using merge-base: $RANGE"
else
# No common ancestor, tag is on unrelated history. Derive the range start from
# the parent of the oldest commit found by commits-since-release.sh. That way
# git can compute TREESAME correctly and the path filter won't include unrelated commits.
OLDEST_COMMIT=$(echo "$COMMITS" | jq -r '.[-1].hash // empty')
OLDEST_PARENT=$(git rev-parse "${OLDEST_COMMIT}^" 2>/dev/null || echo "")
if [ -n "$OLDEST_PARENT" ]; then
RANGE="$OLDEST_PARENT..$ORIGINAL_HEAD"
echo " No common ancestor with tag $TAG: using parent of oldest commit as range start: $RANGE"
else
echo " WARNING: Could not find merge-base for tag $TAG, using $RANGE"
fi
fi
fi
BRANCHES=$(git branch --contains "$TAG_COMMIT" 2>/dev/null || echo "")
if [ -n "$BRANCHES" ]; then
echo "Tag $TAG is in branches: $BRANCHES"
else
echo "Tag $TAG (commit $TAG_COMMIT) is not in any local branch (normal for squash-merged releases)"
fi
# if there is a tag more recent than $TAG, continue the loop
LATEST_TAG=$(git tag -l "$TAG_PREFIX*" --sort=-v:refname | head -1)
if [ "$LATEST_TAG" != "$TAG" ]; then
echo "Tag $TAG is not the latest. Latest is: $LATEST_TAG. main branch has the latest release for $NAME"
# do not skip the release for hotfix branches
if [ "${{ steps.ephemeral-branch.outputs.is_hotfix }}" = "true" ]; then
echo "Continuing with the release for $NAME because it is a hotfix"
else
if [ "${{ inputs.bypass_standard_checks }}" = "false" ]; then
echo "Skipping release for $NAME"
continue
else
echo "Continuing with the release for $NAME because bypass_standard_checks is true"
fi
fi
fi
echo "Executing semver-level.sh for $NAME since $RANGE (tag: $TAG)..."
SEMVER_LEVEL=$("${WORKFLOW_SCRIPTS_ROOT}/semver-level.sh" "$NAME" "refs/tags/$TAG" 2>&1)
echo "Semver level: $SEMVER_LEVEL"
LEVEL=$(echo "$SEMVER_LEVEL" | jq -r '.level')
echo "Executing cargo release for $NAME since $TAG with level $LEVEL..."
cargo release version -p "$NAME" --prev-tag-name "$TAG" --allow-branch "$BRANCH_NAME" -x $LEVEL --no-confirm
else
echo "No previous release tag for $NAME, preparing initial release..."
# Use the version from the crate metadata
VERSION=$(echo "$crate" | jq -r '.version')
LEVEL="major"
TAG=""
RANGE=""
# fail when the version is not an initial release
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" --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 or pending
if git diff --quiet "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"; then
PENDING_COUNT=$(jq 'length' /tmp/pending-major-only.json)
if [ "$PENDING_COUNT" -gt 0 ]; then
echo "No direct version bumps yet, but $PENDING_COUNT crate(s) are pending libdd-* major-bump evaluation; continuing."
else
echo "No changes to push. Cancelling the workflow."
exit 1
fi
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 }}"
# Audit input: crates released in the previous step (api-changes.json) plus the pending
# no-commit candidates. The pending rows carry "pending_release": "true" so we can tell
# them apart below; every row is checked the same way for direct libdd-* major bumps.
jq -s '.[0] + .[1]' /tmp/api-changes.json /tmp/pending-major-only.json > /tmp/major-bumps-input.json
echo "Major-bump audit input:"
jq . /tmp/major-bumps-input.json
# Run the audit in a throwaway worktree so extra worktrees / cargo metadata do not touch
# the job checkout. Check it out at the proposal branch tip (HEAD) — the released ref plus
# this run's version bumps from the previous step. This is deliberate on both ends:
# - It includes the dependency-requirement rewrites cargo-release made in the previous
# step, so a dependency bumped to a new major IN THIS proposal propagates a major bump
# to its dependents (e.g. protobuf 3->4 forces its dependents major).
# - It is built from the released ref, NOT github.sha, so changes present only on current
# main (and absent from a hotfix/older-ref release) never trigger a spurious bump.
MAJOR_BUMPS_WT=$(mktemp -d "${RUNNER_TEMP:-/tmp}/major-bumps-wt.XXXXXX")
PROPOSAL_SHA=$(git rev-parse HEAD)
git worktree add --detach "$MAJOR_BUMPS_WT" "$PROPOSAL_SHA"
set +e
( cd "$MAJOR_BUMPS_WT" && "${WORKFLOW_SCRIPTS_ROOT}/major-bumps-level.sh" /tmp/major-bumps-input.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
# Seed the result with every already-released crate. Pending crates are appended below
# only if they earn a major bump; those that do not stay out of the release entirely.
jq '[.[] | select(.pending_release != "true") | del(.pending_release)]' \
/tmp/api-changes-with-major-bumps-pre-commit.json > /tmp/api-changes-with-major-bumps.json
# iterate over the crates and, where a direct libdd-* dependency had a major bump, 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')
LEVEL=$(echo "$bump" | jq -r '.level')
PREV_TAG=$(echo "$bump" | jq -r '.prev_tag')
TAG=$(echo "$bump" | jq -r '.tag')
VERSION=$(echo "$bump" | jq -r '.version')
PENDING=$(echo "$bump" | jq -r '.pending_release // "false"')
MAJOR_BUMPS=$(echo "$bump" | jq -c '.major_bumps')
if [ "$MAJOR_BUMPS" = "[]" ]; then
if [ "$PENDING" = "true" ]; then
echo "No commits and no direct dependency major bumps for $NAME, keeping it out of the release"
fi
continue
fi
# A crate already bumped to major in the previous step needs nothing more. Pending
# crates always have level "none" here, so this only short-circuits released crates.
if [ "$LEVEL" = "major" ]; then
echo "Skipping $NAME: already bumped at major level in the previous step (major_bumps: $MAJOR_BUMPS)"
continue
fi
# Bump to major: either a pending (no-commit) crate whose direct dependency went major,
# or a released crate bumped below major in the previous step. Both are handled the same.
echo "Bumping $NAME to major due to direct dependency 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"
# Released crates are already in the result (seeded above): update them in place. Pending
# crates are not: append them. The row is derived from the audit entry either way.
ROW=$(echo "$bump" | jq --arg version "$NEXT_VERSION" --arg tag "$NEXT_TAG" \
'del(.pending_release) | . + {level: "major", version: $version, tag: $tag}')
jq --argjson row "$ROW" \
'if any(.[]; .name == $row.name)
then map(if .name == $row.name then $row else . end)
else . + [$row] 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
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')
VERSION=$(echo "$bump" | jq -r '.version')
CRATE_PATH=$(echo "$bump" | jq -r '.path')
INITIAL_RELEASE=$(echo "$bump" | jq -r '.initial_release')
MAJOR_BUMPS=$(echo "$bump" | jq -c '.major_bumps // []')
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' "$VERSION" "$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
# FIXME: $COMMITS could be empty if there are no commits since last release
if [ "$COMMITS" = "[]" ]; then
if [ "$MAJOR_BUMPS" != "[]" ] && [ "$MAJOR_BUMPS" != "null" ]; then
echo "No commits for $NAME but direct dependency major bumps; writing a minimal CHANGELOG entry"
RELEASE_DATE=$(date +%Y-%m-%d)
DEP_LINES=$(echo "$MAJOR_BUMPS" | jq -r '.[] | "- Bump `\(.dependency)` to a new major version (`\(.previous_req)` → `\(.current_req)`)"')
# Match git-cliff's header (see cliff.toml): link the version to a compare view
# against the previous tag when one exists.
REMOTE_URL="https://github.com/datadog/libdatadog"
if [ -n "$TAG" ] && [ "$TAG" != "null" ]; then
HEADER="## [$VERSION]($REMOTE_URL/compare/$TAG..$NEXT_TAG) - $RELEASE_DATE"
else
HEADER="## [$VERSION] - $RELEASE_DATE"
fi
ENTRY_FILE=$(mktemp /tmp/changelog-entry-XXXXXX.md)
printf '%s\n\n### Changed\n\n%s\n\n' "$HEADER" "$DEP_LINES" > "$ENTRY_FILE"
if [ -f "$CRATE_PATH/CHANGELOG.md" ]; then
# Insert the new section above the first existing release section (newest-first),
# mirroring git-cliff --prepend placement and leaving the rest of the file intact.
awk 'NR==FNR { e = e $0 ORS; next }
!inserted && /^## / { printf "%s", e; inserted=1 }
{ print }
END { if (!inserted) printf "%s", e }' \
"$ENTRY_FILE" "$CRATE_PATH/CHANGELOG.md" > "$CRATE_PATH/CHANGELOG.md.tmp"
mv "$CRATE_PATH/CHANGELOG.md.tmp" "$CRATE_PATH/CHANGELOG.md"
else
printf '# Changelog\n\n\n' > "$CRATE_PATH/CHANGELOG.md"
cat "$ENTRY_FILE" >> "$CRATE_PATH/CHANGELOG.md"
fi
rm -f "$ENTRY_FILE"
git add "$CRATE_PATH/CHANGELOG.md"
git commit -m "chore(release): update CHANGELOG.md for $NAME"
else
echo "No commits since last release for $NAME, skipping CHANGELOG generation"
fi
continue
fi
# 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
# go through git-cliff filtering process.
NEWEST_COMMIT=$(echo "$COMMITS" | jq -r '.[0].hash // empty')
OLDEST_COMMIT=$(echo "$COMMITS" | jq -r '.[-1].hash // empty')
OLDEST_PARENT=$(git rev-parse "${OLDEST_COMMIT}^" 2>/dev/null || echo "")
if [ -n "$OLDEST_PARENT" ] && [ -n "$NEWEST_COMMIT" ]; then
COMMITS_RANGE="$OLDEST_PARENT..$NEWEST_COMMIT"
else
COMMITS_RANGE="$RANGE"
fi
echo "Executing git cliff for $NAME since $COMMITS_RANGE (oldest: $OLDEST_COMMIT, newest: $NEWEST_COMMIT), next tag: $NEXT_TAG..."
# git-cliff's --include-path uses cumulative tree diffs rather than per-commit
# diffs. This causes commits that don't touch the crate to pass the filter if an
# earlier commit in the range does touch it. In order to avoid that a first pass
# will generate the context inside the commit range and then a second step will
# filter the the commits according to the previously computed range stored in COMMITS.
CLIFF_CONTEXT_FILE=$(mktemp /tmp/git-cliff-context-XXXXXX.json)
CLIFF_HASHES_FILE=$(mktemp /tmp/git-cliff-hashes-XXXXXX.json)
CLIFF_FILTERED_FILE=$(mktemp /tmp/git-cliff-filtered-XXXXXX.json)
git cliff --context --tag "$NEXT_TAG" --ignore-tags ".*" -v "$COMMITS_RANGE" > "$CLIFF_CONTEXT_FILE"
echo "$COMMITS" | jq '[.[].hash]' > "$CLIFF_HASHES_FILE"
jq --slurpfile hashes "$CLIFF_HASHES_FILE" \
--arg prev_tag "$TAG" \
'map(. + {
commits: [.commits[] | select(.id | IN($hashes[0][]))],
previous: (.previous + {"version": $prev_tag})
})' \
"$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"
git commit -m "chore(release): update CHANGELOG.md for $NAME"
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
# Oldest → newest (chronological). Plain `git log` is newest-first; commit-headless should receive
# parent → child 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
- name: Push commits (verified)
if: ${{ !inputs.bypass_standard_checks }}
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.generate-changelogs.outputs.commits }}"
- name: Push commits (plain, testing only)
if: ${{ inputs.bypass_standard_checks }}
run: git push origin "HEAD:refs/heads/${{ steps.proposal-branch.outputs.branch_name }}"
- name: Upload release data
uses: actions/upload-artifact@v4
with:
name: release-dispatch-data
path: |
/tmp/commits-by-crate.json
/tmp/api-changes.json
/tmp/api-changes-with-major-bumps.json
retention-days: 1
- name: Cleanup on failure
if: failure() && (steps.proposal-branch.outputs.branch_name != '' || steps.ephemeral-branch.outputs.ephemeral_branch != '')
run: |
BRANCH_NAME="${{ steps.proposal-branch.outputs.branch_name }}"
if [ -n "$BRANCH_NAME" ]; then
echo "Job failed, deleting branch $BRANCH_NAME..."
git push origin --delete "$BRANCH_NAME" || echo "Failed to delete branch (may not exist on remote)"
fi
EPHEMERAL_BRANCH="${{ steps.ephemeral-branch.outputs.ephemeral_branch }}"
if [ -n "$EPHEMERAL_BRANCH" ]; then
if [ "${{ steps.ephemeral-branch.outputs.is_hotfix }}" = "true" ]; then
echo "Hotfix mode: not deleting ephemeral base branch $EPHEMERAL_BRANCH"
else
echo "Deleting ephemeral release branch $EPHEMERAL_BRANCH..."
git push origin --delete "$EPHEMERAL_BRANCH" || echo "Failed to delete ephemeral branch (may not exist on remote)"
fi
fi
outputs:
branch_name: ${{ steps.proposal-branch.outputs.branch_name }}
ephemeral_branch: ${{ steps.ephemeral-branch.outputs.ephemeral_branch }}
is_hotfix: ${{ steps.ephemeral-branch.outputs.is_hotfix }}
create-pr:
# Without an explicit condition, the skipped check-membership job (on bypass runs) propagates
# its "skipped" status transitively through cargo-release to here. Gate on the direct needs instead.
if: ${{ !cancelled() && needs.cargo-release.result == 'success' && needs.validate-inputs.result == 'success' }}
needs: [cargo-release, validate-inputs]
runs-on: ubuntu-latest
permissions:
id-token: write # Enable OIDC
pull-requests: write
contents: write
steps:
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2
with:
ref: ${{ needs.cargo-release.outputs.branch_name }}
- name: Download release data
uses: actions/download-artifact@v4
with:
name: release-dispatch-data
path: /tmp
- uses: DataDog/dd-octo-sts-action@acaa02eee7e3bb0839e4272dacb37b8f3b58ba80 # v1.0.3
id: octo-sts
if: ${{ !inputs.bypass_standard_checks }}
with:
scope: DataDog/libdatadog
policy: self.write.pr
- name: Create a PR
env:
GH_TOKEN: ${{ inputs.bypass_standard_checks && github.token || steps.octo-sts.outputs.token }}
MAIN_START_REF: ${{ inputs.main_start_ref }}
MAIN_BRANCH: ${{ env.MAIN_BRANCH }}
BYPASS_STANDARD_CHECKS: ${{ inputs.bypass_standard_checks }}
PROPOSAL_BRANCH_PREFIX: ${{ env.PROPOSAL_BRANCH_PREFIX }}
RELEASE_BRANCH_PREFIX: ${{ env.RELEASE_BRANCH_PREFIX }}
CRATES_COUNT: ${{ needs.validate-inputs.outputs.count }}
run: |
BRANCH_NAME="${{ needs.cargo-release.outputs.branch_name }}"
EPHEMERAL_BRANCH="${{ needs.cargo-release.outputs.ephemeral_branch }}"
HOTFIX_NOTE=""
if [ "${{ needs.cargo-release.outputs.is_hotfix }}" = "true" ] && [ -n "$EPHEMERAL_BRANCH" ]; then
HOTFIX_NOTE=$'### :adhesive_bandage: Hotfix\n\n'"This is a **hotfix** release proposal. The pull request targets \`$EPHEMERAL_BRANCH\`."$'\n\n'
fi
NON_DEFAULT=""
if [ -n "$MAIN_START_REF" ]; then
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### :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
# 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
[ $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 -nr --slurpfile api /tmp/api-changes-with-major-bumps.json "$JQ_FILTER")
if [ "$CRATES_COUNT" -gt 1 ]; then
POSSESSIVE="their"
else
POSSESSIVE="its"
fi
PR_BODY="# Release proposal for ${{ needs.validate-inputs.outputs.crates_display }} and $POSSESSIVE dependencies