Skip to content

refactor(ci): release proposal dispatch wf - #2330

Draft
iunanua wants to merge 13 commits into
mainfrom
igor/versioning/proposal-wf-cleanup
Draft

refactor(ci): release proposal dispatch wf#2330
iunanua wants to merge 13 commits into
mainfrom
igor/versioning/proposal-wf-cleanup

Conversation

@iunanua

@iunanua iunanua commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Move inline bash script in the workflow into separated scripts:

  • semver-level.sh: now handles correctly failures
  • commits-since-release.sh: now includes 'tag_in_local_branch', 'latest_tag' and 'range' in the output json needed by release-version-bumps.sh
  • major-bumps-level.sh: removed not used code
  • check_cargo_metadata.sh: clean up
  • release-version-bumps.sh: Replaces 'Release version bumps' step
  • release-version-major-bumps.sh: Replaces 'Update version for crates with libdd-* …' step
  • release-generate-changelogs.sh: Replaces 'Generate CHANGELOGS' step

Changes

  • set -euo pipefail in the extracted scripts. The steps ran under GitHub's default bash -e {0}: errexit on, but pipefail and nounset off. Concretely, a failing jq -c '.[]' /tmp/commits-by-crate.json left the loop with no input and the step exited 0, releasing nothing.
  • A semver-level.sh failure now prints why. Its output was captured with 2>&1 into a variable that errexit then discarded.
  • api-changes.json now includes the deferred crates. It is in the release-dispatch-data artifacts.

iunanua and others added 13 commits August 6, 2026 13:14
compute_semver_results runs inside a command substitution, so the `exit`
calls in its error paths only terminated that subshell. Every failure path
-- an unparsed cargo-semver-checks result, an unexpected exit code, a
cargo-public-api error -- left RESULT_JSON empty and still exited 0.

release-proposal-dispatch.yml reads the level with `jq -r '.level'` from
that output and passes it to `cargo release version -x $LEVEL` in a step
that does not set -e, so the failure surfaced as a bump with an empty
level rather than as a stopped release.

Capture the substitution's status and propagate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- major-bumps-level.sh: drop the unused `level_rank` helper and the unused
  `FAIL` variable (SC2034).
- check_cargo_metadata.sh: declare and assign separately so the command
  substitution's exit status is not masked by `local` (SC2155).

