diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index cd834ab..541a37e 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -13,6 +13,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + - name: Check PR has a semver release label + if: github.event_name == 'pull_request' + run: | + set -euo pipefail + labels=$(ruby -rjson -e 'puts JSON.parse(File.read(ENV.fetch("GITHUB_EVENT_PATH"))).fetch("pull_request").fetch("labels").map { |label| label.fetch("name") }') + if ! grep -qx 'semver-patch' <<< "$labels" && ! grep -qx 'semver-minor' <<< "$labels"; then + echo "Expected pull request to have either the semver-patch or semver-minor label." + exit 1 + fi - name: Check that the default datadog-ci version is pinned run: | set -euo pipefail @@ -26,7 +35,7 @@ jobs: set -euo pipefail bash -n scripts/bump-datadog-ci-version.sh bash -n scripts/create-datadog-ci-bump-pr.sh - bash -n scripts/release-datadog-ci-bump.sh + bash -n scripts/release-action.sh tmpdir=$(mktemp -d) cp action.yaml README.md "$tmpdir"/ diff --git a/RELEASE.md b/RELEASE.md index 1653903..162c0a3 100644 --- a/RELEASE.md +++ b/RELEASE.md @@ -2,49 +2,91 @@ This repository ships a composite GitHub Action. Releases are Git tags: immutable semver tags such as `v3.1.0`, plus a moving major tag such as `v3`. +## Requirements + +The release scripts require the [GitHub CLI](https://cli.github.com/manual/) and an authenticated session: + +```bash +gh auth login +gh auth status +``` + +Run release scripts from a clean working tree. + +## Release labels + +Every PR 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 over `semver-patch`. Major releases are explicit: pass `--tag vX.0.0` to the release script. + ## Bump datadog-ci -Run the bump helper from a clean working tree: +This repository provides a helper for the common case of bumping the default `datadog-ci-version`. It is not required for every `junit-upload-github-action` release; any merged PR with a `semver-patch` or `semver-minor` label can be released with `scripts/release-action.sh`. + +To create a `datadog-ci-version` bump PR, run the helper from a clean working tree: ```bash scripts/create-datadog-ci-bump-pr.sh ``` -The script uses `gh` to check the latest `DataDog/datadog-ci` release. If that release is newer than the default in `action.yaml`, it creates a branch, updates `action.yaml` and `README.md`, pushes the branch, creates the `datadog-ci-version-bump` label if needed, and opens a PR with that label. +The script uses `gh` to check the latest `DataDog/datadog-ci` release. If that release is newer than the default in `action.yaml`, it creates a branch, updates `action.yaml` and `README.md`, pushes the branch, creates labels if needed, and opens a PR. + +Preview the bump PR without creating a branch, commit, labels, push, or PR: + +```bash +scripts/create-datadog-ci-bump-pr.sh --dry-run +``` + +The PR gets: + +- `datadog-ci-version-bump` +- `semver-patch` when `datadog-ci` only changed by patch +- `semver-minor` when `datadog-ci` changed by minor, or when moving from a floating default to the first pinned version To test a specific version instead of the latest release: ```bash scripts/create-datadog-ci-bump-pr.sh v5.14.0 +scripts/create-datadog-ci-bump-pr.sh v5.14.0 --dry-run ``` Review and merge the PR normally. ## Release the action -After a `datadog-ci-version-bump` PR is merged, run: +Preview the release first: ```bash -scripts/release-datadog-ci-bump.sh +scripts/release-action.sh --dry-run ``` -The script fetches `main` and tags, finds the latest merged PR with the `datadog-ci-version-bump` label that is on `main` but not included in the latest immutable action tag, and releases that merge commit. The action version follows the `datadog-ci` change: +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 releases `origin/main` by default, updates the moving major tag, and creates a GitHub Release with GitHub-generated release notes. -- floating `v5` to the first pinned `v5.x.y`: action minor bump -- `datadog-ci` minor bump: action minor bump -- `datadog-ci` patch bump: action patch bump +If the dry run looks right, publish the release: -The release creates the next immutable action tag, updates the moving major tag, and creates a GitHub Release with GitHub-generated release notes starting from the previous immutable action tag. +```bash +scripts/release-action.sh +``` + +To release a specific commit on `main`: + +```bash +scripts/release-action.sh --sha abc1234 --dry-run +scripts/release-action.sh --sha abc1234 +``` -Preview the release without creating tags or a GitHub Release: +To choose the tag manually: ```bash -scripts/release-datadog-ci-bump.sh --dry-run +scripts/release-action.sh --tag v3.2.1 --dry-run +scripts/release-action.sh --tag v3.2.1 ``` -If multiple bump PRs were merged without releases, the script warns and releases only the latest one. To intentionally release an older merge commit separately, stop and pass a specific PR number or commit SHA before releasing the latest one: +If the requested tag is lower than the merged PR labels imply, for example a patch tag while an unreleased PR has `semver-minor`, the script fails. To publish that tag intentionally: ```bash -scripts/release-datadog-ci-bump.sh --pr 123 -scripts/release-datadog-ci-bump.sh --sha abc1234 +scripts/release-action.sh --tag v3.2.1 --allow-version-mismatch ``` diff --git a/scripts/create-datadog-ci-bump-pr.sh b/scripts/create-datadog-ci-bump-pr.sh index 5746694..55bac08 100755 --- a/scripts/create-datadog-ci-bump-pr.sh +++ b/scripts/create-datadog-ci-bump-pr.sh @@ -3,7 +3,7 @@ set -euo pipefail usage() { cat <<'EOF' -Usage: scripts/create-datadog-ci-bump-pr.sh [vX.Y.Z] +Usage: scripts/create-datadog-ci-bump-pr.sh [vX.Y.Z] [--dry-run] Checks the latest DataDog/datadog-ci release with gh, then opens a PR that bumps the default datadog-ci-version when the release is newer than the current default. @@ -11,24 +11,37 @@ the default datadog-ci-version when the release is newer than the current defaul Arguments: vX.Y.Z Optional exact datadog-ci release tag to use instead of releases/latest. -Environment: - BASE_BRANCH Base branch for the PR. Defaults to main. - BRANCH_NAME Branch to create. Defaults to datadog-ci-bump/. - BUMP_LABEL Label applied to the PR. Defaults to datadog-ci-version-bump. - REMOTE Git remote to push to. Defaults to origin. - REPO GitHub repo for gh commands. Defaults to gh repo view's repo. +Options: + --dry-run Print the bump PR that would be created. EOF } -if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then - usage - exit 0 -fi - -if [[ $# -gt 1 ]]; then - usage >&2 - exit 1 -fi +dry_run=false +requested_version="" +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --dry-run) + dry_run=true + shift + ;; + v[0-9]*.[0-9]*.[0-9]*) + if [[ -n "$requested_version" ]]; then + usage >&2 + exit 1 + fi + requested_version="$1" + shift + ;; + *) + usage >&2 + exit 1 + ;; + esac +done for command in gh git ruby; do if ! command -v "$command" >/dev/null 2>&1; then @@ -37,18 +50,14 @@ for command in gh git ruby; do fi done -if [[ -n "$(git status --porcelain)" ]]; then - echo "Working tree must be clean before creating a bump PR." >&2 - exit 1 -fi - gh auth status >/dev/null -base_branch="${BASE_BRANCH:-main}" -bump_label="${BUMP_LABEL:-datadog-ci-version-bump}" -remote="${REMOTE:-origin}" -repo="${REPO:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner')}" -requested_version="${1:-}" +base_branch="main" +bump_label="datadog-ci-version-bump" +remote="origin" +repo="DataDog/junit-upload-github-action" +semver_minor_label="semver-minor" +semver_patch_label="semver-patch" current_version=$(ruby -ryaml -e 'puts YAML.load_file("action.yaml").fetch("inputs").fetch("datadog-ci-version").fetch("default")') if [[ -n "$requested_version" ]]; then @@ -62,27 +71,69 @@ if [[ ! "$latest_version" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then exit 1 fi -version_gt() { - local left="${1#v}" - local right="${2#v}" - local left_major left_minor left_patch right_major right_minor right_patch +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" +} + +bump_kind_for_datadog_ci_change() { + local current="$1" + local next="$2" + + if ! is_exact_version "$current"; then + echo "minor" + return 0 + fi + + local current_major current_minor current_patch next_major next_minor next_patch + read -r current_major current_minor current_patch <<< "$(semver_parts "$current")" + read -r next_major next_minor next_patch <<< "$(semver_parts "$next")" - IFS=. read -r left_major left_minor left_patch <<< "$left" - IFS=. read -r right_major right_minor right_patch <<< "$right" + if (( next_major == current_major && next_minor == current_minor && next_patch > current_patch )); then + echo "patch" + return 0 + fi - (( left_major > right_major )) && return 0 - (( left_major < right_major )) && return 1 - (( left_minor > right_minor )) && return 0 - (( left_minor < right_minor )) && return 1 - (( left_patch > right_patch )) + if (( next_major > current_major || (next_major == current_major && next_minor > current_minor) )); then + echo "minor" + return 0 + fi + + return 1 } -if ! version_gt "$latest_version" "$current_version"; then +if ! release_bump_kind=$(bump_kind_for_datadog_ci_change "$current_version" "$latest_version"); then echo "No datadog-ci bump needed. Current default is $current_version; latest is $latest_version." exit 0 fi +if [[ "$release_bump_kind" == "minor" ]]; then + release_label="$semver_minor_label" +else + release_label="$semver_patch_label" +fi + +branch_name="datadog-ci-bump/${latest_version#v}" -branch_name="${BRANCH_NAME:-datadog-ci-bump/${latest_version#v}}" +if [[ "$dry_run" == "true" ]]; then + echo "Current datadog-ci version: $current_version" + echo "Target datadog-ci version: $latest_version" + echo "Release bump kind: $release_bump_kind" + echo "Branch to create: $branch_name" + echo "Files to update: action.yaml README.md" + echo "Labels to apply: $bump_label $release_label" + 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" \ @@ -96,7 +147,7 @@ if [[ -n "$existing_pr_url" ]]; then fi if git show-ref --verify --quiet "refs/heads/$branch_name"; then - echo "Local branch '$branch_name' already exists. Delete it or set BRANCH_NAME to another value." >&2 + echo "Local branch '$branch_name' already exists. Delete it before rerunning the script." >&2 exit 1 fi @@ -111,10 +162,16 @@ git push -u "$remote" "$branch_name" gh label create "$bump_label" \ --repo "$repo" \ - --description "Triggers a junit-upload-github-action release after merge" \ + --description "Marks PRs that bump the default datadog-ci version" \ --color "1D76DB" \ --force +gh label create "$release_label" \ + --repo "$repo" \ + --description "Requests a $release_bump_kind junit-upload-github-action release after merge" \ + --color "0E8A16" \ + --force + body_file=$(mktemp) trap 'rm -f "$body_file"' EXIT cat > "$body_file" <&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 v3.2.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 auth status >/dev/null + +base_branch="main" +remote="origin" +repo="DataDog/junit-upload-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 "Commit '$target_sha' is already included in latest action release $latest_tag." >&2 + exit 1 +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 + +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" ]] && continue + [[ -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_tag="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 + +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 tag: $major_tag" +echo "Release commit: $target_sha" + +if [[ "$dry_run" == "true" ]]; then + echo "Dry run only. No tags or GitHub Release were created." + exit 0 +fi + +git tag "$next_tag" "$target_sha" +git tag -f "$major_tag" "$target_sha" +git push "$remote" "refs/tags/$next_tag" +git push --force "$remote" "refs/tags/$major_tag" + +gh release create "$next_tag" \ + --repo "$repo" \ + --title "$next_tag" \ + --verify-tag \ + --generate-notes \ + --notes-start-tag "$latest_tag" diff --git a/scripts/release-datadog-ci-bump.sh b/scripts/release-datadog-ci-bump.sh deleted file mode 100755 index 721ad47..0000000 --- a/scripts/release-datadog-ci-bump.sh +++ /dev/null @@ -1,281 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -usage() { - cat <<'EOF' -Usage: scripts/release-datadog-ci-bump.sh [--dry-run] [--pr NUMBER | --sha COMMIT] - -Finds the latest merged datadog-ci bump PR on main that is not included in the -latest immutable action release tag. When one exists, creates the next action -minor or patch tag, updates the moving major tag, and creates a GitHub Release. -If older unreleased bump PRs exist, the script warns and releases only the latest -change. - -Options: - --dry-run Print the release that would be created. - --pr NUMBER Release a specific merged bump PR instead of the latest one. - --sha COMMIT Release a specific commit on main instead of finding a PR. - -Environment: - BASE_BRANCH Branch to inspect. Defaults to main. - BUMP_LABEL Label that marks release-triggering PRs. Defaults to datadog-ci-version-bump. - REMOTE Git remote to fetch and push. Defaults to origin. - REPO GitHub repo for gh commands. Defaults to gh repo view's repo. - PR_LIMIT Number of merged PRs to inspect. Defaults to 200. -EOF -} - -dry_run=false -requested_pr="" -requested_sha="" -while [[ $# -gt 0 ]]; do - case "$1" in - -h|--help) - usage - exit 0 - ;; - --dry-run) - dry_run=true - shift - ;; - --pr) - if [[ -n "$requested_sha" || -n "$requested_pr" || -z "${2:-}" ]]; then - usage >&2 - exit 1 - fi - requested_pr="$2" - shift 2 - ;; - --sha) - if [[ -n "$requested_pr" || -n "$requested_sha" || -z "${2:-}" ]]; then - usage >&2 - exit 1 - fi - requested_sha="$2" - shift 2 - ;; - *) - usage >&2 - exit 1 - ;; - esac -done - -if [[ -n "$requested_pr" && ! "$requested_pr" =~ ^[0-9]+$ ]]; then - echo "--pr expects a numeric pull request number, got '$requested_pr'" >&2 - exit 1 -fi - -for command in gh git ruby; 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 auth status >/dev/null - -base_branch="${BASE_BRANCH:-main}" -bump_label="${BUMP_LABEL:-datadog-ci-version-bump}" -remote="${REMOTE:-origin}" -repo="${REPO:-$(gh repo view --json nameWithOwner --jq '.nameWithOwner')}" -pr_limit="${PR_LIMIT:-200}" - -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 - -release_pr_number="" -release_pr_url="" -release_pr_merged_at="" -release_sha="" -unreleased_count=0 - -validate_release_sha() { - local sha="$1" - - if ! git cat-file -e "${sha}^{commit}" 2>/dev/null; then - echo "Commit '$sha' was not found locally." >&2 - return 1 - fi - - if ! git merge-base --is-ancestor "$sha" "$base_ref"; then - echo "Commit '$sha' is not in $remote/$base_branch history." >&2 - return 1 - fi - - if git merge-base --is-ancestor "$sha" "$latest_tag"; then - echo "Commit '$sha' is already included in latest action release $latest_tag." >&2 - return 1 - fi - - if ! git merge-base --is-ancestor "$latest_tag" "$sha"; then - echo "Commit '$sha' is not after latest action release $latest_tag." >&2 - return 1 - fi -} - -if [[ -n "$requested_sha" ]]; then - release_sha=$(git rev-parse "$requested_sha") - validate_release_sha "$release_sha" -elif [[ -n "$requested_pr" ]]; then - pr_data=$(gh pr view "$requested_pr" \ - --repo "$repo" \ - --json number,url,mergedAt,mergeCommit,baseRefName,labels,state \ - --jq ' - if .state != "MERGED" then - error("PR #\(.number) is not merged") - elif .baseRefName != "'"$base_branch"'" then - error("PR #\(.number) targets \(.baseRefName), not '"$base_branch"'") - elif ([.labels[].name] | index("'"$bump_label"'") | not) then - error("PR #\(.number) does not have label '"$bump_label"'") - else - [.number, .url, .mergedAt, .mergeCommit.oid] | @tsv - end - ') - IFS=$'\t' read -r release_pr_number release_pr_url release_pr_merged_at release_sha <<< "$pr_data" - validate_release_sha "$release_sha" -else - while IFS=$'\t' read -r pr_number pr_url merged_at merge_sha; do - [[ -z "$pr_number" ]] && continue - [[ -z "$merge_sha" ]] && continue - - if ! validate_release_sha "$merge_sha" 2>/dev/null; then - continue - fi - - unreleased_count=$((unreleased_count + 1)) - release_pr_number="$pr_number" - release_pr_url="$pr_url" - release_pr_merged_at="$merged_at" - release_sha="$merge_sha" - done < <( - gh pr list \ - --repo "$repo" \ - --state merged \ - --base "$base_branch" \ - --label "$bump_label" \ - --limit "$pr_limit" \ - --json number,url,mergedAt,mergeCommit \ - --jq 'sort_by(.mergedAt) | .[] | [.number, .url, .mergedAt, .mergeCommit.oid] | @tsv' - ) - - if (( unreleased_count > 1 )); then - echo "Warning: found $unreleased_count unreleased merged PRs with label '$bump_label'." >&2 - echo "This script will release only the latest one, #$release_pr_number." >&2 - echo "To release an older merge commit separately, stop and rerun with --pr NUMBER or --sha COMMIT before releasing the latest one." >&2 - fi -fi - -if [[ -z "$release_sha" ]]; then - echo "No unreleased merged PR with label '$bump_label' found on $remote/$base_branch." - exit 0 -fi - -if [[ -n "$release_pr_number" ]]; then - release_source="#$release_pr_number: $release_pr_url" -else - release_source="$release_sha" -fi - -extract_datadog_ci_version() { - local ref="$1" - - git show "$ref:action.yaml" | ruby -ryaml -e 'puts YAML.load($stdin.read).fetch("inputs").fetch("datadog-ci-version").fetch("default")' -} - -is_exact_datadog_ci_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" -} - -latest_datadog_ci_version=$(extract_datadog_ci_version "$latest_tag") -release_datadog_ci_version=$(extract_datadog_ci_version "$release_sha") - -if ! is_exact_datadog_ci_version "$release_datadog_ci_version"; then - echo "Expected datadog-ci-version default to be an exact release tag at $release_sha, got '$release_datadog_ci_version'" >&2 - exit 1 -fi - -action_bump_kind="minor" -if is_exact_datadog_ci_version "$latest_datadog_ci_version"; then - read -r latest_dd_major latest_dd_minor latest_dd_patch <<< "$(semver_parts "$latest_datadog_ci_version")" - read -r release_dd_major release_dd_minor release_dd_patch <<< "$(semver_parts "$release_datadog_ci_version")" - - if (( release_dd_major == latest_dd_major && release_dd_minor == latest_dd_minor && release_dd_patch > latest_dd_patch )); then - action_bump_kind="patch" - elif (( release_dd_major > latest_dd_major || (release_dd_major == latest_dd_major && release_dd_minor > latest_dd_minor) )); then - action_bump_kind="minor" - else - echo "Expected datadog-ci-version at $release_sha ('$release_datadog_ci_version') to be newer than the latest released default ('$latest_datadog_ci_version')." >&2 - exit 1 - fi -fi - -version="${latest_tag#v}" -IFS=. read -r major minor patch <<< "$version" -if [[ "$action_bump_kind" == "minor" ]]; then - next_tag="v${major}.$((minor + 1)).0" -else - next_tag="v${major}.${minor}.$((patch + 1))" -fi -major_tag="v${major}" - -if git rev-parse --verify --quiet "refs/tags/$next_tag" >/dev/null; then - echo "Next release 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 action release tag: $latest_tag" -echo "Next action release tag: $next_tag" -echo "Action bump kind: $action_bump_kind" -echo "Moving major tag: $major_tag" -echo "Release commit: $release_sha" -if [[ -n "$release_pr_number" ]]; then - echo "Release PR: #$release_pr_number" -fi -echo "Latest released datadog-ci version: $latest_datadog_ci_version" -echo "Release datadog-ci version: $release_datadog_ci_version" - -if [[ "$dry_run" == "true" ]]; then - echo "Dry run only. No tags or GitHub Release were created." - exit 0 -fi - -git tag "$next_tag" "$release_sha" -git tag -f "$major_tag" "$release_sha" -git push "$remote" "refs/tags/$next_tag" -git push --force "$remote" "refs/tags/$major_tag" - -gh release create "$next_tag" \ - --repo "$repo" \ - --title "$next_tag" \ - --verify-tag \ - --generate-notes \ - --notes-start-tag "$latest_tag"