Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`).
Comment thread
paulmedynski marked this conversation as resolved.
- 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.
Expand Down
202 changes: 202 additions & 0 deletions .github/scripts/cherry-pick-to-release.sh
Original file line number Diff line number Diff line change
@@ -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 <upstream> <head> <limit>' lists commits in <limit>..<head> and
# marks each with '-' (patch already on <upstream>) or '+' (not yet applied).
# By passing <head>=MERGE_COMMIT_SHA and <limit>=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
Comment thread
paulmedynski marked this conversation as resolved.

# 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 <sha>' outputs: <sha> <parent1> [<parent2> ...]
# 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 ---
Comment thread
paulmedynski marked this conversation as resolved.
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
Comment thread
paulmedynski marked this conversation as resolved.
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
136 changes: 136 additions & 0 deletions .github/scripts/extract-hotfix-versions.sh
Original file line number Diff line number Diff line change
@@ -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 <major>.<minor>.<patch>"
# (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=<JSON array> 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}"

Comment thread
paulmedynski marked this conversation as resolved.
# -- '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

Comment thread
paulmedynski marked this conversation as resolved.
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
Comment thread
paulmedynski marked this conversation as resolved.
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 <major>.<minor>.<patch>'."
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}"
Loading
Loading