diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 272a0c2164..4c80da66ff 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -129,6 +129,10 @@ When a new issue is created, follow these steps: - Suggest release note entries for fixes by updating files under `release-notes/` or by using the `release-notes` prompt (instead of editing `CHANGELOG.md` directly). - Tag reviewers based on `CODEOWNERS` file +## 🌿 Branch Naming +- All branches created by AI agents **must** use the `dev/automation/` prefix (e.g. `dev/automation/fix-connection-timeout`). +- Do **not** create branches directly under `main`, `dev/`, or any other top-level prefix. + ## 🧠 Contextual Awareness - All source code is in `src/Microsoft.Data.SqlClient/src/`. Do NOT add code to legacy `netfx/src/` or `netcore/src/` directories. - Only `ref/` folders in `netcore/ref/` and `netfx/ref/` remain active for defining the public API surface. diff --git a/.github/scripts/cherry-pick-to-release.sh b/.github/scripts/cherry-pick-to-release.sh new file mode 100755 index 0000000000..8b3172cc50 --- /dev/null +++ b/.github/scripts/cherry-pick-to-release.sh @@ -0,0 +1,202 @@ +#!/usr/bin/env bash +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +# +# cherry-pick-to-release.sh +# +# Cherry-picks a merge commit from the default branch onto a release branch +# and opens a pull request for the result. If the cherry-pick conflicts, an +# empty-commit placeholder PR is created with manual resolution instructions. +# +# OVERVIEW +# -------- +# This script performs the following steps: +# +# 1. Derive the target release branch from the version's major.minor +# (e.g. "7.0.1" → release/7.0). +# +# 2. Check whether the commit's patch is already present on the target +# branch (via 'git cherry'). If so, exit cleanly — nothing to do. +# +# 3. Detect whether the merge commit is a true merge (2+ parents) or a +# squash-merge (1 parent). True merges require '--mainline 1'. +# +# 4. Attempt the cherry-pick: +# - On success: push the branch, look up the milestone, create a PR. +# - On conflict: abort, push an empty-commit placeholder, create a +# "CONFLICTS" PR with manual resolution instructions. +# +# 5. Milestone lookup is best-effort. If the milestone doesn't exist yet +# the PR is created without one and a warning note is added to the body. +# +# REQUIRED ENVIRONMENT VARIABLES +# ------------------------------ +# VERSION Full hotfix version, e.g. "7.0.1". +# MERGE_COMMIT_SHA SHA of the merge commit on the default branch. +# PR_NUMBER Number of the original PR that was merged. +# PR_TITLE Title of the original PR (used in cherry-pick PR title). +# GH_TOKEN GitHub token for 'gh' CLI authentication. +# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set by Actions. +# +# OUTPUTS +# ------- +# On success or conflict, a new PR is created on GitHub. +# On already-applied, the script exits 0 with a notice. +# +# USAGE +# Typically called from the cherry-pick-hotfix.yml workflow. +# The git working directory must have full history (fetch-depth: 0) and +# user.name / user.email must be configured before calling this script. +# +# Local testing example (dry-run — comment out 'gh pr create' calls): +# +# export VERSION="7.0.1" +# export MERGE_COMMIT_SHA="abc123" +# export PR_NUMBER=42 +# export PR_TITLE="Fix connection timeout" +# export GH_TOKEN="ghp_..." +# export GITHUB_REPOSITORY="dotnet/SqlClient" +# bash .github/scripts/cherry-pick-to-release.sh +# +################################################################################# +set -euo pipefail + +# -- Runtime help ------------------------------------------------------------- +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + # Print the header comment block (between the license banner and the + # closing banner), stripping the leading '# ' prefix. + awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0" + exit 0 +fi + +# -- Input validation --------------------------------------------------------- +: "${VERSION:?VERSION environment variable is required}" +: "${MERGE_COMMIT_SHA:?MERGE_COMMIT_SHA environment variable is required}" +: "${PR_NUMBER:?PR_NUMBER environment variable is required}" +: "${PR_TITLE:?PR_TITLE environment variable is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}" + +# -- Step 1: Derive target branch from major.minor --------------------------- +# "7.0.1" → "7.0", so target branch is "release/7.0". +# Use a bash regex for portability (grep -P is not available on macOS BSD grep). +if [[ "${VERSION}" =~ ^([0-9]+)\.([0-9]+) ]]; then + BRANCH_VERSION="${BASH_REMATCH[1]}.${BASH_REMATCH[2]}" +else + BRANCH_VERSION="" +fi +if [[ -z "${BRANCH_VERSION}" ]]; then + echo "::error::Could not parse major.minor from version '${VERSION}'." + exit 1 +fi + +TARGET_BRANCH="release/${BRANCH_VERSION}" +CHERRY_PICK_BRANCH="dev/automation/pr-${PR_NUMBER}-to-${VERSION}" + +echo "Version: ${VERSION}" +echo "Target branch: ${TARGET_BRANCH}" +echo "Cherry-pick branch: ${CHERRY_PICK_BRANCH}" +echo "Merge commit: ${MERGE_COMMIT_SHA}" + +# Ensure the target branch ref is available locally. +git fetch origin "${TARGET_BRANCH}" + +# -- Step 2: Check if the patch is already applied ---------------------------- +# 'git cherry ' lists commits in .. and +# marks each with '-' (patch already on ) or '+' (not yet applied). +# By passing =MERGE_COMMIT_SHA and =MERGE_COMMIT_SHA^ we scope +# the check to exactly one commit — the PR being cherry-picked. +if git cherry "origin/${TARGET_BRANCH}" "${MERGE_COMMIT_SHA}" "${MERGE_COMMIT_SHA}^" \ + | grep -q '^-'; then + echo "::notice::Commit ${MERGE_COMMIT_SHA} is already applied on" \ + "${TARGET_BRANCH}. Skipping cherry-pick." + exit 0 +fi + +# Create the cherry-pick working branch from the target release branch. +git checkout -b "${CHERRY_PICK_BRANCH}" "origin/${TARGET_BRANCH}" + +# -- Step 3: Detect merge commit type ----------------------------------------- +# True merge commits have 2+ parents and require '--mainline 1' to tell git +# which parent's tree to diff against (the first parent = the target branch). +# Squash-merge commits have exactly 1 parent and must NOT use --mainline. +# +# 'git rev-list --parents -n1 ' outputs: [ ...] +# awk counts fields and subtracts 1 (the SHA itself) to get the parent count. +PARENT_COUNT=$(git rev-list --parents -n1 "${MERGE_COMMIT_SHA}" \ + | awk '{print NF - 1}') +MAINLINE_FLAG="" +if [[ "${PARENT_COUNT}" -gt 1 ]]; then + MAINLINE_FLAG="--mainline 1" + echo "Merge commit has ${PARENT_COUNT} parents — using --mainline 1." +else + echo "Squash-merge commit (single parent) — no --mainline flag needed." +fi + +# -- Helper: look up milestone ------------------------------------------------ +# Milestone assignment is best-effort. If the milestone doesn't exist yet, the +# PR is created without one and a note is appended to the PR body. +lookup_milestone() { + local version="$1" + MILESTONE_ARG="" + MILESTONE_NOTE="" + + if gh api "repos/${GITHUB_REPOSITORY}/milestones" --paginate \ + --field state=open --jq '.[].title' | grep -qx "${version}"; then + MILESTONE_ARG="--milestone ${version}" + echo "Milestone '${version}' found." + else + echo "::warning::Milestone '${version}' does not exist." \ + "PR will be created without a milestone." + MILESTONE_NOTE=$'\n\n> **Note:** Milestone `'"${version}"'` does not exist yet. Please create it and assign this PR manually.' + fi +} + +# -- Step 4: Attempt the cherry-pick ------------------------------------------ +# Options (--mainline) must precede the commit operand. +if git cherry-pick ${MAINLINE_FLAG} "${MERGE_COMMIT_SHA}"; then + # --- Success path --- + echo "Cherry-pick succeeded. Pushing branch and creating PR." + git push origin "${CHERRY_PICK_BRANCH}" + + lookup_milestone "${VERSION}" + + gh pr create \ + --base "${TARGET_BRANCH}" \ + --head "${CHERRY_PICK_BRANCH}" \ + --title "[${VERSION} Cherry-pick] ${PR_TITLE}" \ + --body "Cherry-pick of #${PR_NUMBER} (${MERGE_COMMIT_SHA}) into \`${TARGET_BRANCH}\`.${MILESTONE_NOTE}" \ + ${MILESTONE_ARG} +else + # --- Conflict path --- + echo "::error::Cherry-pick of ${MERGE_COMMIT_SHA} failed due to conflicts." + git cherry-pick --abort + + # Build the cherry-pick command for inclusion in the conflict-resolution + # instructions. Options must precede the commit SHA. + CHERRY_PICK_CMD="git cherry-pick" + if [[ -n "${MAINLINE_FLAG}" ]]; then + CHERRY_PICK_CMD="${CHERRY_PICK_CMD} ${MAINLINE_FLAG}" + fi + CHERRY_PICK_CMD="${CHERRY_PICK_CMD} ${MERGE_COMMIT_SHA}" + + # Create a branch with an empty commit so a PR can be opened. The PR body + # contains step-by-step instructions for manual conflict resolution. + git checkout "origin/${TARGET_BRANCH}" + git checkout -B "${CHERRY_PICK_BRANCH}" + git commit --allow-empty \ + -m "Cherry-pick of #${PR_NUMBER} requires manual resolution" \ + -m "To resolve, run: ${CHERRY_PICK_CMD}" + git push origin "${CHERRY_PICK_BRANCH}" + + lookup_milestone "${VERSION}" + + gh pr create \ + --base "${TARGET_BRANCH}" \ + --head "${CHERRY_PICK_BRANCH}" \ + --title "[${VERSION} Cherry-pick - CONFLICTS] ${PR_TITLE}" \ + ${MILESTONE_ARG} \ + --body $'Cherry-pick of #'"${PR_NUMBER}"' ('"${MERGE_COMMIT_SHA}"') into `'"${TARGET_BRANCH}"'` **failed due to merge conflicts**.'"${MILESTONE_NOTE}"$'\n\nPlease resolve manually:\n```bash\ngit fetch origin\ngit checkout '"${CHERRY_PICK_BRANCH}"'\n'"${CHERRY_PICK_CMD}"$'\n# resolve conflicts\ngit push origin '"${CHERRY_PICK_BRANCH}"' --force\n```' +fi diff --git a/.github/scripts/extract-hotfix-versions.sh b/.github/scripts/extract-hotfix-versions.sh new file mode 100755 index 0000000000..b7c702a8f8 --- /dev/null +++ b/.github/scripts/extract-hotfix-versions.sh @@ -0,0 +1,136 @@ +#!/usr/bin/env bash +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +# +# extract-hotfix-versions.sh +# +# Parses "Hotfix X.Y.Z" labels from a merged GitHub PR and emits a JSON array +# of version strings suitable for use as a GitHub Actions matrix dimension. +# +# OVERVIEW +# -------- +# This script handles two distinct trigger scenarios: +# +# 1. 'closed' event — The PR was just merged. ALL "Hotfix X.Y.Z" labels on +# the PR are processed, emitting one version per valid label. +# +# 2. 'labeled' event — A label was added to an already-merged PR. Only the +# NEWLY ADDED label is considered, and only if a cherry-pick for that +# version hasn't already been created (branch or PR exists). +# +# Label names must match the exact pattern "Hotfix .." +# (e.g. "Hotfix 7.0.1"). All other labels are silently ignored. +# +# REQUIRED ENVIRONMENT VARIABLES +# ------------------------------ +# LABELS Comma-separated list of all label names on the PR. +# EVENT_ACTION The GitHub event action: "closed" or "labeled". +# EVENT_LABEL For 'labeled' events, the name of the label that was added. +# Empty or unset for 'closed' events. +# PR_NUMBER The pull request number (used to derive cherry-pick branch names). +# GH_TOKEN GitHub token for API calls (gh CLI auth). +# GITHUB_REPOSITORY Owner/repo (e.g. "dotnet/SqlClient"). Set automatically by Actions. +# +# OUTPUTS +# ------- +# Writes to $GITHUB_OUTPUT: +# versions= e.g. versions=["7.0.1","8.0.0"] +# +# An empty array (versions=[]) means no work is needed. +# The script exits with code 1 if the 'closed' event has no valid labels. +# +# USAGE +# Called from the cherry-pick-hotfix.yml workflow. Can also be run locally +# for testing by setting the required environment variables and providing a +# writable GITHUB_OUTPUT file: +# +# export LABELS="Hotfix 7.0.1,bug" +# export EVENT_ACTION="closed" +# export PR_NUMBER=42 +# export GITHUB_OUTPUT=$(mktemp) +# bash .github/scripts/extract-hotfix-versions.sh +# cat "$GITHUB_OUTPUT" +# +################################################################################# +set -euo pipefail + +# -- Runtime help ------------------------------------------------------------- +if [[ "${1:-}" == "--help" || "${1:-}" == "-h" ]]; then + # Print the header comment block (between the license banner and the + # closing banner), stripping the leading '# ' prefix. + awk '/^#{2,}$/ { n++; next } n == 2 { sub(/^# ?/, ""); print }' "$0" + exit 0 +fi + +# -- Input validation --------------------------------------------------------- +: "${LABELS:?LABELS environment variable is required}" +: "${EVENT_ACTION:?EVENT_ACTION environment variable is required}" +: "${PR_NUMBER:?PR_NUMBER environment variable is required}" +: "${GITHUB_REPOSITORY:?GITHUB_REPOSITORY environment variable is required}" + +# -- 'labeled' event: process only the newly added label ---------------------- +if [[ "${EVENT_ACTION}" == "labeled" ]]; then + # Extract version from the new label. If it doesn't match "Hotfix X.Y.Z", + # this is a non-hotfix label — emit empty matrix and exit cleanly. + if [[ "${EVENT_LABEL:-}" =~ ^Hotfix\ ([0-9]+\.[0-9]+\.[0-9]+)$ ]]; then + CANDIDATE="${BASH_REMATCH[1]}" + else + CANDIDATE="" + fi + + if [[ -z "${CANDIDATE}" ]]; then + echo "Label '${EVENT_LABEL:-}' is not a valid 'Hotfix X.Y.Z' label. Skipping." + echo "versions=[]" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + # Guard against duplicate cherry-picks. If the cherry-pick branch already + # exists on the remote, or a PR (open, closed, or merged) was already created + # from it, there is nothing left to do. + # + # NOTE: We use the GitHub API rather than 'git ls-remote' because the + # detect-versions job does not check out the repository (no .git directory). + CHERRY_PICK_BRANCH="dev/automation/pr-${PR_NUMBER}-to-${CANDIDATE}" + + if gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${CHERRY_PICK_BRANCH}" \ + --silent 2>/dev/null; then + echo "Cherry-pick branch '${CHERRY_PICK_BRANCH}' already exists. Skipping." + echo "versions=[]" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + EXISTING_PR=$(gh pr list --repo "${GITHUB_REPOSITORY}" --head "${CHERRY_PICK_BRANCH}" --state all \ + --json number --jq 'length') + if [[ "${EXISTING_PR}" -gt 0 ]]; then + echo "A cherry-pick PR from '${CHERRY_PICK_BRANCH}' already exists. Skipping." + echo "versions=[]" >> "${GITHUB_OUTPUT}" + exit 0 + fi + + VERSIONS="${CANDIDATE}" +else + # -- 'closed' event: process all hotfix labels on the PR -------------------- + # Split by comma, keep only labels matching "Hotfix X.Y.Z", extract the version. + # Use sed -E for portable extended regex (works on both GNU and BSD sed). + VERSIONS=$(echo "${LABELS}" | tr ',' '\n' \ + | sed -nE 's/^Hotfix ([0-9]+\.[0-9]+\.[0-9]+)$/\1/p') +fi + +# -- Validate that at least one version was found ---------------------------- +if [[ -z "${VERSIONS}" ]]; then + echo "::error::No valid 'Hotfix X.Y.Z' label found. " \ + "Labels must match 'Hotfix ..'." + exit 1 +fi + +# -- Emit JSON array for the matrix strategy ---------------------------------- +# Convert the newline-separated version list into a compact JSON array. +# e.g. "7.0.1\n8.0.0" → ["7.0.1","8.0.0"] +JSON=$(echo "${VERSIONS}" \ + | jq -R -s -c 'split("\n") | map(select(length > 0))') + +echo "versions=${JSON}" >> "${GITHUB_OUTPUT}" +echo "Detected hotfix versions: ${JSON}" diff --git a/.github/scripts/tests/README.md b/.github/scripts/tests/README.md new file mode 100644 index 0000000000..9aac1c6c88 --- /dev/null +++ b/.github/scripts/tests/README.md @@ -0,0 +1,176 @@ +# Cherry-Pick Workflow Tests + +This directory contains tests for the shell scripts used by the +[cherry-pick-hotfix](./../../../.github/workflows/cherry-pick-hotfix.yml) GitHub Actions workflow. +These tests are intended to be run manually by developers when they are changing the associated +scripts, and not as part of any CI runs. + +## What is Bats? + +**[Bats](https://github.com/bats-core/bats-core)** (Bash Automated Testing System) is a +TAP-compliant testing framework for Bash scripts. Each `.bats` file contains one or more `@test` +blocks that run shell commands and assert outcomes using the built-in `run` helper. A test passes +when every command exits with code 0; it fails on the first non-zero exit. + +Key concepts: + +| Concept | Description | +| ------- | ----------- | +| `@test "name" { ... }` | Defines a single test case | +| `run ` | Captures stdout, stderr, and exit code into `$output` and `$status` | +| `setup()` | Runs before every `@test` — used to create temp files and set env vars | +| `teardown()` | Runs after every `@test` — used to clean up temp files | +| `[[ "$status" -eq 0 ]]` | Assert exit code | +| `[[ "$output" == *"text"* ]]` | Assert output contains a string | + +## Installing Bats + +### Linux (apt) + +```bash +sudo apt-get update && sudo apt-get install -y bats +``` + +### macOS (Homebrew) + +```bash +brew install bats-core +``` + +### From source (any platform) + +```bash +git clone https://github.com/bats-core/bats-core.git +cd bats-core +sudo ./install.sh /usr/local +``` + +## Additional Prerequisites + +The scripts under test also require: + +- **jq** — used to build JSON matrix output +- **gh** (GitHub CLI) — used for API calls and PR creation (mocked during tests, but must be on + `$PATH` for the test stubs to shadow it) + +Most CI runners and development machines have these pre-installed. If not: + +```bash +# Linux (apt) +sudo apt-get install -y jq gh + +# macOS (Homebrew) +brew install jq gh +``` + +### Verify installation + +```bash +bats --version +# Expected output: Bats 1.x.x +``` + +## Running the Tests + +All commands assume you are at the **repository root**. + +### Run all tests + +```bash +bats .github/scripts/tests/ +``` + +### Run a single test file + +```bash +bats .github/scripts/tests/extract-hotfix-versions.bats +bats .github/scripts/tests/cherry-pick-to-release.bats +``` + +### Run a specific test by name + +```bash +bats .github/scripts/tests/extract-hotfix-versions.bats \ + --filter "single Hotfix label" +``` + +### Verbose output (show each test name) + +```bash +bats --tap .github/scripts/tests/ +``` + +### Pretty output (requires bats-core 1.5+) + +```bash +bats --formatter pretty .github/scripts/tests/ +``` + +## Test Files + +| File | Tests | Covers | +| ---- | ----- | ------ | +| `extract-hotfix-versions.bats` | 18 | Label parsing, version extraction, matrix JSON output, edge cases (malformed labels, duplicates, `labeled` vs `closed` events) | +| `cherry-pick-to-release.bats` | 15 | Branch derivation, already-applied detection, clean cherry-pick, conflict handling, milestone lookup, PR creation, duplicate skip logic | + +## How the Tests Work + +Both test files use the same general approach: + +1. **`setup()`** creates a temporary directory and populates it with mock `git` and `gh` executables + — simple shell scripts that echo predetermined responses. Environment variables (`VERSION`, + `MERGE_COMMIT_SHA`, etc.) are set to known values. + +2. **`@test` blocks** call `run bash "$SCRIPT"` to execute the script under test in a subshell. The + mocks intercept all `git` and `gh` invocations, so no real repository or GitHub API access is + needed. + +3. **Assertions** check `$status` (exit code) and `$output` (combined stdout/stderr) for expected + values, error messages, GitHub Actions output file writes via `$GITHUB_OUTPUT`, or other + workflow commands such as `::error::` and `::notice::`. + +4. **`teardown()`** removes the temporary directory and mock binaries. + +### Example mock + +```bash +# Mock git that reports 2 parents (a merge commit) +cat > "${STUB_DIR}/git" <<'MOCK' +#!/usr/bin/env bash +case "$*" in + "rev-list --parents -n1 "*) echo "abc123 parent1 parent2" ;; + "cherry "*) echo "+ abc123" ;; + *) echo "git mock: $*" ;; +esac +MOCK +chmod +x "${STUB_DIR}/git" +``` + +The mock sits earlier on `$PATH` than the real `git`, so the script under test calls the mock +transparently. + +## Troubleshooting + +### `bats: command not found` + +Bats is not installed. See [Installing Bats](#installing-bats) above. + +### Tests fail with `permission denied` + +The scripts under `.github/scripts/` must be executable: + +```bash +chmod +x .github/scripts/*.sh +``` + +### A test fails unexpectedly + +Run with `set -x` tracing to see each command: + +```bash +bats --tap .github/scripts/tests/cherry-pick-to-release.bats \ + --filter "name of failing test" 2>&1 +``` + +Or add `echo "DEBUG: $variable" >&3` inside a test to print to the terminal (file descriptor 3 is +bats's "direct to terminal" channel). diff --git a/.github/scripts/tests/cherry-pick-to-release.bats b/.github/scripts/tests/cherry-pick-to-release.bats new file mode 100644 index 0000000000..b8d40a3b3e --- /dev/null +++ b/.github/scripts/tests/cherry-pick-to-release.bats @@ -0,0 +1,294 @@ +#!/usr/bin/env bats +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +# +# Tests for cherry-pick-to-release.sh +# +# Run with: bats .github/scripts/tests/cherry-pick-to-release.bats +# +# NOTE: These tests mock git and gh commands to validate the script's logic +# without requiring a real repository or GitHub API access. +# +# Dependencies: bats-core (https://github.com/bats-core/bats-core) +# +################################################################################# + +# Path to the script under test (relative to repo root). +SCRIPT=".github/scripts/cherry-pick-to-release.sh" + +# ── Helpers ────────────────────────────────────────────────────────────────── + +setup() { + # Create a directory for mock binaries that override real git/gh. + STUB_DIR="$(mktemp -d)" + export PATH="${STUB_DIR}:${PATH}" + + # Defaults — individual tests override as needed. + export VERSION="7.0.1" + export MERGE_COMMIT_SHA="abc123def456" + export PR_NUMBER="42" + export PR_TITLE="Fix connection timeout" + export GH_TOKEN="fake-token" + export GITHUB_REPOSITORY="dotnet/SqlClient" + export GITHUB_OUTPUT="$(mktemp)" +} + +teardown() { + rm -rf "${STUB_DIR}" + rm -f "${GITHUB_OUTPUT}" +} + +# Write a mock 'git' script. Each call to the mock appends a log line so +# tests can verify which git subcommands were executed and with what args. +write_git_mock() { + local body="$1" + cat > "${STUB_DIR}/git" <> "${STUB_DIR}/git.log" +${body} +STUB + chmod +x "${STUB_DIR}/git" +} + +# Write a mock 'gh' script. +write_gh_mock() { + local body="$1" + cat > "${STUB_DIR}/gh" <> "${STUB_DIR}/gh.log" +${body} +STUB + chmod +x "${STUB_DIR}/gh" +} + +# ── --help flag ────────────────────────────────────────────────────────────── + +@test "prints help text with --help" { + run bash "${SCRIPT}" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Cherry-picks a merge commit"* ]] + [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]] +} + +@test "prints help text with -h" { + run bash "${SCRIPT}" -h + [ "$status" -eq 0 ] + [[ "$output" == *"Cherry-picks"* ]] +} + +# ── Input validation ───────────────────────────────────────────────────────── + +@test "fails when VERSION is unset" { + unset VERSION + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"VERSION"* ]] +} + +@test "fails when MERGE_COMMIT_SHA is unset" { + unset MERGE_COMMIT_SHA + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"MERGE_COMMIT_SHA"* ]] +} + +@test "fails when PR_NUMBER is unset" { + unset PR_NUMBER + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"PR_NUMBER"* ]] +} + +@test "fails when PR_TITLE is unset" { + unset PR_TITLE + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"PR_TITLE"* ]] +} + +@test "fails when GITHUB_REPOSITORY is unset" { + unset GITHUB_REPOSITORY + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"GITHUB_REPOSITORY"* ]] +} + +# ── Version parsing ───────────────────────────────────────────────────────── + +@test "derives release/7.0 from version 7.0.1" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + if [[ "$1" == "rev-list" ]]; then echo "abc123 parent1"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + # Verify the git fetch targeted release/7.0. + grep -q "GIT: fetch origin release/7.0" "${STUB_DIR}/git.log" +} + +@test "derives release/8.0 from version 8.0.0" { + export VERSION="8.0.0" + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + if [[ "$1" == "rev-list" ]]; then echo "abc123 parent1"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "8.0.0"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + grep -q "GIT: fetch origin release/8.0" "${STUB_DIR}/git.log" +} + +@test "fails on unparseable version" { + export VERSION="bad" + run bash "${SCRIPT}" + [ "$status" -eq 1 ] + [[ "$output" == *"Could not parse"* ]] +} + +# ── Already-applied detection ─────────────────────────────────────────────── + +@test "exits cleanly when commit is already applied" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + # git cherry: "-" prefix means patch is already applied. + if [[ "$1" == "cherry" ]]; then echo "- abc123def456"; exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [[ "$output" == *"already applied"* ]] + # Should NOT have attempted a cherry-pick. + ! grep -q "GIT: cherry-pick" "${STUB_DIR}/git.log" +} + +# ── Squash-merge detection (single parent) ────────────────────────────────── + +@test "does not use --mainline for squash merges" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + # Single parent: rev-list outputs "sha parent1" (2 fields → 1 parent). + if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [[ "$output" == *"single parent"* ]] + # cherry-pick should NOT include --mainline. + grep "GIT: cherry-pick" "${STUB_DIR}/git.log" | grep -qv "\-\-mainline" +} + +# ── True merge detection (multiple parents) ───────────────────────────────── + +@test "uses --mainline 1 for true merge commits" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + # Two parents: rev-list outputs "sha parent1 parent2" (3 fields → 2 parents). + if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1 parent2"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [[ "$output" == *"--mainline 1"* ]] +} + +# ── Milestone lookup ──────────────────────────────────────────────────────── + +@test "warns when milestone does not exist" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi + if [[ "$1" == "cherry-pick" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + if [[ "$1" == "config" ]]; then exit 0; fi + exit 0 + ' + # gh api returns a milestone that does NOT match VERSION (7.0.1). + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "6.0.0"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [[ "$output" == *"does not exist"* ]] +} + +# ── Conflict handling ─────────────────────────────────────────────────────── + +@test "creates CONFLICTS PR when cherry-pick fails" { + write_git_mock ' + if [[ "$1" == "fetch" ]]; then exit 0; fi + if [[ "$1" == "cherry" ]]; then echo "+ abc123"; exit 0; fi + if [[ "$1" == "checkout" ]]; then exit 0; fi + if [[ "$1" == "rev-list" ]]; then echo "abc123def456 parent1"; exit 0; fi + # cherry-pick fails with conflicts. + if [[ "$1" == "cherry-pick" ]]; then + if [[ "$2" == "--abort" ]]; then exit 0; fi + exit 1 + fi + if [[ "$1" == "commit" ]]; then exit 0; fi + if [[ "$1" == "push" ]]; then exit 0; fi + exit 0 + ' + write_gh_mock ' + if [[ "$1" == "api" ]]; then echo "7.0.1"; exit 0; fi + if [[ "$1" == "pr" ]]; then exit 0; fi + exit 0 + ' + + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [[ "$output" == *"failed due to conflicts"* ]] + # Should have called cherry-pick --abort. + grep -q "GIT: cherry-pick --abort" "${STUB_DIR}/git.log" + # Should have created an empty commit. + grep -q "GIT: commit --allow-empty" "${STUB_DIR}/git.log" +} diff --git a/.github/scripts/tests/extract-hotfix-versions.bats b/.github/scripts/tests/extract-hotfix-versions.bats new file mode 100644 index 0000000000..e0767ffd04 --- /dev/null +++ b/.github/scripts/tests/extract-hotfix-versions.bats @@ -0,0 +1,244 @@ +#!/usr/bin/env bats +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +# +# Tests for extract-hotfix-versions.sh +# +# Run with: bats .github/scripts/tests/extract-hotfix-versions.bats +# +# Dependencies: bats-core (https://github.com/bats-core/bats-core) +# +################################################################################# + +# Path to the script under test (relative to repo root). +SCRIPT=".github/scripts/extract-hotfix-versions.sh" + +# ── Helpers ────────────────────────────────────────────────────────────────── + +setup() { + # Create a temporary GITHUB_OUTPUT file for each test. + export GITHUB_OUTPUT + GITHUB_OUTPUT="$(mktemp)" + + # Defaults — individual tests override as needed. + export EVENT_ACTION="closed" + export EVENT_LABEL="" + export PR_NUMBER="42" + export GH_TOKEN="fake-token" + export GITHUB_REPOSITORY="dotnet/SqlClient" +} + +teardown() { + rm -f "${GITHUB_OUTPUT}" +} + +# Read the 'versions' output written to GITHUB_OUTPUT. +get_versions() { + grep '^versions=' "${GITHUB_OUTPUT}" | head -1 | cut -d= -f2- +} + +# ── --help flag ────────────────────────────────────────────────────────────── + +@test "prints help text with --help" { + run bash "${SCRIPT}" --help + [ "$status" -eq 0 ] + [[ "$output" == *"Parses"* ]] + [[ "$output" == *"REQUIRED ENVIRONMENT VARIABLES"* ]] +} + +@test "prints help text with -h" { + run bash "${SCRIPT}" -h + [ "$status" -eq 0 ] + [[ "$output" == *"Parses"* ]] +} + +# ── Input validation ───────────────────────────────────────────────────────── + +@test "fails when LABELS is unset" { + unset LABELS + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"LABELS"* ]] +} + +@test "fails when EVENT_ACTION is unset" { + export LABELS="Hotfix 7.0.1" + unset EVENT_ACTION + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"EVENT_ACTION"* ]] +} + +@test "fails when PR_NUMBER is unset" { + export LABELS="Hotfix 7.0.1" + unset PR_NUMBER + run bash "${SCRIPT}" + [ "$status" -ne 0 ] + [[ "$output" == *"PR_NUMBER"* ]] +} + +# ── Closed event: single label ────────────────────────────────────────────── + +@test "closed event: extracts single Hotfix label" { + export LABELS="Hotfix 7.0.1" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '["7.0.1"]' ] +} + +@test "closed event: ignores non-hotfix labels" { + export LABELS="bug,Hotfix 7.0.1,enhancement" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '["7.0.1"]' ] +} + +# ── Closed event: multiple labels ─────────────────────────────────────────── + +@test "closed event: extracts multiple Hotfix labels" { + export LABELS="Hotfix 7.0.1,Hotfix 8.0.0" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '["7.0.1","8.0.0"]' ] +} + +@test "closed event: extracts hotfix labels mixed with other labels" { + export LABELS="bug,Hotfix 7.0.1,enhancement,Hotfix 8.0.0,docs" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '["7.0.1","8.0.0"]' ] +} + +# ── Closed event: malformed labels ────────────────────────────────────────── + +@test "closed event: rejects malformed Hotfix labels (no patch)" { + export LABELS="Hotfix 7.0" + run bash "${SCRIPT}" + [ "$status" -eq 1 ] + [[ "$output" == *"No valid"* ]] +} + +@test "closed event: rejects Hotfix label with text suffix" { + export LABELS="Hotfix 7.0.1-beta" + run bash "${SCRIPT}" + [ "$status" -eq 1 ] + [[ "$output" == *"No valid"* ]] +} + +@test "closed event: rejects Hotfix label with non-numeric version" { + export LABELS="Hotfix abc" + run bash "${SCRIPT}" + [ "$status" -eq 1 ] + [[ "$output" == *"No valid"* ]] +} + +@test "closed event: fails when no labels present" { + export LABELS="" + run bash "${SCRIPT}" + [ "$status" -eq 1 ] +} + +# ── Labeled event: basic behavior ─────────────────────────────────────────── + +@test "labeled event: processes valid newly added label" { + export EVENT_ACTION="labeled" + export EVENT_LABEL="Hotfix 7.0.1" + export LABELS="Hotfix 7.0.1,Hotfix 8.0.0" + + # Mock gh to report no existing branch or PR. + # The script now uses 'gh api' for branch checks (no git checkout in this job). + local stub_dir + stub_dir="$(mktemp -d)" + cat > "${stub_dir}/gh" <<'STUB' +#!/usr/bin/env bash +# gh api repos/.../git/ref/heads/...: exit 1 (branch not found) +# gh pr list: return "0" PRs +if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then + exit 1 +fi +echo "0" +STUB + chmod +x "${stub_dir}/gh" + + export PATH="${stub_dir}:${PATH}" + run bash "${SCRIPT}" + rm -rf "${stub_dir}" + + [ "$status" -eq 0 ] + [ "$(get_versions)" = '["7.0.1"]' ] +} + +@test "labeled event: skips non-hotfix label" { + export EVENT_ACTION="labeled" + export EVENT_LABEL="bug" + export LABELS="bug,Hotfix 7.0.1" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '[]' ] +} + +@test "labeled event: skips malformed hotfix label" { + export EVENT_ACTION="labeled" + export EVENT_LABEL="Hotfix 7.0" + export LABELS="Hotfix 7.0" + run bash "${SCRIPT}" + [ "$status" -eq 0 ] + [ "$(get_versions)" = '[]' ] +} + +# ── Labeled event: duplicate detection ────────────────────────────────────── + +@test "labeled event: skips when cherry-pick branch already exists" { + export EVENT_ACTION="labeled" + export EVENT_LABEL="Hotfix 7.0.1" + export LABELS="Hotfix 7.0.1" + + local stub_dir + stub_dir="$(mktemp -d)" + # gh api returns success — branch exists on the remote. + cat > "${stub_dir}/gh" <<'STUB' +#!/usr/bin/env bash +if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then + exit 0 +fi +echo "0" +STUB + chmod +x "${stub_dir}/gh" + export PATH="${stub_dir}:${PATH}" + + run bash "${SCRIPT}" + rm -rf "${stub_dir}" + + [ "$status" -eq 0 ] + [ "$(get_versions)" = '[]' ] + [[ "$output" == *"already exists"* ]] +} + +@test "labeled event: skips when cherry-pick PR already exists" { + export EVENT_ACTION="labeled" + export EVENT_LABEL="Hotfix 7.0.1" + export LABELS="Hotfix 7.0.1" + + local stub_dir + stub_dir="$(mktemp -d)" + # gh api for branch check returns 1 (not found), but pr list returns 1 PR. + cat > "${stub_dir}/gh" <<'STUB' +#!/usr/bin/env bash +if [[ "$1" == "api" && "$2" == repos/*/git/ref/heads/* ]]; then + exit 1 +fi +echo "1" +STUB + chmod +x "${stub_dir}/gh" + export PATH="${stub_dir}:${PATH}" + + run bash "${SCRIPT}" + rm -rf "${stub_dir}" + + [ "$status" -eq 0 ] + [ "$(get_versions)" = '[]' ] + [[ "$output" == *"already exists"* ]] +} diff --git a/.github/workflows/cherry-pick-hotfix.yml b/.github/workflows/cherry-pick-hotfix.yml new file mode 100644 index 0000000000..9110a58fa2 --- /dev/null +++ b/.github/workflows/cherry-pick-hotfix.yml @@ -0,0 +1,113 @@ +################################################################################# +# Licensed to the .NET Foundation under one or more agreements. # +# The .NET Foundation licenses this file to you under the MIT license. # +# See the LICENSE file in the project root for more information. # +################################################################################# +# +# Cherry-pick Hotfix to Release Branch +# +# Automatically cherry-picks merged PRs into release branches when a +# "Hotfix " label is present. Supports multiple hotfix labels on +# a single PR — each one produces an independent cherry-pick PR targeting +# the corresponding release/ branch. +# +# Usage: +# 1. Merge a PR to the default branch. +# 2. Add a "Hotfix " label (e.g. "Hotfix 7.0.1") either before +# or after merging. +# 3. The workflow derives the release branch from the label's major.minor +# version (e.g. "Hotfix 7.0.1" → release/7.0) and creates a cherry-pick +# PR prefixed with "[ Cherry-pick]". +# 4. If the cherry-pick has conflicts, a placeholder PR is opened with +# "[ Cherry-pick - CONFLICTS]" and manual resolution steps. +# +################################################################################# + +name: Cherry-pick Hotfix to release branch + +# Triggers: +# - 'closed': fires at merge time — if a "Hotfix " label is already present, +# the cherry-pick runs immediately. +# - 'labeled': fires when a label is added after merge — allows retroactive cherry-picks +# by adding the label to an already-merged PR. +on: + pull_request_target: + types: [closed, labeled] + +# 'contents: write' is needed to push the cherry-pick branch. +# 'pull-requests: write' is needed to create the new PR via the GitHub CLI. +permissions: + contents: write + pull-requests: write + +jobs: + # First job: extract all hotfix versions from the PR labels and emit them as + # a JSON array so the matrix strategy can fan out one job per version. + detect-versions: + runs-on: ubuntu-latest + # Only fire for merged PRs targeting the default branch that have at least + # one "Hotfix *" label. The default-branch guard prevents recursive + # cherry-picks when a cherry-pick PR is merged into a release branch. + if: >- + github.event.pull_request.merged == true && + github.event.pull_request.base.ref == github.event.repository.default_branch && + join(github.event.pull_request.labels.*.name, ' ') != '' && + contains(join(github.event.pull_request.labels.*.name, ','), 'Hotfix ') + outputs: + versions: ${{ steps.extract.outputs.versions }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Only the scripts directory is needed; skip full history. + sparse-checkout: .github/scripts + sparse-checkout-cone-mode: false + + - name: Extract hotfix versions from labels + id: extract + env: + # Pass label names via env to avoid script injection from label text. + LABELS: ${{ join(github.event.pull_request.labels.*.name, ',') }} + # For the 'labeled' event, this is the single label that was just added. + # For the 'closed' event this is empty, meaning all labels are processed. + EVENT_LABEL: ${{ github.event.label.name || '' }} + EVENT_ACTION: ${{ github.event.action }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ github.event.pull_request.number }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/extract-hotfix-versions.sh" + + # Second job: runs once per detected version, cherry-picking the merge commit + # into each target release branch. + cherry-pick: + needs: detect-versions + if: needs.detect-versions.outputs.versions != '[]' + runs-on: ubuntu-latest + strategy: + # Don't cancel other cherry-picks if one version fails. + fail-fast: false + matrix: + version: ${{ fromJson(needs.detect-versions.outputs.versions) }} + name: Cherry-pick to release branch (${{ matrix.version }}) + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # Full history is required so the merge commit and target branch are available + # for the cherry-pick operation. + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Cherry-pick and create PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + MERGE_COMMIT_SHA: ${{ github.event.pull_request.merge_commit_sha }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + VERSION: ${{ matrix.version }} + run: bash "${GITHUB_WORKSPACE}/.github/scripts/cherry-pick-to-release.sh" diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 464c0401f0..c1e985ea03 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -23,6 +23,7 @@ on: - main - feat/** - dev/** + - release/** # Scan weekly on Saturdays at 23:33 UTC schedule: diff --git a/AGENTS.md b/AGENTS.md index 56d7555c99..690e9c9c8d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -54,6 +54,15 @@ This repository provides reusable prompts in `.github/prompts/` for common maint 6. **Performance Optimization**: Use pooling, async, efficient allocations 7. **Observability**: EventSource tracing, meaningful errors +## Branch Naming + +All branches created by AI agents **must** live under the `dev/automation/` prefix. Use a descriptive suffix, for example: + +- `dev/automation/fix-connection-timeout` +- `dev/automation/add-json-type-tests` + +Do **not** create branches directly under `main`, `dev/`, or any other top-level prefix. + ## Common Tasks ### Bug Fix Workflow diff --git a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml index d38fb14d57..98cac1bcdd 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-package-reference-pipeline.yml @@ -14,8 +14,8 @@ # It runs via CI push triggers and schedules and uses the Release build # configuration: # -# - Commits to GitHub main -# - Commits to ADO internal/main +# - Commits to GitHub main and release/* branches +# - Commits to ADO internal/main and internal/release/* branches # - Weekdays at 03:00 UTC on GitHub main # - Thursdays at 07:00 UTC on ADO internal/main # @@ -55,13 +55,15 @@ trigger: branches: include: - # GitHub main branch. - - main + # GitHub main and release branches. + - main + - release/* - # ADO internal/main branch. - - internal/main + # ADO main and release branches. + - internal/main + - internal/release/* -# Trigger this pipline on a schedule. +# Trigger this pipeline on a schedule. schedules: # GitHub main on weekdays diff --git a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml index 7fef476e59..7068c9693c 100644 --- a/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml +++ b/eng/pipelines/dotnet-sqlclient-ci-project-reference-pipeline.yml @@ -14,8 +14,8 @@ # It runs via CI push triggers and schedules and uses the Release build # configuration: # -# - Commits to GitHub main -# - Commits to ADO internal/main +# - Commits to GitHub main and release/* branches +# - Commits to ADO internal/main and internal/release/* branches # - Weekdays at 01:00 UTC on GitHub main # - Thursdays at 05:00 UTC on ADO internal/main # @@ -55,13 +55,15 @@ trigger: branches: include: - # GitHub main branch. - - main + # GitHub main and release branches. + - main + - release/* - # ADO internal/main branch. - - internal/main + # ADO main and release branches. + - internal/main + - internal/release/* -# Trigger this pipline on a schedule. +# Trigger this pipeline on a schedule. schedules: # GitHub main on weekdays diff --git a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml index c454da710a..32921a733e 100644 --- a/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-package-ref-pipeline.yml @@ -11,8 +11,9 @@ # - Microsoft.Data.SqlClient # - Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider # -# It is triggered by pushes to PRs that target dev/ and feature/ branches, and -# the main branch in GitHub. +# It is triggered by pushes to PRs that target the main, dev/*, feat/*, and +# release/* branches in GitHub. The dev/* pattern includes dev/automation/* +# branches created by AI agents. # # It maps to the "PR-SqlClient-Package" pipeline in the Public project: # @@ -33,6 +34,7 @@ pr: - dev/* - feat/* - main + - release/* paths: include: diff --git a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml index 539d03fe82..738f8c1b6a 100644 --- a/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml +++ b/eng/pipelines/sqlclient-pr-project-ref-pipeline.yml @@ -11,8 +11,9 @@ # - Microsoft.Data.SqlClient # - Microsoft.Data.SqlClient.AlwaysEncrypted.AzureKeyVaultProvider # -# It is triggered by pushes to PRs that target dev/ and feature/ branches, and -# the main branch in GitHub. +# It is triggered by pushes to PRs that target the main, dev/*, feat/*, and +# release/* branches in GitHub. The dev/* pattern includes dev/automation/* +# branches created by AI agents. # # It maps to the "PR-SqlClient-Project" pipeline in the Public project: # @@ -33,6 +34,7 @@ pr: - dev/* - feat/* - main + - release/* paths: include: