diff --git a/.github/workflows/release-proposal-dispatch.yml b/.github/workflows/release-proposal-dispatch.yml index 4cc3aa674a..6098e8ea7b 100644 --- a/.github/workflows/release-proposal-dispatch.yml +++ b/.github/workflows/release-proposal-dispatch.yml @@ -2,35 +2,14 @@ name: Release - Open a release proposal PR on: workflow_dispatch: inputs: - crate: - description: Crate to release + crates: + description: > + 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). + 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 +36,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 + + mapfile -t 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 ! 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 + printf ' - %s\n' "${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 +166,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 +328,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 +338,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 +380,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 +742,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 @@ -718,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 }}" @@ -766,19 +822,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 [ "$CRATES_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" \