No behaviour change. This lets `shellcheck --severity=warning` run over
scripts/*.sh, which nothing covers today -- actionlint only shellchecks the
`run:` blocks inside workflow files.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ce-release.sh

commits-since-release.sh already worked out the commit range for its own
path-filtered `git log` and then discarded it; the "Release version bumps"
step recomputed the same three-way decision -- tag commit, merge-base when
the tag is not an ancestor of HEAD, or the parent of the oldest commit found
when the tag sits on unrelated history -- eighty lines later.

Export it instead, as two new fields on each crate:

  range       <start-sha>..<head-sha>, resolved to SHAs rather than tag
              names. Empty when the crate has no previous release tag.
  tag_commit  the dereferenced tag, which the workflow still wants for its
              `git branch --contains` diagnostic.

HEAD is resolved once, before the loop, so every crate's range ends at the
same commit. That is what ORIGINAL_HEAD existed for: the workflow captured
`git rev-parse HEAD` immediately after running the script so its own
recomputation would resolve against the same ref. With the range coming from
the script that coupling is gone, and ORIGINAL_HEAD drops out of the bumps
step (Generate CHANGELOGS still uses it for the commit list handed to the
verified push).

Also fixes a defect the move exposed. `git rev-parse <root-commit>^` exits
non-zero but still echoes "<sha>^" on stdout, so the `|| echo ""` guarding
the oldest-commit fallback could never fire and the range would start at a
ref that does not resolve. --verify makes the failure produce no output, as
that fallback was written to expect. The same line appears in Generate
CHANGELOGS, where it would hand git-cliff the bad range start; fixed there
too rather than leaving one of the two broken.

No change to the shape of api-changes.json: `range` was already carried
there and consumed as the git-cliff fallback.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n api-changes.json

pending-major-only.json had two readers: a `jq length` for the "no changes to
push" guard, and a `jq -s '.[0] + .[1]'` in the very next step that merged it
straight back into api-changes.json. Everything after that merge already told
the two kinds of row apart by the "pending_release" field, never by which file
they came from -- the seed filter, the PENDING check in the bump loop, and the
del(.pending_release) in the rebuilt row. The split carried no information the
flag did not.

Write the pending candidates into api-changes.json directly, count them with a
filter instead of a file length, and hand that one file to
major-bumps-level.sh.

Two things change:

- api-changes.json is uploaded in the release-dispatch-data artifact and
  pending-major-only.json was not, so a crate that silently dropped out of a
  release left no trace in the artifact. It does now.
- The audit input is in publication order with pending rows interleaved, rather
  than all released rows followed by all pending ones. The resulting
  api-changes-with-major-bumps.json is byte-identical (verified by replaying the
  seed and update-or-append merge over both orderings): the seed preserves the
  relative order of released rows, and promoted pending rows are still appended
  in the same sequence. Only the order of the "update version for $NAME with
  major bumps" commits differs, and publication order is the more sensible one.

Also corrects the PR-body comment, which claimed api-changes-with-major-bumps
holds the same crates as api-changes.json; it is the release set, i.e. that file
minus the pending candidates that did not earn a major bump.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ase.sh

`git tag -l "$NAME-v*" --sort=-v:refname | head -1` sat in the middle of the
bump loop, one of the last pieces of tag resolution the workflow still did for
itself. It answers a question about a crate's release history, which is what
commits-since-release.sh is for, so export it as `latest_tag` and have the
workflow read the field.

Resolving it in the script also puts it on the same tag snapshot as `range` and
`tag_commit`. The workflow computed it two steps later, after "Create a branch
for the release proposal" runs another `git fetch --tags`; the skip rule and
the commit range could in principle have been decided against different views
of the tags.

The script resolves it for every crate, not only those whose own tag exists, so
a manifest version bumped without a release -- tag_exists false, older tags
present -- is now visible in the artifact instead of being invisible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…branch list

The bump loop ran `git branch --contains "$TAG_COMMIT"` and printed whichever
local branches came back. Only the empty/non-empty distinction was ever acted
on -- the names were never read by anything -- and it was the last git call the
loop made about the tag.

Export it from commits-since-release.sh as `tag_in_local_branch`, a JSON
boolean, and have the workflow warn only when it is false. The branch names are
deliberately not collected.

A tagged commit no local branch contains is normal for a squash-merged release,
but it is also what a tag pushed from an abandoned branch looks like, so it is
worth saying out loud. The positive case no longer logs anything: "tag is in
branches: main" was noise on every crate of every release.

The field is false whenever there is no tag commit to ask about, and the
workflow only reads it inside the tag_exists branch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…umps.sh

The "Release version bumps" step was 142 lines of inline bash and the largest
untestable block in the workflow. It holds the whole release decision matrix:
defer a crate with no commits, skip one whose tag is not the latest (unless
hotfix or bypass), guard an initial release at 0.1.0, pick a level via
semver-level.sh, and build the api-changes row. None of it was reachable by a
test.

Lift it into scripts/release-version-bumps.sh, verbatim: same order, same
messages, same output. The four moves before this one are what made it small
enough to be worth doing -- the loop no longer resolves anything from git, it
reads tag_commit, range, latest_tag and tag_in_local_branch as fields, so the
script's external surface is just cargo-release and semver-level.sh.

semver-level.sh is resolved as a sibling of this script rather than through
WORKFLOW_SCRIPTS_ROOT, which guarantees the two come from the same pinned
snapshot instead of relying on the variable being right.

Verified by extracting the previous step body from git and running both against
identical fixture repositories, across three scenarios (plain, --hotfix,
--bypass-standard-checks) and all four crate paths -- normal release, skipped
because a newer tag exists, initial release, deferred with no commits. The
resulting api-changes.json and the git commits cargo-release produced are
identical in every case.

Three deliberate differences:

- set -euo pipefail. The step ran under GitHub's default `bash -e {0}`, so
  errexit was on but pipefail and nounset were not: a failing
  `jq -c '.[]' commits-by-crate.json` left the loop with no input and the step
  exited 0, releasing nothing.
- A semver-level.sh failure now prints what went wrong. The output was captured
  with 2>&1 into a variable that errexit then discarded, so the run aborted
  with nothing in the log.
- is_hotfix and bypass_standard_checks reach the shell through env: and become
  flags, instead of being interpolated into the script body as ${{ }}.

What stays in the workflow: the shallow-fetch guard and the no-changes-to-push
check, which needs git state the script does not own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Matches the Release version bumps step: the value reaches the shell as an
environment variable instead of being interpolated into the script body, which
is the pattern that keeps expression content out of the code being parsed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…or-bumps.sh

Same lift as release-version-bumps.sh, for the step that audits every release
candidate for direct libdd-* dependencies that went to a new major and promotes
the crates that need it. 90 lines of inline bash become four.

Transposed verbatim: the throwaway-worktree audit, the seed, the promote loop
and the update-or-append merge all keep their order, their messages and their
output. major-bumps-level.sh is resolved as a sibling of this script rather than
through WORKFLOW_SCRIPTS_ROOT, so the two come from the same pinned snapshot.

The intermediate pre-commit audit file moves from a fixed /tmp path to mktemp
with a trap; it was never uploaded as an artifact and nothing else read it. It
is still cat'd when the audit fails, which is the only time anyone wants it.

Verified by extracting the previous step body from git and running both against
identical fixture workspaces, over all four outcomes: a crate already at major
left alone, a released crate below major promoted in place, a pending no-commit
crate pulled into the release and appended, and a crate with no libdd-*
dependency untouched. The resulting JSON, the cargo-release commits and the
worktree cleanup are identical.

Unlike the version-bumps script this one needs no test doubles at all --
major-bumps-level.sh only runs cargo metadata, with no compilation -- so the
tests for it can exercise the whole thing for real.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…hrough env

Its one use, the no-changes-to-push guard, now reads an environment variable
instead of an expression interpolated into the script body. No ${{ }} is left
inside that step's shell.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…angelogs.sh

The last large inline block in the workflow: 112 lines become two. Transposed
verbatim -- the four paths a crate can take, their order, their messages and the
commits they produce are unchanged.

Verified by extracting the previous step body from git and running both against
identical fixture repositories, covering every path in one pass:

  libdd-a  commits present            git-cliff two-pass, prepended above the
                                      existing release section
  libdd-b  no commits, dep went major minimal entry with the dependency lines,
                                      header matching git-cliff's format
  libdd-c  no commits, nothing forced no entry, no file
  libdd-d  initial release, file kept left untouched
  libdd-e  initial release, no file   minimal "Initial release." created

The resulting CHANGELOG.md files are identical once commit SHAs (which differ
per fixture instance) are normalised, and both runs produce the same three
commits.

Two small changes: mktemp honours TMPDIR instead of hardcoding /tmp, and the
compare-link base is a --remote-url option defaulting to the value that was
previously inlined, so the script can be exercised outside this repository.

What stays in the workflow: reading release_head_sha, the no-changes-to-push
guard, and the commit list handed to commit-headless through GITHUB_OUTPUT --
all workflow state the script does not own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The three scripts lifted out of release-proposal-dispatch.yml had no owner.
CODEOWNERS has no catch-all pattern and the validator runs with the `notowned`
experimental check, so unowned files fail CI.

Owned by libdatadog-core, matching the release scripts they sit beside and call
into -- commits-since-release.sh, major-bumps-level.sh, publication-order.sh and
semver-level.sh -- rather than the .github/ rule that covered them while they
were still inline in the workflow.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@dd-octo-sts

dd-octo-sts Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Artifact Size Benchmark Report

aarch64-alpine-linux-musl
Artifact Baseline Commit Change
/aarch64-alpine-linux-musl/lib/libdatadog_profiling.so 8.20 MB 8.20 MB 0% (0 B) 👌
/aarch64-alpine-linux-musl/lib/libdatadog_profiling.a 89.19 MB 89.19 MB 0% (0 B) 👌
aarch64-unknown-linux-gnu
Artifact Baseline Commit Change
/aarch64-unknown-linux-gnu/lib/libdatadog_profiling.a 100.37 MB 100.37 MB 0% (0 B) 👌
/aarch64-unknown-linux-gnu/lib/libdatadog_profiling.so 11.06 MB 11.06 MB 0% (0 B) 👌
libdatadog-x64-windows
Artifact Baseline Commit Change
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.dll 26.63 MB 26.63 MB 0% (0 B) 👌
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.lib 94.56 KB 94.56 KB 0% (0 B) 👌
/libdatadog-x64-windows/debug/dynamic/datadog_profiling_ffi.pdb 191.43 MB 191.42 MB -0% (-8.00 KB) 👌
/libdatadog-x64-windows/debug/static/datadog_profiling_ffi.lib 1001.70 MB 1001.70 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.dll 8.71 MB 8.71 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.lib 94.56 KB 94.56 KB 0% (0 B) 👌
/libdatadog-x64-windows/release/dynamic/datadog_profiling_ffi.pdb 25.62 MB 25.62 MB 0% (0 B) 👌
/libdatadog-x64-windows/release/static/datadog_profiling_ffi.lib 51.01 MB 51.01 MB 0% (0 B) 👌
libdatadog-x86-windows
Artifact Baseline Commit Change
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.dll 23.22 MB 23.22 MB 0% (0 B) 👌
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.lib 96.04 KB 96.04 KB 0% (0 B) 👌
/libdatadog-x86-windows/debug/dynamic/datadog_profiling_ffi.pdb 196.35 MB 196.34 MB -0% (-8.00 KB) 👌
/libdatadog-x86-windows/debug/static/datadog_profiling_ffi.lib 990.83 MB 990.83 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.dll 6.73 MB 6.73 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.lib 96.04 KB 96.04 KB 0% (0 B) 👌
/libdatadog-x86-windows/release/dynamic/datadog_profiling_ffi.pdb 27.55 MB 27.55 MB 0% (0 B) 👌
/libdatadog-x86-windows/release/static/datadog_profiling_ffi.lib 48.56 MB 48.56 MB 0% (0 B) 👌
x86_64-alpine-linux-musl
Artifact Baseline Commit Change
/x86_64-alpine-linux-musl/lib/libdatadog_profiling.a 79.47 MB 79.47 MB 0% (0 B) 👌
/x86_64-alpine-linux-musl/lib/libdatadog_profiling.so 9.13 MB 9.13 MB 0% (0 B) 👌
x86_64-unknown-linux-gnu
Artifact Baseline Commit Change
/x86_64-unknown-linux-gnu/lib/libdatadog_profiling.a 95.12 MB 95.12 MB 0% (0 B) 👌
/x86_64-unknown-linux-gnu/lib/libdatadog_profiling.so 11.14 MB 11.14 MB 0% (0 B) 👌

@datadog-datadog-prod-us1-2

datadog-datadog-prod-us1-2 Bot commented Aug 7, 2026

Copy link
Copy Markdown

Tests

🎉 All green!

🧪 All tests passed
❄️ No new flaky tests detected

🎯 Code Coverage (details)
Patch Coverage: 100.00%
Overall Coverage: 75.89% (-0.02%)

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 75afa6f | Docs | Datadog PR Page | Give us feedback!

@pr-commenter

pr-commenter Bot commented Aug 7, 2026

Copy link
Copy Markdown

Benchmarks

Comparison

Benchmark execution time: 2026-08-07 10:56:36

Comparing candidate commit 75afa6f in PR branch igor/versioning/proposal-wf-cleanup with baseline commit ea75b04 in branch main.

Found 4 performance improvements and 0 performance regressions! Performance is the same for 136 metrics, 0 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:msgpack_decoder::v05/high_sharing/2000

  • 🟩 execution_time [-135.628µs; -135.167µs] or [-7.989%; -7.962%]
  • 🟩 throughput [+101923.784op/s; +102273.817op/s] or [+8.652%; +8.681%]

scenario:vec_map/get_hit/16

  • 🟩 execution_time [-19.214ns; -18.091ns] or [-8.719%; -8.210%]
  • 🟩 throughput [+6536018.455op/s; +6950777.359op/s] or [+9.002%; +9.573%]

Benchmark execution time: 2026-08-07 11:07:45

Comparing candidate commit 75afa6f in PR branch igor/versioning/proposal-wf-cleanup with baseline commit ea75b04 in branch main.

Found 2 performance improvements and 5 performance regressions! Performance is the same for 173 metrics, 10 unstable metrics.

Explanation

This is an A/B test comparing a candidate commit's performance against that of a baseline commit. Performance changes are noted in the tables below as:

  • 🟩 = significantly better candidate vs. baseline
  • 🟥 = significantly worse candidate vs. baseline

We compute a confidence interval (CI) over the relative difference of means between metrics from the candidate and baseline commits, considering the baseline as the reference.

If the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD), the change is considered significant.

Feel free to reach out to #apm-benchmarking-platform on Slack if you have any questions.

More details about the CI and significant changes

You can imagine this CI as a range of values that is likely to contain the true difference of means between the candidate and baseline commits.

CIs of the difference of means are often centered around 0%, because often changes are not that big:

---------------------------------(------|---^--------)-------------------------------->
                              -0.6%    0%  0.3%     +1.2%
                                 |          |        |
         lower bound of the CI --'          |        |
sample mean (center of the CI) -------------'        |
         upper bound of the CI ----------------------'

As described above, a change is considered significant if the CI is entirely outside the configured SIGNIFICANT_IMPACT_THRESHOLD (or the deprecated UNCONFIDENCE_THRESHOLD).

For instance, for an execution time metric, this confidence interval indicates a significantly worse performance:

----------------------------------------|---------|---(---------^---------)---------->
                                       0%        1%  1.3%      2.2%      3.1%
                                                  |   |         |         |
       significant impact threshold --------------'   |         |         |
                      lower bound of CI --------------'         |         |
       sample mean (center of the CI) --------------------------'         |
                      upper bound of CI ----------------------------------'

scenario:alloc_free/system/4096

  • 🟥 execution_time [+13.485ns; +13.639ns] or [+16.886%; +17.078%]

scenario:datadog_sample_span/name_pattern_rule_not_matching/wall_time

  • 🟥 execution_time [+31.912ns; +32.252ns] or [+20.226%; +20.442%]

scenario:glob_matcher/ascii_wildcard_question_match/wall_time

  • 🟩 execution_time [-21.397ns; -21.365ns] or [-36.752%; -36.698%]

scenario:glob_matcher/ascii_wildcard_star_match/wall_time

  • 🟩 execution_time [-21.980ns; -21.950ns] or [-37.745%; -37.693%]

scenario:profiler_attached/fast_path_system/4096

  • 🟥 execution_time [+6.641ns; +6.781ns] or [+6.749%; +6.891%]

scenario:trace_buffer/4_senders/no_delay

  • 🟥 execution_time [+108.867µs; +137.935µs] or [+4.618%; +5.851%]
  • 🟥 throughput [-85925.543op/s; -67570.036op/s] or [-5.620%; -4.419%]

Candidate

Omitted due to size.

Baseline

Omitted due to size.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant