From e84c5e9e146bd5bd25ca2faf594751bf45a20850 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 21:22:01 +0200 Subject: [PATCH 1/2] feat: pin library defaults for v2 --- .github/workflows/release.yaml | 2 + README.md | 66 ++++- RELEASE.md | 91 ++++++ scripts/bump-library-versions.sh | 107 ++++++++ scripts/create-library-version-bump-pr.sh | 307 +++++++++++++++++++++ scripts/release-orb.sh | 321 ++++++++++++++++++++++ src/commands/autoinstrument.yml | 28 +- src/examples/example.yml | 2 +- 8 files changed, 901 insertions(+), 23 deletions(-) create mode 100644 RELEASE.md create mode 100755 scripts/bump-library-versions.sh create mode 100755 scripts/create-library-version-bump-pr.sh create mode 100755 scripts/release-orb.sh diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index e8aa616..aa5830c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -17,5 +17,7 @@ jobs: uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 - name: Release uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2.6.1 + with: + generate_release_notes: true env: GITHUB_TOKEN: ${{ secrets.RELEASE_ACCESS_TOKEN }} diff --git a/README.md b/README.md index 55bdcad..eb3ebdd 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ It can help you investigate and mitigate performance problems and test failures version: 2.1 orbs: - test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@1 + test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@2 jobs: test: @@ -33,6 +33,34 @@ jobs: - run: pytest ``` +### Choose an orb version + +Starting with v2, each orb release pins its default library versions instead of resolving the latest versions during customer runs. [CircleCI registry orbs use semantic version references](https://circleci.com/docs/orbs/use/orb-concepts/#semantic-versioning-registry-orbs), so the value after `@` determines whether a workflow receives newer orb releases and their reviewed library snapshots: + +| Reference | Example | Behavior | Trade-off | +| --------- | ------- | -------- | --------- | +| Moving major | `datadog/test-optimization-circleci-orb@2` | Receives reviewed compatible v2 releases, including library bumps. It does not move to v3. | The orb behavior and default libraries can change when a new v2 release is published. | +| Exact release | `datadog/test-optimization-circleci-orb@2.0.0` | Keeps the selected immutable production orb code and default library snapshot stable. | Requires an explicit update to receive fixes and library bumps. | + +These references select the orb release and its default library snapshot. To hold one library at a specific version independently of the orb reference, [set its version parameter explicitly](#pin-an-individual-library-version). + +For most workflows, `@2` is the simplest way to receive compatible, reviewed updates. Use `@2.0.0` when the orb and its defaults must remain fixed until the configuration is updated explicitly. + +### Migrate from v1 to v2 + +The `@1` reference does not move to v2 automatically. Update the orb declaration in each CircleCI configuration you want to migrate: + +```diff +- test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@1 ++ test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@2 +``` + +No parameter names or required configuration change. Omitted library-version parameters use the pinned v2 snapshot listed in the [configuration table](#configuration), so the first v2 run may use different libraries from the last v1 run. Those defaults no longer change independently on every run. + +Explicit library-version parameters remain authoritative. With `@2`, reviewed v2 releases can update the orb and any omitted library defaults; with `@2.0.0`, the orb and its default snapshot remain fixed until the reference is updated explicitly. + +Before migrating, review the v2 defaults and run a representative workflow. Set an explicit version parameter first if a workflow must keep using a validated library version. + ## Configuration The orb has the following parameters: @@ -42,15 +70,37 @@ The orb has the following parameters: | languages | List of languages to be instrumented. Can be either "all" or any of "java", "js", "python", "dotnet", "ruby" (multiple languages can be specified as a space-separated list). | true | | | site | Datadog site. See https://docs.datadoghq.com/getting_started/site for more information about sites. | false | datadoghq.com | | service | The name of the service or library being tested. | false | | -| dotnet_tracer_version | The version of Datadog .NET tracer to use. Defaults to the latest release. | false | | -| java_tracer_version | The version of Datadog Java tracer to use. Defaults to the latest release. | false | | -| js_tracer_version | The version of Datadog JS tracer to use. Defaults to the latest release. | false | | -| python_tracer_version | The version of Datadog Python tracer to use. Defaults to the latest release. | false | | -| ruby_tracer_version | The version of datadog-ci gem to use. Defaults to the latest release. | false | | -| go_tracer_version | The version of Orchestrion automatic compile-time instrumentation of Go code (https://github.com/datadog/orchestrion) to use. Defaults to the latest release. | false | | -| python_coverage_version | The version of the Python `coverage` package to use. Defaults to `7.13.5`. | false | | +| dotnet_tracer_version | The version of Datadog .NET tracer to use. | false | 3.49.0 | +| java_tracer_version | The version of Datadog Java tracer to use. | false | 1.64.0 | +| js_tracer_version | The version of Datadog JS tracer to use. | false | 6.3.0 | +| python_tracer_version | The version of Datadog Python tracer to use. | false | 4.11.0 | +| ruby_tracer_version | The version of datadog-ci gem to use. | false | 1.34.0 | +| go_tracer_version | The version of Orchestrion automatic compile-time instrumentation of Go code (https://github.com/datadog/orchestrion) to use. | false | v1.11.0 | +| python_coverage_version | The version of the Python `coverage` package to use. | false | 7.15.1 | | java_instrumented_build_system | If provided, only the specified build systems will be instrumented (allowed values are `gradle`,`maven`,`sbt`,`ant`,`all`). `all` is a special value that instruments every Java process. If this property is not provided, all known build systems will be instrumented (Gradle, Maven, SBT, Ant). | false | | +### Pin an individual library version + +Set a library's version parameter to override the default selected by the orb release. The explicit version remains fixed when a moving major reference advances: + +```yaml +orbs: + test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@2 + +jobs: + test: + docker: + - image: eclipse-temurin:17 + steps: + - test-optimization-circleci-orb/autoinstrument: + languages: java + java_tracer_version: 1.64.0 +``` + +This allows orb updates while holding a library at a version you have validated. The explicit parameter overrides the orb release default, remains fixed when `@2` advances, and must be updated manually to receive future fixes for that library. The same behavior applies to every library-version parameter in the table above. + +Maintainers can use the local scripts described in [RELEASE.md](RELEASE.md) to create a batched library bump PR and publish the resulting orb release. + ### Additional configuration Any [additional configuration values](https://docs.datadoghq.com/tracing/trace_collection/library_config/) can be added directly to the job that runs your tests: diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..bdc4879 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,91 @@ +# Release Process + +This repository publishes immutable semantic versions of a CircleCI registry orb, such as `2.0.0`. Git tags use the corresponding `v2.0.0` form and trigger the existing Orb Tools production publishing workflow. + +## Requirements + +The local maintenance scripts require `curl`, `git`, `gh`, and Ruby. Authenticate the GitHub CLI before using them: + +```bash +gh auth login +gh auth status +``` + +Scripts that create commits use signed commits and verify the signature before pushing. Run the bump PR and release helpers from a clean working tree. + +## Release labels + +Every PR intended for a release must have one of these labels: + +- `semver-patch`: requests the next patch release +- `semver-minor`: requests the next minor release +- `semver-major`: requests the next major release + +If a release includes multiple merged PRs, the highest requested version change wins. An explicit tag can also be supplied to the release script. + +## Bump pinned library versions + +Use `scripts/create-library-version-bump-pr.sh` for the normal maintainer workflow. It discovers available updates and handles the branch, signed commit, push, labels, and PR. + +Preview all currently available library updates: + +```bash +scripts/create-library-version-bump-pr.sh --dry-run +``` + +Create one PR containing every available update: + +```bash +scripts/create-library-version-bump-pr.sh +``` + +The script reads the official package sources for .NET, Java, JavaScript, Python, Python coverage, Ruby, and Go/Orchestrion. If at least one pinned default is outdated, it updates `src/commands/autoinstrument.yml` and `README.md`, creates and verifies a signed commit, pushes the branch, and opens a PR. The PR lists exactly which languages changed so release notes remain relevant to users of those languages. + +The PR receives: + +- `library-version-bump` +- `semver-patch` when every update is a patch +- `semver-minor` when at least one library changes its major or minor version + +Use `scripts/bump-library-versions.sh` only as the lower-level manual tool for targeted changes or testing. It accepts explicit versions and updates the orb source and README without querying package sources, creating a branch or commit, pushing, or opening a PR. + +```bash +scripts/bump-library-versions.sh --java 1.65.0 --js 6.3.1 +scripts/bump-library-versions.sh --go v1.12.0 +``` + +Review and merge the resulting changes normally. + +## Release the orb + +Preview the next release first: + +```bash +scripts/release-orb.sh --dry-run +``` + +The script fetches `main` and release tags, finds merged PRs since the latest immutable release, reads their `semver-patch`, `semver-minor`, and `semver-major` labels, and selects the highest release level. It verifies the release commit's signature and creates only the immutable `vX.Y.Z` tag. It never creates or moves a `v2` Git branch; CircleCI resolves `@2` through the orb registry after the tag-triggered Orb Tools workflow publishes the production version. + +Publish the inferred release: + +```bash +scripts/release-orb.sh +``` + +Release a specific commit on `main`: + +```bash +scripts/release-orb.sh --sha abc1234 --dry-run +scripts/release-orb.sh --sha abc1234 +``` + +Choose the tag explicitly: + +```bash +scripts/release-orb.sh --tag v2.0.0 --dry-run +scripts/release-orb.sh --tag v2.0.0 +``` + +If the requested tag is lower than the merged PR labels imply, the script fails. To publish that tag intentionally, pass `--allow-version-mismatch` with `--tag`. + +After the tag is pushed, the CircleCI workflow tests and publishes the production orb version. The GitHub release workflow creates release notes from the merged PRs, including the affected-language details from library bump PRs. diff --git a/scripts/bump-library-versions.sh b/scripts/bump-library-versions.sh new file mode 100755 index 0000000..a5698f7 --- /dev/null +++ b/scripts/bump-library-versions.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/bump-library-versions.sh [OPTIONS] + +Updates one or more pinned library versions in +src/commands/autoinstrument.yml and README.md. + +Options: + --dotnet VERSION Datadog .NET tracer version + --java VERSION Datadog Java tracer version + --js VERSION Datadog JavaScript tracer version + --python VERSION Datadog Python tracer version + --coverage VERSION Python coverage version + --ruby VERSION datadog-ci Ruby gem version + --go VERSION Orchestrion version, including the leading v + -h, --help Show this help +EOF +} + +updates=() +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --dotnet|--java|--js|--python|--coverage|--ruby|--go) + if [[ -z "${2:-}" ]]; then + usage >&2 + exit 1 + fi + updates+=("${1#--}=$2") + shift 2 + ;; + *) + usage >&2 + exit 1 + ;; + esac +done + +if [[ ${#updates[@]} -eq 0 ]]; then + usage >&2 + exit 1 +fi + +if ! command -v ruby >/dev/null 2>&1; then + echo "Missing required command: ruby" >&2 + exit 1 +fi + +ruby - "${updates[@]}" <<'RUBY' +parameter_names = { + 'dotnet' => 'dotnet_tracer_version', + 'java' => 'java_tracer_version', + 'js' => 'js_tracer_version', + 'python' => 'python_tracer_version', + 'coverage' => 'python_coverage_version', + 'ruby' => 'ruby_tracer_version', + 'go' => 'go_tracer_version' +}.freeze + +parsed_updates = ARGV.map do |argument| + name, version = argument.split('=', 2) + abort "Unknown library '#{name}'" unless parameter_names.key?(name) + abort "Missing version for '#{name}'" if version.nil? || version.empty? + + expected_pattern = name == 'go' ? /^v\d+\.\d+\.\d+$/ : /^\d+\.\d+\.\d+$/ + abort "Expected an exact #{name} version, got '#{version}'" unless version.match?(expected_pattern) + + [name, version] +end +updates = parsed_updates.to_h + +abort 'Each library may only be specified once' unless updates.length == parsed_updates.length + +orb_path = 'src/commands/autoinstrument.yml' +readme_path = 'README.md' +orb = File.read(orb_path) +readme = File.read(readme_path) + +updates.each do |name, version| + parameter_name = parameter_names.fetch(name) + orb_pattern = /(^ #{Regexp.escape(parameter_name)}:\n(?:(?!^ [A-Za-z0-9_]+:).*\n)*?^ default: )'[^']+'/ + abort "Unable to find #{parameter_name} default in #{orb_path}" unless orb.scan(orb_pattern).length == 1 + orb.sub!(orb_pattern) { "#{Regexp.last_match(1)}'#{version}'" } + + readme_pattern = /^(\| #{Regexp.escape(parameter_name)}\s+\|[^|]*\|[^|]*\|)([^|]*)(\|)$/ + match = readme.match(readme_pattern) + abort "Unable to find #{parameter_name} default in #{readme_path}" unless match + + default_cell = " #{version} " + abort "Version '#{version}' does not fit in the README default column" if default_cell.length > match[2].length + + readme.sub!(readme_pattern) do + "#{Regexp.last_match(1)}#{default_cell.ljust(Regexp.last_match(2).length)}#{Regexp.last_match(3)}" + end + + puts "Updated #{parameter_name} to #{version}" +end + +File.write(orb_path, orb) +File.write(readme_path, readme) +RUBY diff --git a/scripts/create-library-version-bump-pr.sh b/scripts/create-library-version-bump-pr.sh new file mode 100755 index 0000000..3562754 --- /dev/null +++ b/scripts/create-library-version-bump-pr.sh @@ -0,0 +1,307 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/create-library-version-bump-pr.sh [--dry-run] + +Checks the official package sources for newer supported library releases, then +opens one PR that updates every outdated pinned default. + +Options: + --dry-run Print the bump PR that would be created + -h, --help Show this help +EOF +} + +dry_run=false +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --dry-run) + if [[ "$dry_run" == "true" ]]; then + usage >&2 + exit 1 + fi + dry_run=true + shift + ;; + *) + usage >&2 + exit 1 + ;; + esac +done + +for command in curl gh git ruby; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "Missing required command: $command" >&2 + exit 1 + fi +done + +gh api user --silent >/dev/null + +base_branch="main" +bump_label="library-version-bump" +remote="origin" +repo="DataDog/test-optimization-circleci-orb" +semver_minor_label="semver-minor" +semver_patch_label="semver-patch" + +read_default() { + ruby -ryaml -e 'puts YAML.load_file("src/commands/autoinstrument.yml").fetch("parameters").fetch(ARGV.fetch(0)).fetch("default")' "$1" +} + +current_dotnet=$(read_default dotnet_tracer_version) +current_java=$(read_default java_tracer_version) +current_js=$(read_default js_tracer_version) +current_python=$(read_default python_tracer_version) +current_coverage=$(read_default python_coverage_version) +current_ruby=$(read_default ruby_tracer_version) +current_go=$(read_default go_tracer_version) + +latest_dotnet=$( + curl -fsSL https://api.nuget.org/v3-flatcontainer/dd-trace/index.json | + ruby -rjson -e 'versions = JSON.parse(STDIN.read).fetch("versions").grep(/^\d+\.\d+\.\d+$/); puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_java=$( + curl -fsSL https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/maven-metadata.xml | + ruby -rrexml/document -e 'document = REXML::Document.new(STDIN.read); versions = REXML::XPath.match(document, "/metadata/versioning/versions/version").map(&:text).grep(/^\d+\.\d+\.\d+$/); puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_js=$( + curl -fsSL https://registry.npmjs.org/dd-trace | + ruby -rjson -e 'versions = JSON.parse(STDIN.read).fetch("versions").keys.grep(/^\d+\.\d+\.\d+$/); puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_python=$( + curl -fsSL https://pypi.org/pypi/ddtrace/json | + ruby -rjson -e 'data = JSON.parse(STDIN.read); versions = data.fetch("releases").keys.grep(/^\d+\.\d+\.\d+$/).select { |version| data.fetch("releases").fetch(version).any? { |file| !file.fetch("yanked", false) } }; puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_coverage=$( + curl -fsSL https://pypi.org/pypi/coverage/json | + ruby -rjson -e 'data = JSON.parse(STDIN.read); versions = data.fetch("releases").keys.grep(/^\d+\.\d+\.\d+$/).select { |version| data.fetch("releases").fetch(version).any? { |file| !file.fetch("yanked", false) } }; puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_ruby=$( + curl -fsSL https://rubygems.org/api/v1/versions/datadog-ci.json | + ruby -rjson -e 'versions = JSON.parse(STDIN.read).reject { |item| item.fetch("prerelease", false) || item.fetch("yanked", false) }.map { |item| item.fetch("number") }.grep(/^\d+\.\d+\.\d+$/); puts versions.max_by { |version| version.split(".").map(&:to_i) }' +) +latest_go=$(gh api repos/DataDog/orchestrion/releases/latest --jq '.tag_name') + +is_exact_version() { + local name="$1" + local version="$2" + + if [[ "$name" == "go" ]]; then + [[ "$version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] + else + [[ "$version" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] + fi +} + +semver_parts() { + local version="${1#v}" + IFS=. read -r semver_major semver_minor semver_patch <<< "$version" + echo "$semver_major $semver_minor $semver_patch" +} + +version_is_newer() { + local current_major current_minor current_patch next_major next_minor next_patch + read -r current_major current_minor current_patch <<< "$(semver_parts "$1")" + read -r next_major next_minor next_patch <<< "$(semver_parts "$2")" + + (( next_major > current_major || + (next_major == current_major && next_minor > current_minor) || + (next_major == current_major && next_minor == current_minor && next_patch > current_patch) )) +} + +change_requires_minor_release() { + local current_major current_minor _current_patch next_major next_minor _next_patch + read -r current_major current_minor _current_patch <<< "$(semver_parts "$1")" + read -r next_major next_minor _next_patch <<< "$(semver_parts "$2")" + + (( next_major > current_major || (next_major == current_major && next_minor > current_minor) )) +} + +changes=() +bump_args=() +release_bump_kind="patch" + +record_change() { + local name="$1" + local display_name="$2" + local current="$3" + local latest="$4" + + if ! is_exact_version "$name" "$current" || ! is_exact_version "$name" "$latest"; then + echo "Expected exact semantic versions for $display_name, got '$current' and '$latest'" >&2 + exit 1 + fi + + if [[ "$current" == "$latest" ]]; then + return 0 + fi + + if ! version_is_newer "$current" "$latest"; then + echo "Refusing to downgrade $display_name from $current to $latest" >&2 + exit 1 + fi + + changes+=("$display_name: $current -> $latest") + bump_args+=("--$name" "$latest") + if change_requires_minor_release "$current" "$latest"; then + release_bump_kind="minor" + fi +} + +record_change dotnet ".NET" "$current_dotnet" "$latest_dotnet" +record_change java "Java" "$current_java" "$latest_java" +record_change js "JavaScript" "$current_js" "$latest_js" +record_change python "Python" "$current_python" "$latest_python" +record_change coverage "Python coverage" "$current_coverage" "$latest_coverage" +record_change ruby "Ruby" "$current_ruby" "$latest_ruby" +record_change go "Go/Orchestrion" "$current_go" "$latest_go" + +if [[ ${#changes[@]} -eq 0 ]]; then + echo "All pinned library versions are current." + exit 0 +fi + +if [[ "$release_bump_kind" == "minor" ]]; then + release_label="$semver_minor_label" +else + release_label="$semver_patch_label" +fi + +github_user=$(gh api user --jq '.login') +branch_name="$github_user/library-version-bump-$(date -u +%Y%m%d-%H%M%S)" + +echo "Pinned library updates:" +printf ' - %s\n' "${changes[@]}" +echo "Release bump kind: $release_bump_kind" +echo "Branch to create: $branch_name" +echo "Labels to apply: $bump_label $release_label" + +if [[ "$dry_run" == "true" ]]; then + echo "Dry run only. No branch, commit, labels, push, or PR were created." + exit 0 +fi + +if [[ -n "$(git status --porcelain)" ]]; then + echo "Working tree must be clean before creating a bump PR." >&2 + exit 1 +fi + +existing_pr_url=$(gh pr list \ + --repo "$repo" \ + --head "$branch_name" \ + --state open \ + --json url \ + --jq '.[0].url // ""') +if [[ -n "$existing_pr_url" ]]; then + echo "An open bump PR already exists: $existing_pr_url" + exit 0 +fi + +if git show-ref --verify --quiet "refs/heads/$branch_name"; then + echo "Local branch '$branch_name' already exists. Delete it before rerunning the script." >&2 + exit 1 +fi + +git fetch "$remote" "$base_branch" +git checkout -b "$branch_name" FETCH_HEAD + +scripts/bump-library-versions.sh "${bump_args[@]}" + +git add src/commands/autoinstrument.yml README.md +git commit -S -m "chore: bump pinned library versions" +git verify-commit HEAD +git push -u "$remote" "$branch_name" + +gh label create "$bump_label" \ + --repo "$repo" \ + --description "Marks PRs that bump pinned library versions" \ + --color "1D76DB" \ + --force + +gh label create "$release_label" \ + --repo "$repo" \ + --description "Requests a $release_bump_kind test-optimization-circleci-orb release after merge" \ + --color "0E8A16" \ + --force + +changes_markdown=$(printf -- '- %s\n' "${changes[@]}") +body_file=$(mktemp) +trap 'rm -f "$body_file"' EXIT +cat > "$body_file" < +### What does this PR do? + + + +Updates the pinned default library versions: + +$changes_markdown + +### Motivation + + + +Keep the orb's tested library snapshot current without resolving new defaults during customer workflow runs. + +### Additional Notes + + + +The versions were read from their official package sources by \`scripts/create-library-version-bump-pr.sh\`. + +### Possible Drawbacks / Trade-offs + + + +Only the listed languages receive new defaults in this release. + +### Describe how to test/QA your changes + + + +Run the existing CircleCI test matrix and verify that \`src/commands/autoinstrument.yml\` and \`README.md\` list the same defaults. +EOF + +gh pr create \ + --repo "$repo" \ + --base "$base_branch" \ + --head "$branch_name" \ + --title "chore: bump pinned library versions" \ + --body-file "$body_file" \ + --label "$bump_label" \ + --label "$release_label" diff --git a/scripts/release-orb.sh b/scripts/release-orb.sh new file mode 100755 index 0000000..5dac695 --- /dev/null +++ b/scripts/release-orb.sh @@ -0,0 +1,321 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/release-orb.sh [--tag vX.Y.Z] [--sha COMMIT] [--dry-run] [--allow-version-mismatch] + +Creates an orb release from a signed commit on main. By default, the script +derives the next immutable tag from merged PR labels since the previous release. +The existing CircleCI workflow publishes the production orb from that tag. + +Options: + --tag TAG Immutable release tag to create, e.g. v2.0.0 + --sha COMMIT Commit to release; defaults to origin/main after fetch + --dry-run Print the release without creating or pushing a tag + --allow-version-mismatch + Allow --tag to be lower than the release labels imply + -h, --help Show this help +EOF +} + +dry_run=false +requested_tag="" +requested_sha="" +allow_version_mismatch=false +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --tag) + if [[ -n "$requested_tag" || -z "${2:-}" ]]; then + usage >&2 + exit 1 + fi + requested_tag="$2" + shift 2 + ;; + --sha) + if [[ -n "$requested_sha" || -z "${2:-}" ]]; then + usage >&2 + exit 1 + fi + requested_sha="$2" + shift 2 + ;; + --dry-run) + if [[ "$dry_run" == "true" ]]; then + usage >&2 + exit 1 + fi + dry_run=true + shift + ;; + --allow-version-mismatch) + if [[ "$allow_version_mismatch" == "true" ]]; then + usage >&2 + exit 1 + fi + allow_version_mismatch=true + shift + ;; + *) + usage >&2 + exit 1 + ;; + esac +done + +if [[ -n "$requested_tag" && ! "$requested_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Expected --tag to be an immutable orb tag like v2.0.0, got '$requested_tag'" >&2 + exit 1 +fi + +if [[ "$allow_version_mismatch" == "true" && -z "$requested_tag" ]]; then + echo "--allow-version-mismatch only applies when --tag is set." >&2 + exit 1 +fi + +for command in gh git; do + if ! command -v "$command" >/dev/null 2>&1; then + echo "Missing required command: $command" >&2 + exit 1 + fi +done + +if [[ -n "$(git status --porcelain)" ]]; then + echo "Working tree must be clean before creating a release." >&2 + exit 1 +fi + +gh api user --silent >/dev/null + +base_branch="main" +remote="origin" +repo="DataDog/test-optimization-circleci-orb" +pr_limit=200 +major_label="semver-major" +minor_label="semver-minor" +patch_label="semver-patch" + +git fetch --tags "$remote" +git fetch "$remote" "$base_branch" +base_ref="refs/remotes/$remote/$base_branch" + +latest_tag="" +while IFS= read -r tag; do + if [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + latest_tag="$tag" + break + fi +done < <(git tag --list 'v*' --sort=-v:refname) +if [[ -z "$latest_tag" ]]; then + echo "No existing immutable orb release tag found." >&2 + exit 1 +fi + +if ! git merge-base --is-ancestor "$latest_tag" "$base_ref"; then + echo "Latest immutable orb tag $latest_tag is not in $remote/$base_branch history." >&2 + exit 1 +fi + +target_sha="${requested_sha:-$base_ref}" +target_sha=$(git rev-parse "$target_sha") + +if ! git cat-file -e "${target_sha}^{commit}" 2>/dev/null; then + echo "Commit '$target_sha' was not found locally." >&2 + exit 1 +fi + +if ! git merge-base --is-ancestor "$target_sha" "$base_ref"; then + echo "Commit '$target_sha' is not in $remote/$base_branch history." >&2 + exit 1 +fi + +if git merge-base --is-ancestor "$target_sha" "$latest_tag"; then + echo "No unreleased commits found after $latest_tag." + exit 0 +fi + +if ! git merge-base --is-ancestor "$latest_tag" "$target_sha"; then + echo "Commit '$target_sha' is not after latest orb release $latest_tag." >&2 + exit 1 +fi + +if ! git verify-commit "$target_sha"; then + echo "Release commit '$target_sha' does not have a valid signature." >&2 + exit 1 +fi + +semver_parts() { + local version="${1#v}" + IFS=. read -r semver_major semver_minor semver_patch <<< "$version" + echo "$semver_major $semver_minor $semver_patch" +} + +bump_rank() { + case "$1" in + patch) echo 1 ;; + minor) echo 2 ;; + major) echo 3 ;; + *) + echo "Unknown bump kind '$1'" >&2 + return 1 + ;; + esac +} + +tag_bump_kind() { + local previous="$1" + local next="$2" + local previous_major previous_minor previous_patch next_major next_minor next_patch + + read -r previous_major previous_minor previous_patch <<< "$(semver_parts "$previous")" + read -r next_major next_minor next_patch <<< "$(semver_parts "$next")" + + if (( next_major > previous_major )); then + echo "major" + elif (( next_major == previous_major && next_minor > previous_minor )); then + echo "minor" + elif (( next_major == previous_major && next_minor == previous_minor && next_patch > previous_patch )); then + echo "patch" + else + return 1 + fi +} + +next_tag_for_bump_kind() { + local bump_kind="$1" + local latest_major latest_minor latest_patch + + read -r latest_major latest_minor latest_patch <<< "$(semver_parts "$latest_tag")" + + case "$bump_kind" in + major) + echo "v$((latest_major + 1)).0.0" + ;; + minor) + echo "v${latest_major}.$((latest_minor + 1)).0" + ;; + patch) + echo "v${latest_major}.${latest_minor}.$((latest_patch + 1))" + ;; + *) + echo "Automatic releases support '$major_label', '$minor_label', and '$patch_label'." >&2 + return 1 + ;; + esac +} + +release_bump_kind="" +release_pr_numbers=() + +while IFS=$'\t' read -r pr_number merge_sha labels_csv; do + [[ -z "$pr_number" || -z "$merge_sha" ]] && continue + + if ! git cat-file -e "${merge_sha}^{commit}" 2>/dev/null; then + continue + fi + if ! git merge-base --is-ancestor "$merge_sha" "$target_sha"; then + continue + fi + if git merge-base --is-ancestor "$merge_sha" "$latest_tag"; then + continue + fi + + pr_bump_kind="" + IFS=, read -ra labels <<< "$labels_csv" + for label in "${labels[@]}"; do + case "$label" in + "$major_label") + pr_bump_kind="major" + ;; + "$minor_label") + if [[ "$pr_bump_kind" != "major" ]]; then + pr_bump_kind="minor" + fi + ;; + "$patch_label") + if [[ -z "$pr_bump_kind" ]]; then + pr_bump_kind="patch" + fi + ;; + esac + done + + if [[ -z "$pr_bump_kind" ]]; then + continue + fi + + release_pr_numbers+=("#$pr_number") + if [[ -z "$release_bump_kind" || "$(bump_rank "$pr_bump_kind")" -gt "$(bump_rank "$release_bump_kind")" ]]; then + release_bump_kind="$pr_bump_kind" + fi +done < <( + gh pr list \ + --repo "$repo" \ + --state merged \ + --base "$base_branch" \ + --limit "$pr_limit" \ + --json number,mergedAt,mergeCommit,labels \ + --jq 'sort_by(.mergedAt) | .[] | [.number, (.mergeCommit.oid // ""), ([.labels[].name] | join(","))] | @tsv' +) + +if [[ -z "$release_bump_kind" && -z "$requested_tag" ]]; then + echo "No unreleased merged PRs with '$major_label', '$minor_label', or '$patch_label' found between $latest_tag and $target_sha." + exit 0 +fi + +if [[ -z "$requested_tag" ]]; then + next_tag=$(next_tag_for_bump_kind "$release_bump_kind") +else + if ! requested_bump_kind=$(tag_bump_kind "$latest_tag" "$requested_tag"); then + echo "Requested tag '$requested_tag' must be newer than latest immutable orb release '$latest_tag'." >&2 + exit 1 + fi + + if [[ -n "$release_bump_kind" && "$(bump_rank "$requested_bump_kind")" -lt "$(bump_rank "$release_bump_kind")" ]]; then + mismatch_message="Requested tag '$requested_tag' is a $requested_bump_kind release, but merged PR labels require a $release_bump_kind release." + if [[ "$allow_version_mismatch" != "true" ]]; then + echo "$mismatch_message" >&2 + echo "Rerun with --allow-version-mismatch to publish this tag anyway." >&2 + exit 1 + fi + echo "Warning: $mismatch_message" >&2 + fi + + next_tag="$requested_tag" +fi + +if git rev-parse --verify --quiet "refs/tags/$next_tag" >/dev/null; then + echo "Tag '$next_tag' already exists locally." >&2 + exit 1 +fi + +if gh release view "$next_tag" --repo "$repo" >/dev/null 2>&1; then + echo "GitHub Release '$next_tag' already exists." >&2 + exit 1 +fi + +echo "Latest orb release tag: $latest_tag" +echo "Next orb release tag: $next_tag" +if [[ -n "$release_bump_kind" ]]; then + echo "Inferred release bump kind: $release_bump_kind" + echo "Release PRs: ${release_pr_numbers[*]}" +fi +if [[ -n "$requested_tag" ]]; then + echo "Requested orb release tag: $requested_tag" +fi +echo "Release commit: $target_sha" + +if [[ "$dry_run" == "true" ]]; then + echo "Dry run only. No tag was created or pushed." + exit 0 +fi + +git tag -a "$next_tag" "$target_sha" -m "Release $next_tag" +git push "$remote" "refs/tags/$next_tag" + +echo "Pushed $next_tag. The existing CircleCI workflow will publish the production orb." diff --git a/src/commands/autoinstrument.yml b/src/commands/autoinstrument.yml index f34cff0..5305fe3 100755 --- a/src/commands/autoinstrument.yml +++ b/src/commands/autoinstrument.yml @@ -13,33 +13,33 @@ parameters: type: string default: datadoghq.com dotnet_tracer_version: - description: 'The version of Datadog .NET tracer to use (optional). Defaults to the latest release.' + description: 'The version of Datadog .NET tracer to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '3.49.0' java_tracer_version: - description: 'The version of Datadog Java tracer to use (optional). Defaults to the latest release.' + description: 'The version of Datadog Java tracer to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '1.64.0' js_tracer_version: - description: 'The version of Datadog JS tracer to use (optional). Defaults to the latest release.' + description: 'The version of Datadog JS tracer to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '6.3.0' python_tracer_version: - description: 'The version of Datadog Python tracer to use (optional). Defaults to the latest release.' + description: 'The version of Datadog Python tracer to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '4.11.0' ruby_tracer_version: - description: 'The version of datadog-ci gem to use (optional). Defaults to the latest release.' + description: 'The version of datadog-ci gem to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '1.34.0' go_tracer_version: - description: 'The version of Orchestrion automatic compile-time instrumentation of Go code (https://github.com/datadog/orchestrion) to use (optional). Defaults to the latest release.' + description: 'The version of Orchestrion automatic compile-time instrumentation of Go code (https://github.com/datadog/orchestrion) to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: 'v1.11.0' python_coverage_version: - description: 'The version of the Python coverage package to use (optional). Defaults to 7.13.5.' + description: 'The version of the Python coverage package to use (optional). Defaults to the version pinned by this orb release.' type: string - default: '' + default: '7.15.1' java_instrumented_build_system: description: 'If provided, only the specified build systems will be instrumented (allowed values are `gradle` and `maven`). Otherwise every Java process will be instrumented.' type: string diff --git a/src/examples/example.yml b/src/examples/example.yml index 5f626c1..fc52dea 100644 --- a/src/examples/example.yml +++ b/src/examples/example.yml @@ -3,7 +3,7 @@ description: > usage: version: 2.1 orbs: - test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@1 + test-optimization-circleci-orb: datadog/test-optimization-circleci-orb@2 jobs: test: docker: From 167fe35ed46113101bfcd28a1b77dbb81c230fb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 23:22:52 +0200 Subject: [PATCH 2/2] fix: validate release PR labels --- scripts/release-orb.sh | 55 +++++++++++++++++++++--------------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/scripts/release-orb.sh b/scripts/release-orb.sh index 5dac695..504bb94 100755 --- a/scripts/release-orb.sh +++ b/scripts/release-orb.sh @@ -209,8 +209,22 @@ next_tag_for_bump_kind() { esac } +if ! merged_pr_rows=$( + gh pr list \ + --repo "$repo" \ + --state merged \ + --base "$base_branch" \ + --limit "$pr_limit" \ + --json number,mergedAt,mergeCommit,labels \ + --jq 'sort_by(.mergedAt) | .[] | [.number, (.mergeCommit.oid // ""), ([.labels[].name] | join(","))] | @tsv' +); then + echo "Failed to query merged PR labels for $repo." >&2 + exit 1 +fi + release_bump_kind="" release_pr_numbers=() +unlabeled_release_pr_numbers=() while IFS=$'\t' read -r pr_number merge_sha labels_csv; do [[ -z "$pr_number" || -z "$merge_sha" ]] && continue @@ -226,26 +240,14 @@ while IFS=$'\t' read -r pr_number merge_sha labels_csv; do fi pr_bump_kind="" - IFS=, read -ra labels <<< "$labels_csv" - for label in "${labels[@]}"; do - case "$label" in - "$major_label") - pr_bump_kind="major" - ;; - "$minor_label") - if [[ "$pr_bump_kind" != "major" ]]; then - pr_bump_kind="minor" - fi - ;; - "$patch_label") - if [[ -z "$pr_bump_kind" ]]; then - pr_bump_kind="patch" - fi - ;; - esac - done + case ",$labels_csv," in + *",$major_label,"*) pr_bump_kind="major" ;; + *",$minor_label,"*) pr_bump_kind="minor" ;; + *",$patch_label,"*) pr_bump_kind="patch" ;; + esac if [[ -z "$pr_bump_kind" ]]; then + unlabeled_release_pr_numbers+=("#$pr_number") continue fi @@ -253,15 +255,14 @@ while IFS=$'\t' read -r pr_number merge_sha labels_csv; do if [[ -z "$release_bump_kind" || "$(bump_rank "$pr_bump_kind")" -gt "$(bump_rank "$release_bump_kind")" ]]; then release_bump_kind="$pr_bump_kind" fi -done < <( - gh pr list \ - --repo "$repo" \ - --state merged \ - --base "$base_branch" \ - --limit "$pr_limit" \ - --json number,mergedAt,mergeCommit,labels \ - --jq 'sort_by(.mergedAt) | .[] | [.number, (.mergeCommit.oid // ""), ([.labels[].name] | join(","))] | @tsv' -) +done <<< "$merged_pr_rows" + +if [[ ${#unlabeled_release_pr_numbers[@]} -gt 0 ]]; then + echo "Merged PRs included in the release are missing a semver label:" >&2 + printf ' - %s\n' "${unlabeled_release_pr_numbers[@]}" >&2 + echo "Apply '$major_label', '$minor_label', or '$patch_label' before releasing." >&2 + exit 1 +fi if [[ -z "$release_bump_kind" && -z "$requested_tag" ]]; then echo "No unreleased merged PRs with '$major_label', '$minor_label', or '$patch_label' found between $latest_tag and $target_sha."