chore: sync local Mosaic changes
This commit is contained in:
247
rails/git/ci-queue-wait.ps1
Normal file
247
rails/git/ci-queue-wait.ps1
Normal file
@@ -0,0 +1,247 @@
|
||||
# ci-queue-wait.ps1 - Wait until project CI queue is clear (no running/queued pipeline on branch head)
|
||||
# Usage: .\ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose merge] [-RequireStatus]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Alias("B")]
|
||||
[string]$Branch = "main",
|
||||
|
||||
[Alias("t")]
|
||||
[int]$TimeoutSeconds = 900,
|
||||
|
||||
[Alias("i")]
|
||||
[int]$IntervalSeconds = 15,
|
||||
|
||||
[ValidateSet("push", "merge")]
|
||||
[string]$Purpose = "merge",
|
||||
|
||||
[switch]$RequireStatus,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
|
||||
. "$ScriptDir\detect-platform.ps1"
|
||||
|
||||
function Show-Usage {
|
||||
@"
|
||||
Usage: ci-queue-wait.ps1 [-Branch main] [-TimeoutSeconds 900] [-IntervalSeconds 15] [-Purpose push|merge] [-RequireStatus]
|
||||
|
||||
Options:
|
||||
-Branch, -B BRANCH Branch head to inspect (default: main)
|
||||
-TimeoutSeconds, -t SECONDS Max wait time (default: 900)
|
||||
-IntervalSeconds, -i SECONDS Poll interval (default: 15)
|
||||
-Purpose VALUE push or merge (default: merge)
|
||||
-RequireStatus Fail if no CI status contexts are present
|
||||
-Help, -h Show help
|
||||
"@
|
||||
}
|
||||
|
||||
if ($Help) {
|
||||
Show-Usage
|
||||
exit 0
|
||||
}
|
||||
|
||||
if ($TimeoutSeconds -lt 1 -or $IntervalSeconds -lt 1) {
|
||||
Write-Error "TimeoutSeconds and IntervalSeconds must be positive integers."
|
||||
exit 1
|
||||
}
|
||||
|
||||
function Get-RemoteHost {
|
||||
$remoteUrl = git remote get-url origin 2>$null
|
||||
if ([string]::IsNullOrEmpty($remoteUrl)) { return $null }
|
||||
if ($remoteUrl -match "^https?://([^/]+)/") { return $Matches[1] }
|
||||
if ($remoteUrl -match "^git@([^:]+):") { return $Matches[1] }
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-GiteaToken {
|
||||
param([string]$Host)
|
||||
|
||||
if ($env:GITEA_TOKEN) { return $env:GITEA_TOKEN }
|
||||
|
||||
$credPath = Join-Path $HOME ".git-credentials"
|
||||
if (-not (Test-Path $credPath)) { return $null }
|
||||
|
||||
$line = Get-Content $credPath | Where-Object { $_ -like "*$Host*" } | Select-Object -First 1
|
||||
if (-not $line) { return $null }
|
||||
|
||||
if ($line -match 'https?://[^@]*:([^@/]+)@') {
|
||||
return $Matches[1]
|
||||
}
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-QueueState {
|
||||
param([object]$Payload)
|
||||
|
||||
$pending = @("pending", "queued", "running", "waiting")
|
||||
$failure = @("failure", "error", "failed")
|
||||
$success = @("success")
|
||||
|
||||
$state = ""
|
||||
if ($null -ne $Payload.state) {
|
||||
$state = "$($Payload.state)".ToLowerInvariant()
|
||||
}
|
||||
|
||||
if ($pending -contains $state) { return "pending" }
|
||||
if ($failure -contains $state) { return "terminal-failure" }
|
||||
if ($success -contains $state) { return "terminal-success" }
|
||||
|
||||
$values = @()
|
||||
$statuses = @()
|
||||
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
||||
|
||||
foreach ($s in $statuses) {
|
||||
if ($null -eq $s) { continue }
|
||||
$v = ""
|
||||
if ($null -ne $s.status) { $v = "$($s.status)".ToLowerInvariant() }
|
||||
elseif ($null -ne $s.state) { $v = "$($s.state)".ToLowerInvariant() }
|
||||
if (-not [string]::IsNullOrEmpty($v)) { $values += $v }
|
||||
}
|
||||
|
||||
if ($values.Count -eq 0 -and [string]::IsNullOrEmpty($state)) { return "no-status" }
|
||||
if (($values | Where-Object { $pending -contains $_ }).Count -gt 0) { return "pending" }
|
||||
if (($values | Where-Object { $failure -contains $_ }).Count -gt 0) { return "terminal-failure" }
|
||||
if ($values.Count -gt 0 -and ($values | Where-Object { -not ($success -contains $_) }).Count -eq 0) { return "terminal-success" }
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
function Print-PendingContexts {
|
||||
param([object]$Payload)
|
||||
|
||||
$pending = @("pending", "queued", "running", "waiting")
|
||||
$statuses = @()
|
||||
if ($null -ne $Payload.statuses) { $statuses = @($Payload.statuses) }
|
||||
|
||||
if ($statuses.Count -eq 0) {
|
||||
Write-Host "[ci-queue-wait] no status contexts reported"
|
||||
return
|
||||
}
|
||||
|
||||
$found = $false
|
||||
foreach ($s in $statuses) {
|
||||
if ($null -eq $s) { continue }
|
||||
$name = if ($s.context) { $s.context } elseif ($s.name) { $s.name } else { "unknown-context" }
|
||||
$value = if ($s.status) { "$($s.status)".ToLowerInvariant() } elseif ($s.state) { "$($s.state)".ToLowerInvariant() } else { "unknown" }
|
||||
$target = if ($s.target_url) { $s.target_url } elseif ($s.url) { $s.url } else { "" }
|
||||
if ($pending -contains $value) {
|
||||
$found = $true
|
||||
if ($target) {
|
||||
Write-Host "[ci-queue-wait] pending: $name=$value ($target)"
|
||||
}
|
||||
else {
|
||||
Write-Host "[ci-queue-wait] pending: $name=$value"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (-not $found) {
|
||||
Write-Host "[ci-queue-wait] no pending contexts"
|
||||
}
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
$owner = Get-GitRepoOwner
|
||||
$repo = Get-GitRepoName
|
||||
|
||||
if ([string]::IsNullOrEmpty($owner) -or [string]::IsNullOrEmpty($repo)) {
|
||||
Write-Error "Could not determine repository owner/name from git remote."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$headSha = $null
|
||||
$host = $null
|
||||
$giteaToken = $null
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
if (-not (Get-Command gh -ErrorAction SilentlyContinue)) {
|
||||
Write-Error "gh CLI is required for GitHub CI queue guard."
|
||||
exit 1
|
||||
}
|
||||
$headSha = (& gh api "repos/$owner/$repo/branches/$Branch" --jq ".commit.sha").Trim()
|
||||
if ([string]::IsNullOrEmpty($headSha)) {
|
||||
Write-Error "Could not resolve $Branch head SHA."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] platform=github purpose=$Purpose branch=$Branch sha=$headSha"
|
||||
}
|
||||
"gitea" {
|
||||
$host = Get-RemoteHost
|
||||
if ([string]::IsNullOrEmpty($host)) {
|
||||
Write-Error "Could not determine remote host."
|
||||
exit 1
|
||||
}
|
||||
$giteaToken = Get-GiteaToken -Host $host
|
||||
if ([string]::IsNullOrEmpty($giteaToken)) {
|
||||
Write-Error "Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials."
|
||||
exit 1
|
||||
}
|
||||
try {
|
||||
$branchUrl = "https://$host/api/v1/repos/$owner/$repo/branches/$Branch"
|
||||
$branchPayload = Invoke-RestMethod -Method Get -Uri $branchUrl -Headers @{ Authorization = "token $giteaToken" }
|
||||
$headSha = ($branchPayload.commit.id | Out-String).Trim()
|
||||
}
|
||||
catch {
|
||||
Write-Error "Could not resolve $Branch head SHA from Gitea API."
|
||||
exit 1
|
||||
}
|
||||
if ([string]::IsNullOrEmpty($headSha)) {
|
||||
Write-Error "Could not resolve $Branch head SHA."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] platform=gitea purpose=$Purpose branch=$Branch sha=$headSha"
|
||||
}
|
||||
default {
|
||||
Write-Error "Unsupported platform '$platform'."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
|
||||
|
||||
while ($true) {
|
||||
if ((Get-Date) -gt $deadline) {
|
||||
Write-Error "Timed out waiting for CI queue to clear on $Branch after ${TimeoutSeconds}s."
|
||||
exit 124
|
||||
}
|
||||
|
||||
try {
|
||||
if ($platform -eq "github") {
|
||||
$statusJson = & gh api "repos/$owner/$repo/commits/$headSha/status"
|
||||
$payload = $statusJson | ConvertFrom-Json
|
||||
}
|
||||
else {
|
||||
$statusUrl = "https://$host/api/v1/repos/$owner/$repo/commits/$headSha/status"
|
||||
$payload = Invoke-RestMethod -Method Get -Uri $statusUrl -Headers @{ Authorization = "token $giteaToken" }
|
||||
}
|
||||
}
|
||||
catch {
|
||||
Write-Error "Failed to query commit status for queue guard."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$state = Get-QueueState -Payload $payload
|
||||
Write-Host "[ci-queue-wait] state=$state purpose=$Purpose branch=$Branch"
|
||||
|
||||
switch ($state) {
|
||||
"pending" {
|
||||
Print-PendingContexts -Payload $payload
|
||||
Start-Sleep -Seconds $IntervalSeconds
|
||||
}
|
||||
"no-status" {
|
||||
if ($RequireStatus) {
|
||||
Write-Error "No CI status contexts found while -RequireStatus is set."
|
||||
exit 1
|
||||
}
|
||||
Write-Host "[ci-queue-wait] no status contexts present; proceeding."
|
||||
exit 0
|
||||
}
|
||||
"terminal-success" { exit 0 }
|
||||
"terminal-failure" { exit 0 }
|
||||
"unknown" { exit 0 }
|
||||
default { exit 0 }
|
||||
}
|
||||
}
|
||||
307
rails/git/ci-queue-wait.sh
Executable file
307
rails/git/ci-queue-wait.sh
Executable file
@@ -0,0 +1,307 @@
|
||||
#!/bin/bash
|
||||
# ci-queue-wait.sh - Wait until project CI queue is clear (no running/queued pipeline on branch head)
|
||||
# Usage: ci-queue-wait.sh [-B branch] [-t timeout_sec] [-i interval_sec] [--purpose push|merge] [--require-status]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
BRANCH="main"
|
||||
TIMEOUT_SEC=900
|
||||
INTERVAL_SEC=15
|
||||
PURPOSE="merge"
|
||||
REQUIRE_STATUS=0
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [-B branch] [-t timeout_sec] [-i interval_sec] [--purpose push|merge] [--require-status]
|
||||
|
||||
Options:
|
||||
-B, --branch BRANCH Branch head to inspect (default: main)
|
||||
-t, --timeout SECONDS Max wait time in seconds (default: 900)
|
||||
-i, --interval SECONDS Poll interval in seconds (default: 15)
|
||||
--purpose VALUE Log context: push|merge (default: merge)
|
||||
--require-status Fail if no CI status contexts are present
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --purpose push -B main -t 600 -i 10
|
||||
EOF
|
||||
}
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
state = (payload.get("state") or "").lower()
|
||||
|
||||
pending_values = {"pending", "queued", "running", "waiting"}
|
||||
failure_values = {"failure", "error", "failed"}
|
||||
success_values = {"success"}
|
||||
|
||||
if state in pending_values:
|
||||
print("pending")
|
||||
raise SystemExit(0)
|
||||
if state in failure_values:
|
||||
print("terminal-failure")
|
||||
raise SystemExit(0)
|
||||
if state in success_values:
|
||||
print("terminal-success")
|
||||
raise SystemExit(0)
|
||||
|
||||
values = []
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
value = (item.get("status") or item.get("state") or "").lower()
|
||||
if value:
|
||||
values.append(value)
|
||||
|
||||
if not values and not state:
|
||||
print("no-status")
|
||||
elif any(v in pending_values for v in values):
|
||||
print("pending")
|
||||
elif any(v in failure_values for v in values):
|
||||
print("terminal-failure")
|
||||
elif values and all(v in success_values for v in values):
|
||||
print("terminal-success")
|
||||
else:
|
||||
print("unknown")
|
||||
PY
|
||||
}
|
||||
|
||||
print_pending_contexts() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("[ci-queue-wait] unable to decode status payload")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
if not statuses:
|
||||
print("[ci-queue-wait] no status contexts reported")
|
||||
raise SystemExit(0)
|
||||
|
||||
pending_values = {"pending", "queued", "running", "waiting"}
|
||||
found = False
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("context") or item.get("name") or "unknown-context"
|
||||
value = (item.get("status") or item.get("state") or "unknown").lower()
|
||||
target = item.get("target_url") or item.get("url") or ""
|
||||
if value in pending_values:
|
||||
found = True
|
||||
if target:
|
||||
print(f"[ci-queue-wait] pending: {name}={value} ({target})")
|
||||
else:
|
||||
print(f"[ci-queue-wait] pending: {name}={value}")
|
||||
if not found:
|
||||
print("[ci-queue-wait] no pending contexts")
|
||||
PY
|
||||
}
|
||||
|
||||
github_get_branch_head_sha() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local branch="$3"
|
||||
gh api "repos/${owner}/${repo}/branches/${branch}" --jq '.commit.sha'
|
||||
}
|
||||
|
||||
github_get_commit_status_json() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
gh api "repos/${owner}/${repo}/commits/${sha}/status"
|
||||
}
|
||||
|
||||
gitea_get_branch_head_sha() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local branch="$3"
|
||||
local token="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/branches/${branch}"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
commit = data.get("commit") or {}
|
||||
print((commit.get("id") or "").strip())
|
||||
'
|
||||
}
|
||||
|
||||
gitea_get_commit_status_json() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
local token="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-B|--branch)
|
||||
BRANCH="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--timeout)
|
||||
TIMEOUT_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interval)
|
||||
INTERVAL_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
--purpose)
|
||||
PURPOSE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--require-status)
|
||||
REQUIRE_STATUS=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: timeout and interval must be integer seconds." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
detect_platform > /dev/null
|
||||
PLATFORM="${PLATFORM:-unknown}"
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh CLI is required for GitHub CI queue guard." >&2
|
||||
exit 1
|
||||
fi
|
||||
HEAD_SHA=$(github_get_branch_head_sha "$OWNER" "$REPO" "$BRANCH")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve ${BRANCH} head SHA." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] platform=github purpose=${PURPOSE} branch=${BRANCH} sha=${HEAD_SHA}"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
HOST=$(get_remote_host) || {
|
||||
echo "Error: Could not determine remote host." >&2
|
||||
exit 1
|
||||
}
|
||||
TOKEN=$(get_gitea_token "$HOST") || {
|
||||
echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2
|
||||
exit 1
|
||||
}
|
||||
HEAD_SHA=$(gitea_get_branch_head_sha "$HOST" "$OWNER/$REPO" "$BRANCH" "$TOKEN")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve ${BRANCH} head SHA." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] platform=gitea purpose=${PURPOSE} branch=${BRANCH} sha=${HEAD_SHA}"
|
||||
else
|
||||
echo "Error: Unsupported platform '${PLATFORM}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
START_TS=$(date +%s)
|
||||
DEADLINE_TS=$((START_TS + TIMEOUT_SEC))
|
||||
|
||||
while true; do
|
||||
NOW_TS=$(date +%s)
|
||||
if (( NOW_TS > DEADLINE_TS )); then
|
||||
echo "Error: Timed out waiting for CI queue to clear on ${BRANCH} after ${TIMEOUT_SEC}s." >&2
|
||||
exit 124
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA")
|
||||
else
|
||||
STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$HEAD_SHA" "$TOKEN")
|
||||
fi
|
||||
|
||||
STATE=$(printf '%s' "$STATUS_JSON" | get_state_from_status_json)
|
||||
echo "[ci-queue-wait] state=${STATE} purpose=${PURPOSE} branch=${BRANCH}"
|
||||
|
||||
case "$STATE" in
|
||||
pending)
|
||||
printf '%s' "$STATUS_JSON" | print_pending_contexts
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
no-status)
|
||||
if [[ "$REQUIRE_STATUS" -eq 1 ]]; then
|
||||
echo "Error: No CI status contexts found for ${BRANCH} while --require-status is set." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[ci-queue-wait] no status contexts present; proceeding."
|
||||
exit 0
|
||||
;;
|
||||
terminal-success|terminal-failure|unknown)
|
||||
# Queue guard only blocks on pending/running/queued states.
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "[ci-queue-wait] unrecognized state '${STATE}', proceeding conservatively."
|
||||
exit 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
@@ -13,6 +13,80 @@ BODY=""
|
||||
LABELS=""
|
||||
MILESTONE=""
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_create_api() {
|
||||
local host repo token url payload
|
||||
host=$(get_remote_host) || {
|
||||
echo "Error: could not determine remote host for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
repo=$(get_repo_info) || {
|
||||
echo "Error: could not determine repo owner/name for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
token=$(get_gitea_token "$host") || {
|
||||
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
if [[ -n "$LABELS" || -n "$MILESTONE" ]]; then
|
||||
echo "Warning: API fallback currently applies title/body only; labels/milestone require authenticated tea setup." >&2
|
||||
fi
|
||||
|
||||
payload=$(TITLE="$TITLE" BODY="$BODY" python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
payload = {"title": os.environ["TITLE"]}
|
||||
body = os.environ.get("BODY", "")
|
||||
if body:
|
||||
payload["body"] = body
|
||||
print(json.dumps(payload))
|
||||
PY
|
||||
)
|
||||
|
||||
url="https://${host}/api/v1/repos/${repo}/issues"
|
||||
curl -fsS -X POST \
|
||||
-H "Authorization: token ${token}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$payload" \
|
||||
"$url"
|
||||
}
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [OPTIONS]
|
||||
@@ -78,12 +152,18 @@ case "$PLATFORM" in
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
CMD="tea issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --labels \"$LABELS\""
|
||||
# tea accepts milestone by name directly (verified 2026-02-05)
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
eval "$CMD"
|
||||
if command -v tea >/dev/null 2>&1; then
|
||||
CMD="tea issue create --title \"$TITLE\""
|
||||
[[ -n "$BODY" ]] && CMD="$CMD --description \"$BODY\""
|
||||
[[ -n "$LABELS" ]] && CMD="$CMD --labels \"$LABELS\""
|
||||
# tea accepts milestone by name directly (verified 2026-02-05)
|
||||
[[ -n "$MILESTONE" ]] && CMD="$CMD --milestone \"$MILESTONE\""
|
||||
if eval "$CMD"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Warning: tea issue create failed, trying Gitea API fallback..." >&2
|
||||
fi
|
||||
gitea_issue_create_api
|
||||
;;
|
||||
*)
|
||||
echo "Error: Could not detect git platform" >&2
|
||||
|
||||
@@ -10,6 +10,64 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
# Parse arguments
|
||||
ISSUE_NUMBER=""
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
gitea_issue_view_api() {
|
||||
local host repo token url
|
||||
host=$(get_remote_host) || {
|
||||
echo "Error: could not determine remote host for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
repo=$(get_repo_info) || {
|
||||
echo "Error: could not determine repo owner/name for API fallback" >&2
|
||||
return 1
|
||||
}
|
||||
token=$(get_gitea_token "$host") || {
|
||||
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
url="https://${host}/api/v1/repos/${repo}/issues/${ISSUE_NUMBER}"
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -m json.tool
|
||||
else
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
fi
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
-i|--issue)
|
||||
@@ -41,7 +99,13 @@ detect_platform
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue view "$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
tea issue "$ISSUE_NUMBER"
|
||||
if command -v tea >/dev/null 2>&1; then
|
||||
if tea issue "$ISSUE_NUMBER"; then
|
||||
exit 0
|
||||
fi
|
||||
echo "Warning: tea issue view failed, trying Gitea API fallback..." >&2
|
||||
fi
|
||||
gitea_issue_view_api
|
||||
else
|
||||
echo "Error: Unknown platform"
|
||||
exit 1
|
||||
|
||||
@@ -28,12 +28,12 @@ Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-release: 0.X.0 for breaking changes, 0.X.Y for patches
|
||||
- Post-release: X.0.0 for breaking changes
|
||||
- MVP starts at 0.1.0
|
||||
- Pre-MVP milestones MUST use 0.0.x and MUST start at 0.0.1
|
||||
- 0.1.0 is reserved for MVP release
|
||||
- After MVP, continue semantic progression (0.1.x, 0.2.x, ...)
|
||||
|
||||
Options:
|
||||
-Title, -t TITLE Milestone title/version (e.g., "0.2.0")
|
||||
-Title, -t TITLE Milestone title/version (e.g., "0.0.1")
|
||||
-Description, -d DESC Milestone description
|
||||
-Due DATE Due date (YYYY-MM-DD format)
|
||||
-List List existing milestones
|
||||
@@ -41,8 +41,8 @@ Options:
|
||||
|
||||
Examples:
|
||||
.\milestone-create.ps1 -List
|
||||
.\milestone-create.ps1 -t "0.1.0" -d "MVP Release"
|
||||
.\milestone-create.ps1 -t "0.2.0" -d "User Authentication Feature" -Due "2025-03-01"
|
||||
.\milestone-create.ps1 -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
||||
.\milestone-create.ps1 -t "0.1.0" -d "MVP Release" -Due "2025-03-01"
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
|
||||
@@ -21,12 +21,12 @@ Create or list milestones on the current repository (Gitea or GitHub).
|
||||
|
||||
Versioning Convention:
|
||||
- Features get dedicated milestones
|
||||
- Pre-release: 0.X.0 for breaking changes, 0.X.Y for patches
|
||||
- Post-release: X.0.0 for breaking changes
|
||||
- MVP starts at 0.1.0
|
||||
- Pre-MVP milestones MUST use 0.0.x and MUST start at 0.0.1
|
||||
- 0.1.0 is reserved for MVP release
|
||||
- After MVP, continue semantic progression (0.1.x, 0.2.x, ...)
|
||||
|
||||
Options:
|
||||
-t, --title TITLE Milestone title/version (e.g., "0.2.0")
|
||||
-t, --title TITLE Milestone title/version (e.g., "0.0.1")
|
||||
-d, --desc DESCRIPTION Milestone description
|
||||
--due DATE Due date (YYYY-MM-DD format)
|
||||
--list List existing milestones
|
||||
@@ -34,8 +34,8 @@ Options:
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --list
|
||||
$(basename "$0") -t "0.1.0" -d "MVP Release"
|
||||
$(basename "$0") -t "0.2.0" -d "User Authentication Feature" --due "2025-03-01"
|
||||
$(basename "$0") -t "0.0.1" -d "Pre-MVP Foundation Sprint"
|
||||
$(basename "$0") -t "0.1.0" -d "MVP Release" --due "2025-03-01"
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
273
rails/git/pr-ci-wait.sh
Executable file
273
rails/git/pr-ci-wait.sh
Executable file
@@ -0,0 +1,273 @@
|
||||
#!/bin/bash
|
||||
# pr-ci-wait.sh - Wait for PR CI status to reach terminal state (GitHub/Gitea)
|
||||
# Usage: pr-ci-wait.sh -n <pr_number> [-t timeout_sec] [-i interval_sec]
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
PR_NUMBER=""
|
||||
TIMEOUT_SEC=1800
|
||||
INTERVAL_SEC=15
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") -n <pr_number> [-t timeout_sec] [-i interval_sec]
|
||||
|
||||
Options:
|
||||
-n, --number NUMBER PR number (required)
|
||||
-t, --timeout SECONDS Max wait time in seconds (default: 1800)
|
||||
-i, --interval SECONDS Poll interval in seconds (default: 15)
|
||||
-h, --help Show this help
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -n 643
|
||||
$(basename "$0") -n 643 -t 900 -i 10
|
||||
EOF
|
||||
}
|
||||
|
||||
get_remote_host() {
|
||||
local remote_url
|
||||
remote_url=$(git remote get-url origin 2>/dev/null || true)
|
||||
if [[ -z "$remote_url" ]]; then
|
||||
return 1
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^https?://([^/]+)/ ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
if [[ "$remote_url" =~ ^git@([^:]+): ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
get_gitea_token() {
|
||||
local host="$1"
|
||||
if [[ -n "${GITEA_TOKEN:-}" ]]; then
|
||||
echo "$GITEA_TOKEN"
|
||||
return 0
|
||||
fi
|
||||
|
||||
local creds="$HOME/.git-credentials"
|
||||
if [[ -f "$creds" ]]; then
|
||||
local token
|
||||
token=$(grep -F "$host" "$creds" 2>/dev/null | sed -n 's#https\?://[^@]*:\([^@/]*\)@.*#\1#p' | head -n 1)
|
||||
if [[ -n "$token" ]]; then
|
||||
echo "$token"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
extract_state_from_status_json() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("unknown")
|
||||
raise SystemExit(0)
|
||||
|
||||
state = (payload.get("state") or "").lower()
|
||||
if state in {"success", "pending", "failure", "error"}:
|
||||
print(state)
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
values = []
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
value = (item.get("status") or item.get("state") or "").lower()
|
||||
if value:
|
||||
values.append(value)
|
||||
|
||||
if any(v in {"failure", "error"} for v in values):
|
||||
print("failure")
|
||||
elif values and all(v == "success" for v in values):
|
||||
print("success")
|
||||
elif any(v in {"pending", "running", "queued", "waiting"} for v in values):
|
||||
print("pending")
|
||||
else:
|
||||
print("unknown")
|
||||
PY
|
||||
}
|
||||
|
||||
print_status_summary() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import sys
|
||||
|
||||
try:
|
||||
payload = json.load(sys.stdin)
|
||||
except Exception:
|
||||
print("[pr-ci-wait] status payload unavailable")
|
||||
raise SystemExit(0)
|
||||
|
||||
statuses = payload.get("statuses") or []
|
||||
if not statuses:
|
||||
print("[pr-ci-wait] no status contexts reported yet")
|
||||
raise SystemExit(0)
|
||||
|
||||
for item in statuses:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = item.get("context") or item.get("name") or "unknown-context"
|
||||
state = item.get("status") or item.get("state") or "unknown-state"
|
||||
target = item.get("target_url") or item.get("url") or ""
|
||||
if target:
|
||||
print(f"[pr-ci-wait] {name}: {state} ({target})")
|
||||
else:
|
||||
print(f"[pr-ci-wait] {name}: {state}")
|
||||
PY
|
||||
}
|
||||
|
||||
github_get_pr_head_sha() {
|
||||
gh pr view "$PR_NUMBER" --json headRefOid --jq '.headRefOid'
|
||||
}
|
||||
|
||||
github_get_commit_status_json() {
|
||||
local owner="$1"
|
||||
local repo="$2"
|
||||
local sha="$3"
|
||||
gh api "repos/${owner}/${repo}/commits/${sha}/status"
|
||||
}
|
||||
|
||||
gitea_get_pr_head_sha() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local token="$3"
|
||||
local url="https://${host}/api/v1/repos/${repo}/pulls/${PR_NUMBER}"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url" | python3 -c '
|
||||
import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
print((data.get("head") or {}).get("sha", ""))
|
||||
'
|
||||
}
|
||||
|
||||
gitea_get_commit_status_json() {
|
||||
local host="$1"
|
||||
local repo="$2"
|
||||
local token="$3"
|
||||
local sha="$4"
|
||||
local url="https://${host}/api/v1/repos/${repo}/commits/${sha}/status"
|
||||
curl -fsS -H "Authorization: token ${token}" "$url"
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-n|--number)
|
||||
PR_NUMBER="$2"
|
||||
shift 2
|
||||
;;
|
||||
-t|--timeout)
|
||||
TIMEOUT_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interval)
|
||||
INTERVAL_SEC="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown option: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$PR_NUMBER" ]]; then
|
||||
echo "Error: PR number is required (-n)." >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! [[ "$TIMEOUT_SEC" =~ ^[0-9]+$ ]] || ! [[ "$INTERVAL_SEC" =~ ^[0-9]+$ ]]; then
|
||||
echo "Error: timeout and interval must be integer seconds." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
detect_platform > /dev/null
|
||||
|
||||
OWNER=$(get_repo_owner)
|
||||
REPO=$(get_repo_name)
|
||||
START_TS=$(date +%s)
|
||||
DEADLINE_TS=$((START_TS + TIMEOUT_SEC))
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
if ! command -v gh >/dev/null 2>&1; then
|
||||
echo "Error: gh CLI is required for GitHub CI status polling." >&2
|
||||
exit 1
|
||||
fi
|
||||
HEAD_SHA=$(github_get_pr_head_sha)
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[pr-ci-wait] Platform=github PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
HOST=$(get_remote_host) || {
|
||||
echo "Error: Could not determine remote host." >&2
|
||||
exit 1
|
||||
}
|
||||
TOKEN=$(get_gitea_token "$HOST") || {
|
||||
echo "Error: Gitea token not found. Set GITEA_TOKEN or configure ~/.git-credentials." >&2
|
||||
exit 1
|
||||
}
|
||||
HEAD_SHA=$(gitea_get_pr_head_sha "$HOST" "$OWNER/$REPO" "$TOKEN")
|
||||
if [[ -z "$HEAD_SHA" ]]; then
|
||||
echo "Error: Could not resolve head SHA for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "[pr-ci-wait] Platform=gitea host=${HOST} PR=#${PR_NUMBER} head_sha=${HEAD_SHA}"
|
||||
else
|
||||
echo "Error: Unsupported platform '${PLATFORM}'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
while true; do
|
||||
NOW_TS=$(date +%s)
|
||||
if (( NOW_TS > DEADLINE_TS )); then
|
||||
echo "Error: Timed out waiting for CI status on PR #$PR_NUMBER after ${TIMEOUT_SEC}s." >&2
|
||||
exit 124
|
||||
fi
|
||||
|
||||
if [[ "$PLATFORM" == "github" ]]; then
|
||||
STATUS_JSON=$(github_get_commit_status_json "$OWNER" "$REPO" "$HEAD_SHA")
|
||||
else
|
||||
STATUS_JSON=$(gitea_get_commit_status_json "$HOST" "$OWNER/$REPO" "$TOKEN" "$HEAD_SHA")
|
||||
fi
|
||||
|
||||
STATE=$(printf '%s' "$STATUS_JSON" | extract_state_from_status_json)
|
||||
echo "[pr-ci-wait] state=${STATE} pr=#${PR_NUMBER} sha=${HEAD_SHA}"
|
||||
|
||||
case "$STATE" in
|
||||
success)
|
||||
printf '%s' "$STATUS_JSON" | print_status_summary
|
||||
echo "[pr-ci-wait] CI is green for PR #$PR_NUMBER."
|
||||
exit 0
|
||||
;;
|
||||
failure|error)
|
||||
printf '%s' "$STATUS_JSON" | print_status_summary
|
||||
echo "Error: CI reported ${STATE} for PR #$PR_NUMBER." >&2
|
||||
exit 1
|
||||
;;
|
||||
pending|unknown)
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
*)
|
||||
echo "[pr-ci-wait] Unrecognized state '${STATE}', continuing to poll..."
|
||||
sleep "$INTERVAL_SEC"
|
||||
;;
|
||||
esac
|
||||
done
|
||||
47
rails/git/pr-merge.ps1
Normal file → Executable file
47
rails/git/pr-merge.ps1
Normal file → Executable file
@@ -1,5 +1,5 @@
|
||||
# pr-merge.ps1 - Merge pull requests on Gitea or GitHub
|
||||
# Usage: .\pr-merge.ps1 -Number PR_NUMBER [-Method method] [-DeleteBranch]
|
||||
# Usage: .\pr-merge.ps1 -Number PR_NUMBER [-Method squash] [-DeleteBranch]
|
||||
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
@@ -8,12 +8,13 @@ param(
|
||||
[int]$Number,
|
||||
|
||||
[Alias("m")]
|
||||
[ValidateSet("merge", "squash", "rebase")]
|
||||
[string]$Method = "merge",
|
||||
[string]$Method = "squash",
|
||||
|
||||
[Alias("d")]
|
||||
[switch]$DeleteBranch,
|
||||
|
||||
[switch]$SkipQueueGuard,
|
||||
|
||||
[Alias("h")]
|
||||
[switch]$Help
|
||||
)
|
||||
@@ -29,14 +30,15 @@ Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-Number, -n NUMBER PR number to merge (required)
|
||||
-Method, -m METHOD Merge method: merge, squash, rebase (default: merge)
|
||||
-Method, -m METHOD Merge method: squash only (default: squash)
|
||||
-DeleteBranch, -d Delete the head branch after merge
|
||||
-SkipQueueGuard Skip CI queue guard wait before merge
|
||||
-Help, -h Show this help message
|
||||
|
||||
Examples:
|
||||
.\pr-merge.ps1 -n 42 # Merge PR #42
|
||||
.\pr-merge.ps1 -n 42 -m squash # Squash merge
|
||||
.\pr-merge.ps1 -n 42 -m rebase -d # Rebase and delete branch
|
||||
.\pr-merge.ps1 -n 42 -d # Squash merge and delete branch
|
||||
"@
|
||||
exit 1
|
||||
}
|
||||
@@ -45,27 +47,42 @@ if ($Help) {
|
||||
Show-Usage
|
||||
}
|
||||
|
||||
if ($Method -ne "squash") {
|
||||
Write-Error "Mosaic policy enforces squash merge only. Received '$Method'."
|
||||
exit 1
|
||||
}
|
||||
|
||||
$platform = Get-GitPlatform
|
||||
|
||||
switch ($platform) {
|
||||
"github" {
|
||||
$cmd = @("gh", "pr", "merge", $Number)
|
||||
switch ($Method) {
|
||||
"merge" { $cmd += "--merge" }
|
||||
"squash" { $cmd += "--squash" }
|
||||
"rebase" { $cmd += "--rebase" }
|
||||
$baseRef = (& gh pr view $Number --json baseRefName --jq ".baseRefName").Trim()
|
||||
if ($baseRef -ne "main") {
|
||||
Write-Error "Mosaic policy allows merges only for PRs targeting 'main' (found '$baseRef')."
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $SkipQueueGuard) {
|
||||
$timeout = if ($env:MOSAIC_CI_QUEUE_TIMEOUT_SEC) { [int]$env:MOSAIC_CI_QUEUE_TIMEOUT_SEC } else { 900 }
|
||||
$interval = if ($env:MOSAIC_CI_QUEUE_POLL_SEC) { [int]$env:MOSAIC_CI_QUEUE_POLL_SEC } else { 15 }
|
||||
& "$ScriptDir\ci-queue-wait.ps1" -Purpose merge -Branch $baseRef -TimeoutSeconds $timeout -IntervalSeconds $interval
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$cmd = @("gh", "pr", "merge", $Number, "--squash")
|
||||
if ($DeleteBranch) { $cmd += "--delete-branch" }
|
||||
& $cmd[0] $cmd[1..($cmd.Length-1)]
|
||||
}
|
||||
"gitea" {
|
||||
$cmd = @("tea", "pr", "merge", $Number)
|
||||
switch ($Method) {
|
||||
"merge" { $cmd += @("--style", "merge") }
|
||||
"squash" { $cmd += @("--style", "squash") }
|
||||
"rebase" { $cmd += @("--style", "rebase") }
|
||||
if (-not $SkipQueueGuard) {
|
||||
$timeout = if ($env:MOSAIC_CI_QUEUE_TIMEOUT_SEC) { [int]$env:MOSAIC_CI_QUEUE_TIMEOUT_SEC } else { 900 }
|
||||
$interval = if ($env:MOSAIC_CI_QUEUE_POLL_SEC) { [int]$env:MOSAIC_CI_QUEUE_POLL_SEC } else { 15 }
|
||||
& "$ScriptDir\ci-queue-wait.ps1" -Purpose merge -Branch "main" -TimeoutSeconds $timeout -IntervalSeconds $interval
|
||||
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
||||
}
|
||||
|
||||
$cmd = @("tea", "pr", "merge", $Number, "--style", "squash")
|
||||
|
||||
if ($DeleteBranch) {
|
||||
Write-Warning "Branch deletion after merge may need to be done separately with tea"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#!/bin/bash
|
||||
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m method] [-d]
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--skip-queue-guard]
|
||||
|
||||
set -e
|
||||
|
||||
@@ -9,8 +9,9 @@ source "$SCRIPT_DIR/detect-platform.sh"
|
||||
|
||||
# Default values
|
||||
PR_NUMBER=""
|
||||
MERGE_METHOD="merge" # merge, squash, rebase
|
||||
MERGE_METHOD="squash"
|
||||
DELETE_BRANCH=false
|
||||
SKIP_QUEUE_GUARD=false
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -20,14 +21,16 @@ Merge a pull request on the current repository (Gitea or GitHub).
|
||||
|
||||
Options:
|
||||
-n, --number NUMBER PR number to merge (required)
|
||||
-m, --method METHOD Merge method: merge, squash, rebase (default: merge)
|
||||
-m, --method METHOD Merge method: squash only (default: squash)
|
||||
-d, --delete-branch Delete the head branch after merge
|
||||
--skip-queue-guard Skip CI queue guard wait before merge
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
$(basename "$0") -n 42 # Merge PR #42
|
||||
$(basename "$0") -n 42 -m squash # Squash merge
|
||||
$(basename "$0") -n 42 -m rebase -d # Rebase and delete branch
|
||||
$(basename "$0") -n 42 -d # Squash merge and delete branch
|
||||
$(basename "$0") -n 42 --skip-queue-guard # Skip queue guard wait
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
@@ -47,6 +50,10 @@ while [[ $# -gt 0 ]]; do
|
||||
DELETE_BRANCH=true
|
||||
shift
|
||||
;;
|
||||
--skip-queue-guard)
|
||||
SKIP_QUEUE_GUARD=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
@@ -62,37 +69,36 @@ if [[ -z "$PR_NUMBER" ]]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
if [[ "$MERGE_METHOD" != "squash" ]]; then
|
||||
echo "Error: Mosaic policy enforces squash merge only. Received '$MERGE_METHOD'." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
BASE_BRANCH="$("$SCRIPT_DIR/pr-metadata.sh" -n "$PR_NUMBER" | python3 -c 'import json, sys; print((json.load(sys.stdin).get("baseRefName") or "").strip())')"
|
||||
if [[ "$BASE_BRANCH" != "main" ]]; then
|
||||
echo "Error: Mosaic policy allows merges only for PRs targeting 'main' (found '$BASE_BRANCH')." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "$SKIP_QUEUE_GUARD" != true ]]; then
|
||||
"$SCRIPT_DIR/ci-queue-wait.sh" \
|
||||
--purpose merge \
|
||||
-B "$BASE_BRANCH" \
|
||||
-t "${MOSAIC_CI_QUEUE_TIMEOUT_SEC:-900}" \
|
||||
-i "${MOSAIC_CI_QUEUE_POLL_SEC:-15}"
|
||||
fi
|
||||
|
||||
PLATFORM=$(detect_platform)
|
||||
|
||||
case "$PLATFORM" in
|
||||
github)
|
||||
CMD="gh pr merge $PR_NUMBER"
|
||||
case "$MERGE_METHOD" in
|
||||
merge) CMD="$CMD --merge" ;;
|
||||
squash) CMD="$CMD --squash" ;;
|
||||
rebase) CMD="$CMD --rebase" ;;
|
||||
*)
|
||||
echo "Error: Invalid merge method '$MERGE_METHOD'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
CMD="gh pr merge $PR_NUMBER --squash"
|
||||
[[ "$DELETE_BRANCH" == true ]] && CMD="$CMD --delete-branch"
|
||||
eval "$CMD"
|
||||
;;
|
||||
gitea)
|
||||
# tea pr merge syntax
|
||||
CMD="tea pr merge $PR_NUMBER"
|
||||
|
||||
# tea merge style flags
|
||||
case "$MERGE_METHOD" in
|
||||
merge) CMD="$CMD --style merge" ;;
|
||||
squash) CMD="$CMD --style squash" ;;
|
||||
rebase) CMD="$CMD --style rebase" ;;
|
||||
*)
|
||||
echo "Error: Invalid merge method '$MERGE_METHOD'" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
CMD="tea pr merge $PR_NUMBER --style squash"
|
||||
|
||||
# Delete branch after merge if requested
|
||||
if [[ "$DELETE_BRANCH" == true ]]; then
|
||||
|
||||
Reference in New Issue
Block a user