Deploy convention at M.Video: a release-* branch push deploys to stage and a v* tag push deploys to PROD. The old guard only matched release/* (slash) and never caught tag pushes, so a prod deploy could run unconfirmed. - permission-gate: match main/master, release-[-/], --tags/--follow-tags, and whitespace-preceded v<digit> tags; false-positive-safe (space discriminator) - create-mr.sh: refuse source branch release-* (not just release/*) - jira-workflow doc updated to release-* Verified with a behavioral test suite (10 cases incl. v2, false-positive checks).
113 lines
3.8 KiB
Bash
Executable File
113 lines
3.8 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# create-mr.sh "<title>" [target-branch] [source-branch] [jira-key]
|
|
#
|
|
# Creates a GitLab merge request for the current repository via the GitLab API.
|
|
# Host and project path are derived from `git remote origin` (or GITLAB_HOST),
|
|
# so no URLs are hardcoded. If a Jira key is given and TRACKER_URL/JIRA_TOKEN are
|
|
# set, the MR link is also attached to the ticket via link-mr.sh.
|
|
#
|
|
# Env:
|
|
# GITLAB_TOKEN (required) GitLab personal/project access token with api scope
|
|
# GITLAB_HOST (optional) override host, e.g. git.codelab.vc
|
|
# MR_PUSH (optional) 0 to skip pushing the source branch (default: push)
|
|
# MR_DRY_RUN (optional) 1 to print the request and exit without calling the API
|
|
#
|
|
# Examples:
|
|
# ./create-mr.sh "Fix PROJ-123: null pointer" main
|
|
# ./create-mr.sh "Add feature X" main feature/PROJ-42-x PROJ-42
|
|
set -euo pipefail
|
|
|
|
TITLE="${1:?usage: create-mr.sh \"<title>\" [target-branch] [source-branch] [jira-key]}"
|
|
TARGET="${2:-main}"
|
|
SOURCE="${3:-$(git rev-parse --abbrev-ref HEAD)}"
|
|
JIRA_KEY="${4:-}"
|
|
|
|
: "${GITLAB_TOKEN:?set GITLAB_TOKEN (GitLab access token with api scope)}"
|
|
|
|
# --- Refuse to open an MR *from* a protected branch (safety). ----------------
|
|
case "$SOURCE" in
|
|
main | master | release-* | release/*)
|
|
echo "Refusing: source branch '$SOURCE' looks protected/release. Create a feature branch first." >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
# --- Derive host and project path from the origin remote. -------------------
|
|
URL="$(git config --get remote.origin.url)"
|
|
URL="${URL%.git}"
|
|
case "$URL" in
|
|
*://*) # scheme://[user@]host[:port]/path
|
|
REST="${URL#*://}"
|
|
REST="${REST#*@}"
|
|
HOST_PORT="${REST%%/*}"
|
|
PATH_NS="${REST#*/}"
|
|
HOST="${HOST_PORT%%:*}"
|
|
;;
|
|
*@*:*) # scp-like: user@host:path
|
|
REST="${URL#*@}"
|
|
HOST="${REST%%:*}"
|
|
PATH_NS="${REST#*:}"
|
|
;;
|
|
*)
|
|
echo "Cannot parse origin remote URL: $URL" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
|
|
HOST="${GITLAB_HOST:-$HOST}"
|
|
if [ -z "$HOST" ] || [ -z "$PATH_NS" ]; then
|
|
echo "Could not determine GitLab host/project from origin ($URL). Set GITLAB_HOST." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# URL-encode the project path (only '/' needs encoding for a namespaced path).
|
|
PROJECT_ENC="${PATH_NS//\//%2F}"
|
|
API="https://${HOST}/api/v4/projects/${PROJECT_ENC}/merge_requests"
|
|
|
|
# --- Build request body (jq escapes; fall back to naive JSON). --------------
|
|
if command -v jq >/dev/null 2>&1; then
|
|
BODY="$(jq -n \
|
|
--arg s "$SOURCE" --arg t "$TARGET" --arg title "$TITLE" \
|
|
'{source_branch:$s, target_branch:$t, title:$title, remove_source_branch:true, squash:true}')"
|
|
else
|
|
BODY="{\"source_branch\":\"${SOURCE}\",\"target_branch\":\"${TARGET}\",\"title\":\"${TITLE//\"/\\\"}\",\"remove_source_branch\":true,\"squash\":true}"
|
|
fi
|
|
|
|
if [ "${MR_DRY_RUN:-0}" = "1" ]; then
|
|
echo "DRY RUN"
|
|
echo "POST ${API}"
|
|
echo "body: ${BODY}"
|
|
echo "would push: $([ "${MR_PUSH:-1}" = "1" ] && echo "git push -u origin ${SOURCE}" || echo "(skipped)")"
|
|
exit 0
|
|
fi
|
|
|
|
# --- Ensure the source branch exists on the remote. -------------------------
|
|
if [ "${MR_PUSH:-1}" = "1" ]; then
|
|
git push -u origin "$SOURCE"
|
|
fi
|
|
|
|
# --- Create the MR. ---------------------------------------------------------
|
|
RESPONSE="$(curl -fsS -X POST \
|
|
-H "PRIVATE-TOKEN: ${GITLAB_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$BODY" "$API")"
|
|
|
|
if command -v jq >/dev/null 2>&1; then
|
|
WEB_URL="$(printf '%s' "$RESPONSE" | jq -r '.web_url // empty')"
|
|
else
|
|
WEB_URL="$(printf '%s' "$RESPONSE" | sed -n 's/.*"web_url":"\([^"]*\)".*/\1/p' | head -1)"
|
|
fi
|
|
|
|
if [ -z "$WEB_URL" ]; then
|
|
echo "MR request sent, but could not parse web_url from response:" >&2
|
|
echo "$RESPONSE" >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "MR created: $WEB_URL"
|
|
|
|
# --- Optionally attach the MR link to the Jira ticket. ----------------------
|
|
if [ -n "$JIRA_KEY" ] && [ -n "${TRACKER_URL:-}" ] && [ -n "${JIRA_TOKEN:-}" ]; then
|
|
"$(dirname "$0")/link-mr.sh" "$JIRA_KEY" "$WEB_URL" "$TITLE"
|
|
fi
|