From a534fe0c52fbc44bf13e0b09c444e8ebdc3c3238 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 16:22:35 +0200 Subject: [PATCH 1/8] fix: pin default library versions --- README.md | 20 +++++++++++++------- action.yml | 21 ++++++++++++++------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 5de018a..201ffc1 100644 --- a/README.md +++ b/README.md @@ -41,13 +41,13 @@ The action has the following parameters: | api_key | Datadog API key. Can be found at | true | | | site | Datadog site. See 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 | | -| python-coverage-version | The version of the Python `coverage` package to use. Defaults to `7.13.5`. | false | | -| ruby-tracer-version | The version of datadog-ci Ruby gem to use. Defaults to the latest release. | false | | -| go-tracer-version | The version of Orchestrion to use. Defaults to the latest release. | 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 | +| python-coverage-version | The version of the Python `coverage` package to use. | false | 7.13.5 | +| ruby-tracer-version | The version of datadog-ci Ruby gem to use. | false | 1.34.0 | +| go-tracer-version | The version of Orchestrion to use. | false | v1.11.0 | | go-module-dir | Path to the Go module root directory to instrument. Use this when the repository contains multiple Go modules or the Go module is not in the workspace root. | false | | | 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 | | | java-tracer-repository-url | Base URL of a Maven repository (or proxy/mirror) used to download the Java tracer JAR and its sha256. The path under the base must follow the standard Maven layout for `com.datadoghq:dd-java-agent`. Defaults to Maven Central. | false | | @@ -56,6 +56,12 @@ The action has the following parameters: | cache | Enable caching of downloaded tracers. | false | true | | print-github-step-summary | Print a summary of the installed tracers to the GitHub step summary. If set to false, the summary is printed to console instead. | false | true | +### Dependency updates + +A given action release pins its default library versions so that it installs the same versions on every run. Compatible library updates are reviewed and batched into action releases. Release notes identify which languages changed so users pinned to a specific action release can decide whether an update affects them. + +Set a language's version input explicitly to override the default selected by the action release. + ### Additional configuration Any [additional configuration values](https://docs.datadoghq.com/tracing/trace_collection/library_config/) can be added directly to the step that runs your tests: diff --git a/action.yml b/action.yml index 405de09..2908dc9 100644 --- a/action.yml +++ b/action.yml @@ -18,26 +18,33 @@ inputs: description: 'The name of the service or library being tested.' required: false 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 action release.' required: false + 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 action release.' required: false + 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 action release.' required: false + 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 action release.' required: false + default: '4.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 action release.' required: false + default: '7.13.5' ruby-tracer-version: - description: 'The version of datadog-ci Ruby gem to use (optional). Defaults to the latest release.' + description: 'The version of datadog-ci Ruby gem to use (optional). Defaults to the version pinned by this action release.' required: false + 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 action release.' required: false + default: 'v1.11.0' go-module-dir: description: 'Path to the Go module root directory to instrument (optional). Use this when the repository contains multiple Go modules or the Go module is not in the workspace root.' required: false From a95ec10f838c6a5f0492449ee2b32e8ba7b268ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 16:41:11 +0200 Subject: [PATCH 2/8] chore: add local dependency release tooling --- .github/workflows/release.yaml | 2 + README.md | 4 +- RELEASE.md | 90 +++++++ action.yml | 2 +- scripts/bump-library-versions.sh | 106 ++++++++ scripts/create-library-version-bump-pr.sh | 295 ++++++++++++++++++++ scripts/release-action.sh | 312 ++++++++++++++++++++++ 7 files changed, 809 insertions(+), 2 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-action.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 201ffc1..59ee1a9 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,7 @@ The action has the following parameters: | 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 | -| python-coverage-version | The version of the Python `coverage` package to use. | false | 7.13.5 | +| python-coverage-version | The version of the Python `coverage` package to use. | false | 7.15.1 | | ruby-tracer-version | The version of datadog-ci Ruby gem to use. | false | 1.34.0 | | go-tracer-version | The version of Orchestrion to use. | false | v1.11.0 | | go-module-dir | Path to the Go module root directory to instrument. Use this when the repository contains multiple Go modules or the Go module is not in the workspace root. | false | | @@ -62,6 +62,8 @@ A given action release pins its default library versions so that it installs the Set a language's version input explicitly to override the default selected by the action release. +Maintainers can use the local scripts described in [RELEASE.md](RELEASE.md) to create a batched library bump PR and publish the resulting action release. + ### Additional configuration Any [additional configuration values](https://docs.datadoghq.com/tracing/trace_collection/library_config/) can be added directly to the step that runs your tests: diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..099b018 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,90 @@ +# Release Process + +This repository ships a composite GitHub Action. Releases use immutable semantic-version tags such as `v2.11.0` and a moving major branch such as `v2`. + +## Requirements + +The local release scripts require `curl`, `git`, `gh`, and Ruby. Authenticate the GitHub CLI before using them: + +```bash +gh auth login +gh auth status +``` + +The scripts that create commits use signed commits and verify the signature before pushing. Run the bump 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 + +If a release includes multiple merged PRs, `semver-minor` wins. Major releases are explicit: pass `--tag vX.0.0` to the release script. + +## Bump pinned library versions + +Preview all currently available library updates: + +```bash +scripts/create-library-version-bump-pr.sh --dry-run +``` + +Create a single 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 creates a branch, updates `action.yml` and `README.md`, creates and verifies a signed commit, pushes the branch, and opens a PR. + +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 + +To update only selected versions without opening a PR, use the lower-level helper: + +```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 action + +Preview the next release first: + +```bash +scripts/release-action.sh --dry-run +``` + +The script fetches `main` and tags, finds merged PRs since the latest immutable action tag, reads their `semver-patch` and `semver-minor` labels, and chooses the next action tag. It verifies the release commit's signature, then atomically pushes the immutable tag and moving major branch. The existing release workflow creates the GitHub Release with generated notes. + +Publish the inferred release: + +```bash +scripts/release-action.sh +``` + +Release a specific commit on `main`: + +```bash +scripts/release-action.sh --sha abc1234 --dry-run +scripts/release-action.sh --sha abc1234 +``` + +Choose the tag manually: + +```bash +scripts/release-action.sh --tag v2.11.0 --dry-run +scripts/release-action.sh --tag v2.11.0 +``` + +If the requested tag is lower than the merged PR labels imply, the script fails. To publish that tag intentionally: + +```bash +scripts/release-action.sh --tag v2.10.1 --allow-version-mismatch +``` diff --git a/action.yml b/action.yml index 2908dc9..bbd543f 100644 --- a/action.yml +++ b/action.yml @@ -36,7 +36,7 @@ inputs: python-coverage-version: description: 'The version of the Python coverage package to use (optional). Defaults to the version pinned by this action release.' required: false - default: '7.13.5' + default: '7.15.1' ruby-tracer-version: description: 'The version of datadog-ci Ruby gem to use (optional). Defaults to the version pinned by this action release.' required: false diff --git a/scripts/bump-library-versions.sh b/scripts/bump-library-versions.sh new file mode 100755 index 0000000..6e6d536 --- /dev/null +++ b/scripts/bump-library-versions.sh @@ -0,0 +1,106 @@ +#!/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 action.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' +input_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 input_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 + +action_path = 'action.yml' +readme_path = 'README.md' +action = File.read(action_path) +readme = File.read(readme_path) + +updates.each do |name, version| + input_name = input_names.fetch(name) + action_pattern = /(^ #{Regexp.escape(input_name)}:\n(?:(?!^ [A-Za-z0-9_-]+:).*\n)*?^ default: )'[^']+'/ + abort "Unable to find #{input_name} default in #{action_path}" unless action.scan(action_pattern).length == 1 + action.sub!(action_pattern) { "#{Regexp.last_match(1)}'#{version}'" } + + readme_pattern = /^(\| #{Regexp.escape(input_name)}\s+\|[^|]*\|[^|]*\|)([^|]*)(\|)$/ + match = readme.match(readme_pattern) + abort "Unable to find #{input_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 #{input_name} to #{version}" +end + +File.write(action_path, action) +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..e6cbb6c --- /dev/null +++ b/scripts/create-library-version-bump-pr.sh @@ -0,0 +1,295 @@ +#!/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) + 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-visibility-github-action" +semver_minor_label="semver-minor" +semver_patch_label="semver-patch" + +read_default() { + ruby -ryaml -e 'puts YAML.load_file("action.yml").fetch("inputs").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"); puts versions.reject { |version| version.include?("-") }.last' +) +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); puts REXML::XPath.first(document, "/metadata/versioning/latest").text' +) +latest_js=$( + curl -fsSL https://registry.npmjs.org/dd-trace/latest | + ruby -rjson -e 'puts JSON.parse(STDIN.read).fetch("version")' +) +latest_python=$( + curl -fsSL https://pypi.org/pypi/ddtrace/json | + ruby -rjson -e 'puts JSON.parse(STDIN.read).fetch("info").fetch("version")' +) +latest_coverage=$( + curl -fsSL https://pypi.org/pypi/coverage/json | + ruby -rjson -e 'puts JSON.parse(STDIN.read).fetch("info").fetch("version")' +) +latest_ruby=$( + curl -fsSL https://rubygems.org/api/v1/gems/datadog-ci.json | + ruby -rjson -e 'puts JSON.parse(STDIN.read).fetch("version")' +) +latest_go=$(gh api repos/DataDog/orchestrion/releases/latest --jq '.tag_name') + +is_exact_version() { + [[ "$1" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]] +} + +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 "$current" || ! is_exact_version "$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 action.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-visibility-github-action 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 action'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 GitHub Actions test matrix and verify that \`action.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-action.sh b/scripts/release-action.sh new file mode 100755 index 0000000..61396f0 --- /dev/null +++ b/scripts/release-action.sh @@ -0,0 +1,312 @@ +#!/usr/bin/env bash +set -euo pipefail + +usage() { + cat <<'EOF' +Usage: scripts/release-action.sh [--tag vX.Y.Z] [--sha COMMIT] [--dry-run] [--allow-version-mismatch] + +Creates an action release from a signed commit on main. By default, the script +derives the next immutable action tag from merged PR labels since the previous +immutable tag. It atomically pushes that tag and the moving major branch; the +existing release workflow then creates the GitHub Release. + +Options: + --tag TAG Immutable action tag to create, e.g. v2.11.0 + --sha COMMIT Commit to release; defaults to origin/main after fetch + --dry-run Print the release without creating or pushing refs + --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) + dry_run=true + shift + ;; + --allow-version-mismatch) + 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 action tag like v2.11.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-visibility-github-action" +pr_limit=200 +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=$(git tag --list 'v[0-9]*.[0-9]*.[0-9]*' --sort=-v:refname | head -n 1) +if [[ -z "$latest_tag" ]]; then + echo "No existing immutable action release tag found." >&2 + exit 1 +fi + +if ! git merge-base --is-ancestor "$latest_tag" "$base_ref"; then + echo "Latest immutable action 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 action 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 + minor) + echo "v${latest_major}.$((latest_minor + 1)).0" + ;; + patch) + echo "v${latest_major}.${latest_minor}.$((latest_patch + 1))" + ;; + *) + echo "Automatic releases support '$minor_label' and '$patch_label'. Use --tag for other release types." >&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 + "$minor_label") + pr_bump_kind="minor" + ;; + "$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 '$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 action 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 + +read -r next_major _next_minor _next_patch <<< "$(semver_parts "$next_tag")" +major_branch="v${next_major}" + +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 + +if git ls-remote --exit-code --heads "$remote" "$major_branch" >/dev/null 2>&1; then + git fetch "$remote" "$major_branch" + major_branch_sha=$(git rev-parse FETCH_HEAD) + if ! git merge-base --is-ancestor "$major_branch_sha" "$target_sha"; then + echo "Moving branch '$major_branch' cannot be fast-forwarded to '$target_sha'." >&2 + exit 1 + fi +fi + +echo "Latest action release tag: $latest_tag" +echo "Next action 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 action release tag: $requested_tag" +fi +echo "Moving major branch: $major_branch" +echo "Release commit: $target_sha" + +if [[ "$dry_run" == "true" ]]; then + echo "Dry run only. No tag or branch was pushed." + exit 0 +fi + +git tag -a "$next_tag" "$target_sha" -m "Release $next_tag" +git push --atomic "$remote" "refs/tags/$next_tag" "$target_sha:refs/heads/$major_branch" + +echo "Pushed $next_tag and $major_branch. The release workflow will create the GitHub Release." From 1ac61b76b4e4dad28dc4cea788b6c05c5fff93a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 17:02:56 +0200 Subject: [PATCH 3/8] feat: prepare pinned defaults for v3 --- README.md | 35 +++++++++++++++++++++++++++++------ RELEASE.md | 13 +++++++------ scripts/release-action.sh | 19 ++++++++++++++----- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 59ee1a9..1150cb4 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ It can help you investigate and mitigate performance problems and test failures ```yaml steps: - name: Configure Datadog Test Optimization - uses: datadog/test-visibility-github-action@v2 + uses: datadog/test-visibility-github-action@v3 with: languages: java api_key: ${{ secrets.DD_API_KEY }} @@ -31,6 +31,31 @@ It can help you investigate and mitigate performance problems and test failures > Otherwise, installed tracing libraries might be removed by the steps that precede tests execution > (for example, `actions/checkout` will wipe out whatever was installed in the action workspace). +### Choose an action version + +Starting with v3, each action release pins its default library versions instead of resolving `latest` at runtime. The action reference after `@` determines whether your workflow receives newer action releases and their pinned library versions: + +| Reference | Example | Advantages | Disadvantages | +| --------- | ------- | ---------- | ------------- | +| Moving major | `datadog/test-visibility-github-action@v3` | Automatically receives reviewed bug fixes and library bumps released within v3. It will not move to v4. | The action code and default library versions can change between workflow runs. | +| Exact release tag | `datadog/test-visibility-github-action@v3.0.0` | Uses a readable, known action release and its pinned library versions. Release notes make changes easy to review before upgrading. | Does not receive bug fixes or library bumps automatically. Tags can technically be moved, so this is not as strong an immutability guarantee as a full commit SHA. | +| Full commit SHA | `datadog/test-visibility-github-action@ # v3.0.0` | Strongest reproducibility and supply-chain protection: every run uses the exact same action code and pinned library versions. | Does not receive fixes or new releases automatically. Replace the placeholder with the full SHA for the release you reviewed and use an update mechanism such as Dependabot. | + +For most workflows, `@v3` provides the simplest way to receive compatible updates. For workflows that require an immutable action, use the full commit SHA associated with a v3 release. Keeping the release tag as an inline comment lets readers identify the version and allows Dependabot to update the comment with the SHA. + +To have Dependabot propose updates for an action pinned to a SHA, add the following to `.github/dependabot.yml` in the repository that uses the action: + +```yaml +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly +``` + +Dependabot then opens pull requests when newer action releases are available, so the new SHA and any changes to the pinned library versions can be reviewed before merging. See GitHub's guidance on [keeping actions up to date with Dependabot](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/auto-update-actions) and [pinning actions securely](https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions). + ## Configuration The action has the following parameters: @@ -56,9 +81,7 @@ The action has the following parameters: | cache | Enable caching of downloaded tracers. | false | true | | print-github-step-summary | Print a summary of the installed tracers to the GitHub step summary. If set to false, the summary is printed to console instead. | false | true | -### Dependency updates - -A given action release pins its default library versions so that it installs the same versions on every run. Compatible library updates are reviewed and batched into action releases. Release notes identify which languages changed so users pinned to a specific action release can decide whether an update affects them. +### Library version defaults Set a language's version input explicitly to override the default selected by the action release. @@ -83,7 +106,7 @@ By default the Java tracer JAR is fetched from Maven Central (`https://repo1.mav ```yaml - name: Configure Datadog Test Optimization - uses: datadog/test-visibility-github-action@v2 + uses: datadog/test-visibility-github-action@v3 with: languages: java api_key: ${{ secrets.DD_API_KEY }} @@ -99,7 +122,7 @@ If your repository contains multiple Go modules, or the Go module you want to in ```yaml - name: Configure Datadog Test Optimization - uses: datadog/test-visibility-github-action@v2 + uses: datadog/test-visibility-github-action@v3 with: languages: go api_key: ${{ secrets.DD_API_KEY }} diff --git a/RELEASE.md b/RELEASE.md index 099b018..5c60987 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -1,6 +1,6 @@ # Release Process -This repository ships a composite GitHub Action. Releases use immutable semantic-version tags such as `v2.11.0` and a moving major branch such as `v2`. +This repository ships a composite GitHub Action. Releases use immutable semantic-version tags such as `v3.0.0` and a moving major branch such as `v3`. ## Requirements @@ -19,8 +19,9 @@ 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, `semver-minor` wins. Major releases are explicit: pass `--tag vX.0.0` to the release script. +If a release includes multiple merged PRs, the highest requested version change wins. A major release can also be selected explicitly by passing `--tag vX.0.0` to the release script. ## Bump pinned library versions @@ -61,7 +62,7 @@ Preview the next release first: scripts/release-action.sh --dry-run ``` -The script fetches `main` and tags, finds merged PRs since the latest immutable action tag, reads their `semver-patch` and `semver-minor` labels, and chooses the next action tag. It verifies the release commit's signature, then atomically pushes the immutable tag and moving major branch. The existing release workflow creates the GitHub Release with generated notes. +The script fetches `main` and tags, finds merged PRs since the latest immutable action tag, reads their `semver-patch`, `semver-minor`, and `semver-major` labels, and chooses the next action tag. It verifies the release commit's signature, then atomically pushes the immutable tag and moving major branch. The existing release workflow creates the GitHub Release with generated notes. Publish the inferred release: @@ -79,12 +80,12 @@ scripts/release-action.sh --sha abc1234 Choose the tag manually: ```bash -scripts/release-action.sh --tag v2.11.0 --dry-run -scripts/release-action.sh --tag v2.11.0 +scripts/release-action.sh --tag v3.0.0 --dry-run +scripts/release-action.sh --tag v3.0.0 ``` If the requested tag is lower than the merged PR labels imply, the script fails. To publish that tag intentionally: ```bash -scripts/release-action.sh --tag v2.10.1 --allow-version-mismatch +scripts/release-action.sh --tag v3.0.1 --allow-version-mismatch ``` diff --git a/scripts/release-action.sh b/scripts/release-action.sh index 61396f0..35a6b49 100755 --- a/scripts/release-action.sh +++ b/scripts/release-action.sh @@ -11,7 +11,7 @@ immutable tag. It atomically pushes that tag and the moving major branch; the existing release workflow then creates the GitHub Release. Options: - --tag TAG Immutable action tag to create, e.g. v2.11.0 + --tag TAG Immutable action tag to create, e.g. v3.0.0 --sha COMMIT Commit to release; defaults to origin/main after fetch --dry-run Print the release without creating or pushing refs --allow-version-mismatch @@ -62,7 +62,7 @@ while [[ $# -gt 0 ]]; do done if [[ -n "$requested_tag" && ! "$requested_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "Expected --tag to be an immutable action tag like v2.11.0, got '$requested_tag'" >&2 + echo "Expected --tag to be an immutable action tag like v3.0.0, got '$requested_tag'" >&2 exit 1 fi @@ -89,6 +89,7 @@ base_branch="main" remote="origin" repo="DataDog/test-visibility-github-action" pr_limit=200 +major_label="semver-major" minor_label="semver-minor" patch_label="semver-patch" @@ -179,6 +180,9 @@ next_tag_for_bump_kind() { 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" ;; @@ -186,7 +190,7 @@ next_tag_for_bump_kind() { echo "v${latest_major}.${latest_minor}.$((latest_patch + 1))" ;; *) - echo "Automatic releases support '$minor_label' and '$patch_label'. Use --tag for other release types." >&2 + echo "Automatic releases support '$major_label', '$minor_label', and '$patch_label'." >&2 return 1 ;; esac @@ -212,8 +216,13 @@ while IFS=$'\t' read -r pr_number merge_sha labels_csv; do IFS=, read -ra labels <<< "$labels_csv" for label in "${labels[@]}"; do case "$label" in + "$major_label") + pr_bump_kind="major" + ;; "$minor_label") - pr_bump_kind="minor" + if [[ "$pr_bump_kind" != "major" ]]; then + pr_bump_kind="minor" + fi ;; "$patch_label") if [[ -z "$pr_bump_kind" ]]; then @@ -242,7 +251,7 @@ done < <( ) if [[ -z "$release_bump_kind" && -z "$requested_tag" ]]; then - echo "No unreleased merged PRs with '$minor_label' or '$patch_label' found between $latest_tag and $target_sha." + echo "No unreleased merged PRs with '$major_label', '$minor_label', or '$patch_label' found between $latest_tag and $target_sha." exit 0 fi From 3de9217fefe1db86e93db73708433f9f8ac329f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 17:10:20 +0200 Subject: [PATCH 4/8] docs: clarify library bump workflows --- RELEASE.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/RELEASE.md b/RELEASE.md index 5c60987..fbfb6e2 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -25,6 +25,8 @@ If a release includes multiple merged PRs, the highest requested version change ## Bump pinned library versions +Use `scripts/create-library-version-bump-pr.sh` for the normal maintainer workflow. It discovers available updates and handles the complete branch, signed commit, push, and PR flow. + Preview all currently available library updates: ```bash @@ -45,7 +47,7 @@ The PR receives: - `semver-patch` when every update is a patch - `semver-minor` when at least one library changes its major or minor version -To update only selected versions without opening a PR, use the lower-level helper: +Use `scripts/bump-library-versions.sh` only as a lower-level manual tool when you already know the exact versions to apply, such as for a targeted update or when testing the file changes. It only updates `action.yml` and `README.md`; it does not query package sources, create a branch or commit, push, or open a PR. ```bash scripts/bump-library-versions.sh --java 1.65.0 --js 6.3.1 From e3e6e0fe37f34edd1aed8684323174f86b3f4e9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 17:16:22 +0200 Subject: [PATCH 5/8] docs: add v3 migration guide --- README.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/README.md b/README.md index 1150cb4..d18202e 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,26 @@ updates: Dependabot then opens pull requests when newer action releases are available, so the new SHA and any changes to the pinned library versions can be reviewed before merging. See GitHub's guidance on [keeping actions up to date with Dependabot](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/auto-update-actions) and [pinning actions securely](https://docs.github.com/en/actions/reference/security/secure-use#using-third-party-actions). +### Migrate from v2 to v3 + +The `v2` reference does not move to v3 automatically. Update the action reference in each workflow you want to migrate: + +```diff +- uses: datadog/test-visibility-github-action@v2 ++ uses: datadog/test-visibility-github-action@v3 +``` + +No input names or required configuration change. The migration only changes how omitted library-version inputs are resolved: + +| Your workflow | Experience after migrating | +| ------------- | -------------------------- | +| No library-version inputs | The first v3 run uses the versions listed in the [configuration table](#configuration), which may differ from the versions resolved by the last v2 run. Defaults no longer change independently on every run. | +| Explicit library-version inputs | Your selected versions continue to override the action defaults, so the installed library versions do not change as part of the migration. | + +With `@v3`, pinned defaults change only when a reviewed v3 release moves the major reference. Use an exact release tag or full commit SHA instead if the action and its defaults must remain unchanged until you explicitly update the reference. + +Before migrating, review the v3 defaults and run a representative test workflow. If you need to preserve a specific library version, set its version input explicitly. + ## Configuration The action has the following parameters: From bb4d7f67da3d9475ec4bf3f2d7c3ec92b5f668b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 17:19:42 +0200 Subject: [PATCH 6/8] docs: explain per-library version pins --- README.md | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d18202e..44443a1 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Starting with v3, each action release pins its default library versions instead | Exact release tag | `datadog/test-visibility-github-action@v3.0.0` | Uses a readable, known action release and its pinned library versions. Release notes make changes easy to review before upgrading. | Does not receive bug fixes or library bumps automatically. Tags can technically be moved, so this is not as strong an immutability guarantee as a full commit SHA. | | Full commit SHA | `datadog/test-visibility-github-action@ # v3.0.0` | Strongest reproducibility and supply-chain protection: every run uses the exact same action code and pinned library versions. | Does not receive fixes or new releases automatically. Replace the placeholder with the full SHA for the release you reviewed and use an update mechanism such as Dependabot. | +These references select the action code and its default library snapshot. To keep an individual library on a specific version independently of the action reference, [set its version input explicitly](#pin-an-individual-library-version). + For most workflows, `@v3` provides the simplest way to receive compatible updates. For workflows that require an immutable action, use the full commit SHA associated with a v3 release. Keeping the release tag as an inline comment lets readers identify the version and allows Dependabot to update the comment with the SHA. To have Dependabot propose updates for an action pinned to a SHA, add the following to `.github/dependabot.yml` in the repository that uses the action: @@ -101,9 +103,20 @@ The action has the following parameters: | cache | Enable caching of downloaded tracers. | false | true | | print-github-step-summary | Print a summary of the installed tracers to the GitHub step summary. If set to false, the summary is printed to console instead. | false | true | -### Library version defaults +### Pin an individual library version + +Set a library's version input to override the default selected by the action release. The explicit version remains in effect when the action reference moves or is updated: + +```yaml +- name: Configure Datadog Test Optimization + uses: datadog/test-visibility-github-action@v3 + with: + languages: java + api_key: ${{ secrets.DD_API_KEY }} + java-tracer-version: 1.64.0 +``` -Set a language's version input explicitly to override the default selected by the action release. +This lets you receive action updates while holding a particular library at a version you have validated. That library will not receive automatic bumps, so update the input explicitly when you want a newer version. The same behavior applies to every library-version input 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 action release. From 5b4d5761423147bccbfa235573e97d332d1c90d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Wed, 15 Jul 2026 21:44:37 +0200 Subject: [PATCH 7/8] fix: prevent duplicate library bump PRs --- scripts/create-library-version-bump-pr.sh | 24 ++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/scripts/create-library-version-bump-pr.sh b/scripts/create-library-version-bump-pr.sh index e6cbb6c..5c4e7c3 100755 --- a/scripts/create-library-version-bump-pr.sh +++ b/scripts/create-library-version-bump-pr.sh @@ -164,6 +164,19 @@ else release_label="$semver_patch_label" fi +if [[ "$dry_run" != "true" ]]; then + existing_pr_url=$(gh pr list \ + --repo "$repo" \ + --label "$bump_label" \ + --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 +fi + github_user=$(gh api user --jq '.login') branch_name="$github_user/library-version-bump-$(date -u +%Y%m%d-%H%M%S)" @@ -183,17 +196,6 @@ if [[ -n "$(git status --porcelain)" ]]; then 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 From 04225d85ec0d30fd5ea010ca0e97299041e3f239 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Juan=20Antonio=20Fern=C3=A1ndez=20de=20Alba?= Date: Thu, 16 Jul 2026 11:22:31 +0200 Subject: [PATCH 8/8] fix: verify release commits with GitHub --- RELEASE.md | 2 +- scripts/release-action.sh | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/RELEASE.md b/RELEASE.md index fbfb6e2..d5d7b53 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -64,7 +64,7 @@ Preview the next release first: scripts/release-action.sh --dry-run ``` -The script fetches `main` and tags, finds merged PRs since the latest immutable action tag, reads their `semver-patch`, `semver-minor`, and `semver-major` labels, and chooses the next action tag. It verifies the release commit's signature, then atomically pushes the immutable tag and moving major branch. The existing release workflow creates the GitHub Release with generated notes. +The script fetches `main` and tags, finds merged PRs since the latest immutable action tag, reads their `semver-patch`, `semver-minor`, and `semver-major` labels, and chooses the next action tag. It requires GitHub to report the release commit's signature as verified, then atomically pushes the immutable tag and moving major branch. The existing release workflow creates the GitHub Release with generated notes. Publish the inferred release: diff --git a/scripts/release-action.sh b/scripts/release-action.sh index 35a6b49..c630251 100755 --- a/scripts/release-action.sh +++ b/scripts/release-action.sh @@ -131,8 +131,9 @@ if ! git merge-base --is-ancestor "$latest_tag" "$target_sha"; then exit 1 fi -if ! git verify-commit "$target_sha"; then - echo "Release commit '$target_sha' does not have a valid signature." >&2 +commit_verified=$(gh api "repos/$repo/commits/$target_sha" --jq '.commit.verification.verified') +if [[ "$commit_verified" != "true" ]]; then + echo "Release commit '$target_sha' does not have a signature verified by GitHub." >&2 exit 1 fi