From ed7a32cf4b1b28e33aa0bd9e971db5220c934cf0 Mon Sep 17 00:00:00 2001 From: iunanua Date: Tue, 19 May 2026 16:38:30 +0200 Subject: [PATCH 1/2] ci(release): accept multiple crates in release-proposal-dispatch Replace the single-choice `crate` input with a `crates` string input that accepts comma/whitespace-separated names, validated up front against publishable workspace members. Hotfix releases still require a single crate. Branch names collapse to `+-more` when more than one crate is selected; the PR title preserves the existing `chore(release): proposal for ` prefix. Co-Authored-By: Claude Opus 4.7 --- .../workflows/release-proposal-dispatch.yml | 143 +++++++++++++----- 1 file changed, 103 insertions(+), 40 deletions(-) diff --git a/.github/workflows/release-proposal-dispatch.yml b/.github/workflows/release-proposal-dispatch.yml index 4cc3aa674a..c790dd0531 100644 --- a/.github/workflows/release-proposal-dispatch.yml +++ b/.github/workflows/release-proposal-dispatch.yml @@ -2,35 +2,16 @@ name: Release - Open a release proposal PR on: workflow_dispatch: inputs: - crate: - description: Crate to release + crates: + description: > + Crate(s) to release. Provide one or more publishable workspace crate names separated + by commas or whitespace (e.g. "libdd-common" or "libdd-common, libdd-telemetry"). + Each selected crate is released along with its workspace dependencies (other libdd-* crates it depends on). + Selecting libdd-telemetry alone releases libdd-telemetry plus every libdd-* crate it transitively depends on. + Hotfix releases (main_start_ref matching hotfix//.x.x) accept a single crate only. required: true - type: choice - options: - - libdd-alloc - - libdd-capabilities - - libdd-capabilities-impl - - libdd-common - - libdd-crashtracker - - libdd-data-pipeline - - libdd-ddsketch - - libdd-dogstatsd-client - - libdd-http-client - - libdd-library-config - - libdd-log - - libdd-otel-thread-ctx - - libdd-profiling - - libdd-profiling-protobuf - - libdd-sampling - - libdd-shared-runtime - - libdd-telemetry - - libdd-tinybytes - - libdd-trace-normalization - - libdd-trace-obfuscation - - libdd-trace-protobuf - - libdd-trace-stats - - libdd-trace-utils - - libdd-tracer-flare + type: string + default: '' main_start_ref: description: > Optional git ref to cut the release from: commit SHA (short or full), branch name, @@ -57,8 +38,81 @@ env: PROPOSAL_BRANCH_PREFIX: ${{ inputs.bypass_standard_checks && 'release-proposal-testing' || 'release-proposal' }} jobs: + validate-inputs: + runs-on: ubuntu-latest + outputs: + crates: ${{ steps.normalize.outputs.crates }} + crates_display: ${{ steps.normalize.outputs.crates_display }} + crates_branch: ${{ steps.normalize.outputs.crates_branch }} + count: ${{ steps.normalize.outputs.count }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 + - name: Normalize and validate crate list + id: normalize + env: + RAW_CRATES: ${{ inputs.crates }} + RAW_MAIN_START_REF: ${{ inputs.main_start_ref }} + run: | + set -euo pipefail + + # Split on commas/whitespace, drop empties, dedupe, sort for stable downstream order. + mapfile -t CRATES < <(printf '%s\n' "$RAW_CRATES" | tr ',' ' ' | tr -s '[:space:]' '\n' | sed '/^$/d' | sort -u) + + if [ "${#CRATES[@]}" -eq 0 ]; then + echo "Error: 'crates' input is empty after normalization." >&2 + exit 1 + fi + + AVAILABLE=$(cargo metadata --no-deps --format-version=1 | jq -r ' + .packages[] + | select(.publish == null or (.publish | type == "array" and length > 0)) + | .name + ' | sort -u) + + UNKNOWN=() + for c in "${CRATES[@]}"; do + if ! grep -qx "$c" <<< "$AVAILABLE"; then + UNKNOWN+=("$c") + fi + done + if [ "${#UNKNOWN[@]}" -gt 0 ]; then + echo "Error: unknown or unpublishable crate(s): ${UNKNOWN[*]}" >&2 + echo "Available crates:" >&2 + sed 's/^/ - /' <<< "$AVAILABLE" >&2 + exit 1 + fi + + REF="$(echo "$RAW_MAIN_START_REF" | tr -d '[:space:]')" + if [[ -n "$REF" && "$REF" =~ ^hotfix/[^/]+/[0-9]+\.x\.x$ ]] && [ "${#CRATES[@]}" -gt 1 ]; then + echo "Error: hotfix releases (main_start_ref=$REF) accept only a single crate; got ${#CRATES[@]}: ${CRATES[*]}" >&2 + exit 1 + fi + + CRATES_SPACE="${CRATES[*]}" + CRATES_DISPLAY=$(IFS=,; echo "${CRATES[*]}") + CRATES_DISPLAY=${CRATES_DISPLAY//,/, } + # Branch segment: keep the path concise when many crates are bundled. + if [ "${#CRATES[@]}" -eq 1 ]; then + CRATES_BRANCH="${CRATES[0]}" + else + CRATES_BRANCH="${CRATES[0]}+$(( ${#CRATES[@]} - 1 ))-more" + fi + + echo "Normalized crates: $CRATES_SPACE" + echo "Display: $CRATES_DISPLAY" + echo "Branch segment: $CRATES_BRANCH" + echo "Count: ${#CRATES[@]}" + + { + echo "crates=$CRATES_SPACE" + echo "crates_display=$CRATES_DISPLAY" + echo "crates_branch=$CRATES_BRANCH" + echo "count=${#CRATES[@]}" + } >> "$GITHUB_OUTPUT" + check-proposal-ongoing: runs-on: ubuntu-latest + needs: validate-inputs steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 with: @@ -114,7 +168,7 @@ jobs: id-token: write # Enable OIDC pull-requests: write contents: write - needs: check-membership + needs: [check-membership, validate-inputs] runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # 4.2.2 @@ -276,7 +330,7 @@ jobs: echo "ephemeral_branch=$EPHEMERAL_BRANCH" >> "$GITHUB_OUTPUT" echo "is_hotfix=true" >> "$GITHUB_OUTPUT" else - EPHEMERAL_BRANCH="${{ env.RELEASE_BRANCH_PREFIX }}/${{ inputs.crate }}/$TIMESTAMP" + EPHEMERAL_BRANCH="${{ env.RELEASE_BRANCH_PREFIX }}/${{ needs.validate-inputs.outputs.crates_branch }}/$TIMESTAMP" git checkout -b "$EPHEMERAL_BRANCH" git push origin "$EPHEMERAL_BRANCH" echo "Ephemeral release branch created: $EPHEMERAL_BRANCH branch ($(git rev-parse --short HEAD))" @@ -286,11 +340,14 @@ jobs: echo "timestamp=$TIMESTAMP" >> "$GITHUB_OUTPUT" - name: Get publication order for crate and dependencies + env: + CRATES: ${{ needs.validate-inputs.outputs.crates }} run: | - echo "Getting publication order for ${{ inputs.crate }}..." - - # Get the publication order as JSON and save to file - "${WORKFLOW_SCRIPTS_ROOT}/publication-order.sh" --format=json "${{ inputs.crate }}" > /tmp/crates.json + echo "Getting publication order for ${{ needs.validate-inputs.outputs.crates_display }}..." + + # CRATES is a space-separated list validated upstream; word-splitting is intentional. + # shellcheck disable=SC2086 + "${WORKFLOW_SCRIPTS_ROOT}/publication-order.sh" --format=json $CRATES > /tmp/crates.json echo "Publication order:" cat /tmp/crates.json @@ -325,7 +382,7 @@ jobs: git checkout "${{ steps.ephemeral-branch.outputs.ephemeral_branch }}" TIMESTAMP="${{ steps.ephemeral-branch.outputs.timestamp }}" - BRANCH_NAME="${{ env.PROPOSAL_BRANCH_PREFIX }}/${{ inputs.crate }}/$TIMESTAMP" + BRANCH_NAME="${{ env.PROPOSAL_BRANCH_PREFIX }}/${{ needs.validate-inputs.outputs.crates_branch }}/$TIMESTAMP" git checkout -b "$BRANCH_NAME" git push origin "$BRANCH_NAME" --tags echo "Branch created: $BRANCH_NAME from ${{ steps.ephemeral-branch.outputs.ephemeral_branch }} branch" @@ -687,7 +744,7 @@ jobs: is_hotfix: ${{ steps.ephemeral-branch.outputs.is_hotfix }} create-pr: - needs: cargo-release + needs: [cargo-release, validate-inputs] runs-on: ubuntu-latest permissions: id-token: write # Enable OIDC @@ -766,19 +823,25 @@ jobs: COMMITS_AND_API_BODY=$(jq -nr --slurpfile api /tmp/api-changes-with-major-bumps.json "$JQ_FILTER") - PR_BODY="# Release proposal for ${{ inputs.crate }} and its dependencies + if [ "${{ needs.validate-inputs.outputs.count }}" -gt 1 ]; then + POSSESSIVE="their" + else + POSSESSIVE="its" + fi + + PR_BODY="# Release proposal for ${{ needs.validate-inputs.outputs.crates_display }} and $POSSESSIVE dependencies This PR contains version bumps based on public API changes and commits since last release. ${HOTFIX_NOTE}${NON_DEFAULT}${COMMITS_AND_API_BODY}" - + echo "$PR_BODY" > /tmp/pr-body.md echo "PR body written to /tmp/pr-body.md (length: $(wc -c < /tmp/pr-body.md) bytes)" - # NOTE: the PR title is used to filter gitlab CI jobs. If you change it, you need to update the gitlab CI job filter. + # NOTE: the PR title must start with 'chore(release): proposal for'. Downstream tooling matches on that prefix. gh pr create \ --head "$BRANCH_NAME" \ - --title "chore(release): proposal for ${{ inputs.crate }}" \ + --title "chore(release): proposal for ${{ needs.validate-inputs.outputs.crates_display }}" \ --body-file /tmp/pr-body.md \ --label "release-proposal" \ --label "skip-metadata-check" \ From 2a737179742383106bd5ec0a657c8b6d1e72e7de Mon Sep 17 00:00:00 2001 From: iunanua Date: Tue, 19 May 2026 16:50:35 +0200 Subject: [PATCH 2/2] ci(release): satisfy shellcheck in validate-inputs and create-pr SC2001 (line 80): replace `sed 's/^/ - /' <<< "$AVAILABLE"` with `printf ' - %s\n' "${AVAILABLE[@]}"` by making AVAILABLE an array; the membership check uses `printf | grep -qxF` accordingly. SC2170 (line 826): expose `validate-inputs.outputs.count` through the step's existing `env:` block as `$CRATES_COUNT` so shellcheck sees a shell variable instead of a literal `${{ ... }}` in the numeric test. Co-Authored-By: Claude Opus 4.7 --- .github/workflows/release-proposal-dispatch.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-proposal-dispatch.yml b/.github/workflows/release-proposal-dispatch.yml index c790dd0531..6098e8ea7b 100644 --- a/.github/workflows/release-proposal-dispatch.yml +++ b/.github/workflows/release-proposal-dispatch.yml @@ -4,10 +4,8 @@ on: inputs: crates: description: > - Crate(s) to release. Provide one or more publishable workspace crate names separated - by commas or whitespace (e.g. "libdd-common" or "libdd-common, libdd-telemetry"). + Crate(s) to release. Names separated by commas or whitespace (e.g. "libdd-common" or "libdd-common, libdd-telemetry"). Each selected crate is released along with its workspace dependencies (other libdd-* crates it depends on). - Selecting libdd-telemetry alone releases libdd-telemetry plus every libdd-* crate it transitively depends on. Hotfix releases (main_start_ref matching hotfix//.x.x) accept a single crate only. required: true type: string @@ -63,7 +61,7 @@ jobs: exit 1 fi - AVAILABLE=$(cargo metadata --no-deps --format-version=1 | jq -r ' + mapfile -t AVAILABLE < <(cargo metadata --no-deps --format-version=1 | jq -r ' .packages[] | select(.publish == null or (.publish | type == "array" and length > 0)) | .name @@ -71,14 +69,14 @@ jobs: UNKNOWN=() for c in "${CRATES[@]}"; do - if ! grep -qx "$c" <<< "$AVAILABLE"; then + if ! printf '%s\n' "${AVAILABLE[@]}" | grep -qxF "$c"; then UNKNOWN+=("$c") fi done if [ "${#UNKNOWN[@]}" -gt 0 ]; then echo "Error: unknown or unpublishable crate(s): ${UNKNOWN[*]}" >&2 echo "Available crates:" >&2 - sed 's/^/ - /' <<< "$AVAILABLE" >&2 + printf ' - %s\n' "${AVAILABLE[@]}" >&2 exit 1 fi @@ -775,6 +773,7 @@ jobs: BYPASS_STANDARD_CHECKS: ${{ inputs.bypass_standard_checks }} PROPOSAL_BRANCH_PREFIX: ${{ env.PROPOSAL_BRANCH_PREFIX }} RELEASE_BRANCH_PREFIX: ${{ env.RELEASE_BRANCH_PREFIX }} + CRATES_COUNT: ${{ needs.validate-inputs.outputs.count }} run: | BRANCH_NAME="${{ needs.cargo-release.outputs.branch_name }}" EPHEMERAL_BRANCH="${{ needs.cargo-release.outputs.ephemeral_branch }}" @@ -823,7 +822,7 @@ jobs: COMMITS_AND_API_BODY=$(jq -nr --slurpfile api /tmp/api-changes-with-major-bumps.json "$JQ_FILTER") - if [ "${{ needs.validate-inputs.outputs.count }}" -gt 1 ]; then + if [ "$CRATES_COUNT" -gt 1 ]; then POSSESSIVE="their" else POSSESSIVE="its"