MOSAIC_GIT_IDENTITY=fargo produced objects attributed to mos-dt-0: every write wrapper resolved its acting principal from tea's login list, which enumerates whatever logins the host happens to hold and knows nothing about which seat is calling. The identity-aware code was present and correct but unreachable on the happy path — it sat on arms that only ran when tea failed. One shared resolver, not twenty patches: resolve_gitea_principal() in detect-platform.sh implements the precedence (explicit --login beats MOSAIC_GIT_IDENTITY / worktree git config mosaic.gitIdentity; the tea login list is the LAST resort), fails loud (nonzero, naming the identity or login and the expected slot path) when the requested principal has no credential, and never prints a token value. gitea_identity_token_slot() is the single source of truth for the slot layout, shared with get_gitea_token, so resolver and token resolution cannot disagree. Call-site conversions (the proving five): pr-review.sh (principal resolved once for every action; the comment action now honors --login), issue-comment.sh, pr-create.sh (identity mode reaches the REST API on the HAPPY path — tea is never consulted, so the login list cannot shadow the identity; --login wins even on the tea-failure fallback arm), issue-create.sh (same), pr-merge.sh (gains --login; --dry-run reports the principal the merge WOULD act as, resolved exactly as the merge resolves it; no cross-principal fallback — an identity-bound 401 is a hard stop). Remaining wrappers are call-site conversions onto the same resolver, measured: write-path issue-assign, issue-close, issue-edit, issue-reopen, milestone-close, milestone-create, pr-close; read-path issue-list, milestone-list, pr-list, pr-view (issue-view mixed). pr-diff, pr-metadata, pr-ci-wait and ci-queue-wait already inherit identity-first resolution via get_gitea_token. Known interaction: on a host with a workstation-GLOBAL mosaic.gitIdentity, this fix activates identity mode for every seat that has not set a local one — correct behavior driven by a wrong configuration (measured: #1282-#1287, six accidental live issues, closed with provenance by fred). Set mosaic.gitIdentity per-worktree, never --global. Tests: test-gitea-principal-resolution.sh (resolver matrix — identity present/absent, --login precedence, env vs git-config, unrecognized-host containment, slot-path-by-path-never-by-value); test-pr-create-identity- first.sh (the load-bearing ordering test: identity arm REACHED on the happy path with tea never invoked, fail-loud BEFORE any write on a missing slot, --login wins, default preserved); test-pr-merge-principal- resolution.sh (dry-run truthfulness, merge credential binding, unknown --login never reaches the provider). All wired into test:framework-shell. Sabotage control: precedence inverted to tea-list-first inside the resolver -> exactly the three new suites redden with the #1280 signatures (identity resolves to the tea-list account; missing slot returns rc=0 with silent fallthrough) while all 11 pre-existing git suites stay green; restored byte-identical (sha256 verified); all 14 green again.
287 lines
9.7 KiB
Bash
Executable File
287 lines
9.7 KiB
Bash
Executable File
#!/bin/bash
|
|
# pr-create.sh - Create pull requests on Gitea or GitHub
|
|
# Usage: pr-create.sh -t "Title" [-b "Body"] [-B base] [-H head] [-l "labels"] [-m "milestone"] [--login <name>]
|
|
#
|
|
# Acting principal is resolved identity-first (#1280): an explicit --login
|
|
# wins; otherwise MOSAIC_GIT_IDENTITY / per-worktree git config
|
|
# mosaic.gitIdentity selects the principal when a per-slot token exists (and
|
|
# the wrapper then creates the PR through the REST API with that identity's
|
|
# token — tea is never invoked, so the tea login list cannot shadow the
|
|
# requested principal); the tea login list is the LAST resort. A requested
|
|
# identity with no per-slot token fails LOUD rather than writing under
|
|
# whichever account tea happens to hold.
|
|
|
|
set -e
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
source "$SCRIPT_DIR/detect-platform.sh"
|
|
|
|
# Default values
|
|
TITLE=""
|
|
BODY=""
|
|
BASE_BRANCH=""
|
|
HEAD_BRANCH=""
|
|
LABELS=""
|
|
MILESTONE=""
|
|
DRAFT=false
|
|
ISSUE=""
|
|
|
|
# get_remote_host, get_gitea_token, get_repo_info, and get_gitea_repo_args are provided by detect-platform.sh
|
|
|
|
# Acting-principal mode set in the Gitea branch below (from
|
|
# resolve_gitea_principal): "login" when --login was given, "identity" when a
|
|
# git identity bound, "default" otherwise. PRINCIPAL_MODE=login makes the API
|
|
# arm resolve the --login principal's token too, so an explicit --login keeps
|
|
# winning even on the tea-FAILURE fallback arm (otherwise the fallback would
|
|
# silently re-resolve to the environment identity or shared credential).
|
|
PRINCIPAL_MODE=""
|
|
PRINCIPAL_NAME=""
|
|
|
|
gitea_pr_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
|
|
}
|
|
if [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
|
token=$(get_gitea_token_for_login "$PRINCIPAL_NAME" "$host") || {
|
|
echo "Error: could not resolve a host-matched Gitea token for --login '$PRINCIPAL_NAME' on host '$host' (API path)" >&2
|
|
return 1
|
|
}
|
|
else
|
|
# Identity-first when MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity
|
|
# is set (per-slot token, fail-loud on absence); shared default otherwise.
|
|
token=$(get_gitea_token "$host") || {
|
|
echo "Error: Gitea token not found for API fallback (set GITEA_TOKEN or configure ~/.git-credentials)" >&2
|
|
return 1
|
|
}
|
|
fi
|
|
|
|
if [[ -n "$LABELS" || -n "$MILESTONE" || "$DRAFT" == true ]]; then
|
|
echo "Warning: API fallback applies title/body/head/base only; labels/milestone/draft require authenticated tea setup." >&2
|
|
fi
|
|
|
|
payload=$(TITLE="$TITLE" BODY="$BODY" HEAD_BRANCH="$HEAD_BRANCH" BASE_BRANCH="$BASE_BRANCH" python3 - <<'PY'
|
|
import json
|
|
import os
|
|
|
|
payload = {
|
|
"title": os.environ["TITLE"],
|
|
"head": os.environ["HEAD_BRANCH"],
|
|
"base": os.environ["BASE_BRANCH"] or "main",
|
|
}
|
|
body = os.environ.get("BODY", "")
|
|
if body:
|
|
payload["body"] = body
|
|
print(json.dumps(payload))
|
|
PY
|
|
)
|
|
|
|
url="https://${host}/api/v1/repos/${repo}/pulls"
|
|
curl -fsS -X POST \
|
|
-H "User-Agent: curl/8" \
|
|
-H "Authorization: token ${token}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$payload" \
|
|
"$url"
|
|
}
|
|
|
|
usage() {
|
|
cat <<EOF
|
|
Usage: $(basename "$0") [OPTIONS]
|
|
|
|
Create a pull request on the current repository (Gitea or GitHub).
|
|
|
|
Options:
|
|
-t, --title TITLE PR title (required, or use --issue)
|
|
-b, --body BODY PR description/body
|
|
-B, --base BRANCH Base branch to merge into (default: main/master)
|
|
-H, --head BRANCH Head branch with changes (default: current branch)
|
|
-l, --labels LABELS Comma-separated labels
|
|
-m, --milestone NAME Milestone name
|
|
--login NAME Act as this Gitea tea login (wins over MOSAIC_GIT_IDENTITY)
|
|
-i, --issue NUMBER Link to issue (auto-generates title if not provided)
|
|
-d, --draft Create as draft PR
|
|
-h, --help Show this help message
|
|
|
|
Examples:
|
|
$(basename "$0") -t "Add login feature" -b "Implements user authentication"
|
|
$(basename "$0") -t "Fix bug" -B main -H feature/fix-123
|
|
$(basename "$0") -i 42 -b "Implements the feature described in #42"
|
|
$(basename "$0") -t "WIP: New feature" --draft
|
|
EOF
|
|
exit "${1:-1}"
|
|
}
|
|
|
|
# Parse arguments
|
|
while [[ $# -gt 0 ]]; do
|
|
case $1 in
|
|
-t|--title)
|
|
TITLE="$2"
|
|
shift 2
|
|
;;
|
|
-b|--body)
|
|
BODY="$2"
|
|
shift 2
|
|
;;
|
|
-B|--base)
|
|
BASE_BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
-H|--head)
|
|
HEAD_BRANCH="$2"
|
|
shift 2
|
|
;;
|
|
-l|--labels)
|
|
LABELS="$2"
|
|
shift 2
|
|
;;
|
|
-m|--milestone)
|
|
MILESTONE="$2"
|
|
shift 2
|
|
;;
|
|
--login)
|
|
LOGIN_OVERRIDE="$2"
|
|
shift 2
|
|
;;
|
|
-i|--issue)
|
|
ISSUE="$2"
|
|
shift 2
|
|
;;
|
|
-d|--draft)
|
|
DRAFT=true
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
usage 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1" >&2
|
|
usage
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# If no title but issue provided, generate title
|
|
if [[ -z "$TITLE" ]] && [[ -n "$ISSUE" ]]; then
|
|
TITLE="Fixes #$ISSUE"
|
|
fi
|
|
|
|
if [[ -z "$TITLE" ]]; then
|
|
echo "Error: Title is required (-t) or provide an issue (-i)" >&2
|
|
usage
|
|
fi
|
|
|
|
# Default head branch to current branch
|
|
if [[ -z "$HEAD_BRANCH" ]]; then
|
|
HEAD_BRANCH=$(git branch --show-current)
|
|
fi
|
|
|
|
# Add issue reference to body if provided
|
|
if [[ -n "$ISSUE" ]]; then
|
|
if [[ -n "$BODY" ]]; then
|
|
BODY="$BODY
|
|
|
|
Fixes #$ISSUE"
|
|
else
|
|
BODY="Fixes #$ISSUE"
|
|
fi
|
|
fi
|
|
|
|
PLATFORM=$(detect_platform)
|
|
|
|
case "$PLATFORM" in
|
|
github)
|
|
CMD=(gh pr create --title "$TITLE")
|
|
[[ -n "$BODY" ]] && CMD+=(--body "$BODY")
|
|
[[ -n "$BASE_BRANCH" ]] && CMD+=(--base "$BASE_BRANCH")
|
|
[[ -n "$HEAD_BRANCH" ]] && CMD+=(--head "$HEAD_BRANCH")
|
|
[[ -n "$LABELS" ]] && CMD+=(--label "$LABELS")
|
|
[[ -n "$MILESTONE" ]] && CMD+=(--milestone "$MILESTONE")
|
|
[[ "$DRAFT" == true ]] && CMD+=(--draft)
|
|
"${CMD[@]}"
|
|
;;
|
|
gitea)
|
|
# Resolve the acting principal identity-first (#1280). The tea login
|
|
# list is the LAST resort: it knows nothing about which seat is calling,
|
|
# and a login resolved from it first is what attributed PRs to the wrong
|
|
# account even when MOSAIC_GIT_IDENTITY was set.
|
|
principal_host=$(get_remote_host 2>/dev/null || true)
|
|
if ! principal_resolved="$(resolve_gitea_principal "${LOGIN_OVERRIDE:-}" "$principal_host")"; then
|
|
# resolve_gitea_principal already printed the fail-loud diagnostic.
|
|
exit 1
|
|
fi
|
|
PRINCIPAL_MODE="$(printf '%s' "$principal_resolved" | cut -f1)"
|
|
PRINCIPAL_NAME="$(printf '%s' "$principal_resolved" | cut -f2)"
|
|
|
|
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
|
# HAPPY PATH for a requested identity: the per-slot token IS the
|
|
# credential, so create through the REST API directly and never
|
|
# invoke tea — the identity arm must be REACHED, not sit behind a
|
|
# tea failure (#1280). Fail-loud on a missing slot already happened
|
|
# in resolve_gitea_principal.
|
|
gitea_pr_create_api
|
|
exit $?
|
|
fi
|
|
|
|
# tea pull create syntax. Always pass --repo because tea repo inference
|
|
# is unreliable in Mosaic worktrees/profile shells. Use arrays instead
|
|
# of eval so markdown backticks/body content are not shell-executed.
|
|
REPO_SLUG=$(get_repo_slug)
|
|
if [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
|
GITEA_LOGIN_NAME="$PRINCIPAL_NAME"
|
|
else
|
|
GITEA_LOGIN_NAME=$(get_gitea_login) || {
|
|
echo "Warning: could not resolve Gitea login for tea; trying Gitea API fallback..." >&2
|
|
gitea_pr_create_api
|
|
exit $?
|
|
}
|
|
fi
|
|
if ! get_gitea_authenticated_user "$GITEA_LOGIN_NAME" >/dev/null; then
|
|
echo "Warning: Tea authenticated-user validation failed (possible stale user/login); trying Gitea API fallback..." >&2
|
|
gitea_pr_create_api
|
|
exit $?
|
|
fi
|
|
REPO_ARGS=(--repo "$REPO_SLUG" --login "$GITEA_LOGIN_NAME")
|
|
CMD=(tea pr create "${REPO_ARGS[@]}" --title "$TITLE")
|
|
[[ -n "$BODY" ]] && CMD+=(--description "$BODY")
|
|
[[ -n "$BASE_BRANCH" ]] && CMD+=(--base "$BASE_BRANCH")
|
|
[[ -n "$HEAD_BRANCH" ]] && CMD+=(--head "$HEAD_BRANCH")
|
|
|
|
# Handle labels for tea
|
|
if [[ -n "$LABELS" ]]; then
|
|
# tea may use --labels flag
|
|
CMD+=(--labels "$LABELS")
|
|
fi
|
|
|
|
# Handle milestone for tea
|
|
if [[ -n "$MILESTONE" ]]; then
|
|
MILESTONE_ID=$(tea milestones list "${REPO_ARGS[@]}" 2>/dev/null | grep -E "^\s*[0-9]+" | grep "$MILESTONE" | awk '{print $1}' | head -1)
|
|
if [[ -n "$MILESTONE_ID" ]]; then
|
|
CMD+=(--milestone "$MILESTONE_ID")
|
|
else
|
|
echo "Warning: Could not find milestone '$MILESTONE', creating without milestone" >&2
|
|
fi
|
|
fi
|
|
|
|
# Note: tea may not support --draft flag in all versions
|
|
if [[ "$DRAFT" == true ]]; then
|
|
echo "Note: Draft PR may not be supported by your tea version" >&2
|
|
fi
|
|
|
|
if "${CMD[@]}"; then
|
|
exit 0
|
|
fi
|
|
echo "Warning: tea pr create failed, trying Gitea API fallback..." >&2
|
|
gitea_pr_create_api
|
|
;;
|
|
*)
|
|
echo "Error: Could not detect git platform" >&2
|
|
exit 1
|
|
;;
|
|
esac
|