fix(git): identity-first principal resolution across write wrappers (#1280)
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.
This commit is contained in:
@@ -30,7 +30,7 @@ The Gitea API token is **never passed on a curl command line.** An `Authorizatio
|
||||
|
||||
### `--login` override
|
||||
|
||||
Both `pr-review.sh` and `issue-comment.sh` accept an optional `--login <name>` flag that overrides the automatically detected Gitea login for that single invocation. The override selects **which credential the REST write, the `/user` identity lookup, and the read-back all use** — its token is resolved from the tea config for that login name (`get_gitea_token_for_login`), falling back to the repo host's credential when no login is named. The resolved login is **host- and port-bound**: the login's configured URL host **and effective port** (the scheme's default port — 80 for `http`, 443 for `https` — applies when a port is omitted, symmetrically on both sides) must match the repo remote's, so a login name shared across hosts (or an override configured for a different Gitea, including one on a different port of the same host) can never send one host's credential to another — a host or port mismatch fails closed rather than leaking a cross-host token. Resolving the acting identity and the read-back from the _same_ login that performs the write is essential: a write performed under an overridden login must be verified against that login's identity, not the host default's. Callers who need a different login than the host default should pass `--login <reviewer-login>`.
|
||||
Both `pr-review.sh` and `issue-comment.sh` accept an optional `--login <name>` flag that overrides the automatically detected Gitea login for that single invocation (as of #1280, `pr-create.sh`, `pr-merge.sh` and `issue-create.sh` accept it too, and it wins over `MOSAIC_GIT_IDENTITY` everywhere). The override selects **which credential the REST write, the `/user` identity lookup, and the read-back all use** — its token is resolved from the tea config for that login name (`get_gitea_token_for_login`), falling back to the repo host's credential when no login is named. The resolved login is **host- and port-bound**: the login's configured URL host **and effective port** (the scheme's default port — 80 for `http`, 443 for `https` — applies when a port is omitted, symmetrically on both sides) must match the repo remote's, so a login name shared across hosts (or an override configured for a different Gitea, including one on a different port of the same host) can never send one host's credential to another — a host or port mismatch fails closed rather than leaking a cross-host token. Resolving the acting identity and the read-back from the _same_ login that performs the write is essential: a write performed under an overridden login must be verified against that login's identity, not the host default's. Callers who need a different login than the host default should pass `--login <reviewer-login>`.
|
||||
|
||||
As a durable successor to this mechanism, consider giving each reviewer/approver slot its own dedicated Gitea login credential, so that author≠reviewer holds at the credential level rather than relying on wrapper-level `--login` bookkeeping. This is a recommendation for future hardening, not something implemented by this flag.
|
||||
|
||||
@@ -58,6 +58,36 @@ token file present, both tools fall through to the existing shared-account path
|
||||
unchanged, so this feature is a no-op on any host that hasn't provisioned per-slot
|
||||
tokens.
|
||||
|
||||
### Identity-first principal resolution in the wrappers (#1280)
|
||||
|
||||
`resolve_gitea_principal()` (detect-platform.sh) gives the write wrappers —
|
||||
`pr-create.sh`, `pr-merge.sh`, `pr-review.sh`, `issue-create.sh`, `issue-comment.sh` —
|
||||
ONE precedence for choosing the acting principal:
|
||||
|
||||
1. an explicit `--login <name>` (now accepted by all five; operator intent beats
|
||||
environment), then
|
||||
2. the per-agent identity above (`MOSAIC_GIT_IDENTITY` env / worktree
|
||||
`mosaic.gitIdentity`) when a per-slot token exists — the wrapper then writes via the
|
||||
REST API with that identity's token and never consults `tea`, so the tea login list
|
||||
cannot shadow the requested principal, then
|
||||
3. the tea login list — the LAST resort, never the first, because it enumerates
|
||||
whatever logins the host happens to hold and knows nothing about which seat is
|
||||
calling.
|
||||
|
||||
A requested identity whose per-slot token is absent, or a `--login` whose token cannot
|
||||
resolve host-bound, **fails loud** (nonzero, naming the identity/login and the expected
|
||||
slot) instead of silently writing under whatever account `tea` has configured — that
|
||||
silent fallthrough is defect #1280 (reviews, comments, merges, PRs and issues filed
|
||||
under the wrong account). `pr-merge.sh --dry-run` reports the principal the merge would
|
||||
act as, resolved exactly as the real merge resolves it. ⚠ A **workstation-global**
|
||||
`mosaic.gitIdentity` shadows every seat on that host (a fresh clone with no local value
|
||||
resolves the global one) — set it per-worktree, not with `--global`.
|
||||
|
||||
The resolver is covered by `test-gitea-principal-resolution.sh`; the happy-path
|
||||
ordering (identity arm REACHED, not sitting behind a tea failure) by
|
||||
`test-pr-create-identity-first.sh`; merge credential binding by
|
||||
`test-pr-merge-principal-resolution.sh`.
|
||||
|
||||
### Enabling it for a clone
|
||||
|
||||
The framework installer syncs `git-credential-mosaic` to
|
||||
|
||||
@@ -497,6 +497,32 @@ get_gitea_url_for_host() {
|
||||
return 1
|
||||
}
|
||||
|
||||
# Map a Gitea host to the per-agent identity-token slot PREFIX ("gitea-usc" /
|
||||
# "gitea-mosaicstack") used by identity-first principal resolution
|
||||
# (MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity; #1280). Returns 1 for
|
||||
# hosts with no per-slot scheme — callers treat that as "identity does not
|
||||
# bind here" and fall through to existing behavior, never as an error. This is
|
||||
# the single source of truth for the slot layout: get_gitea_token and
|
||||
# resolve_gitea_principal both derive their slot paths from here, so the two
|
||||
# resolutions can never disagree about where an identity's credential lives.
|
||||
gitea_identity_slot_prefix() {
|
||||
case "$1" in
|
||||
git.uscllc.com) echo "gitea-usc" ;;
|
||||
git.mosaicstack.dev) echo "gitea-mosaicstack" ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Resolve the per-slot token FILE PATH for an identity on a host. Prints the
|
||||
# absolute path on success; returns 1 (no output) when the host has no per-slot
|
||||
# scheme. Prints a PATH only — never a token value.
|
||||
gitea_identity_token_slot() {
|
||||
local identity="$1" host="$2" prefix
|
||||
[[ -n "$identity" ]] || return 1
|
||||
prefix=$(gitea_identity_slot_prefix "$host") || return 1
|
||||
printf '%s\n' "$HOME/.config/mosaic/secrets/gitea-tokens/${prefix}-${identity}.token"
|
||||
}
|
||||
|
||||
# Resolve a Gitea API token for the given host.
|
||||
# Priority: Mosaic credential loader → GITEA_TOKEN env → ~/.git-credentials
|
||||
get_gitea_token() {
|
||||
@@ -517,13 +543,8 @@ get_gitea_token() {
|
||||
_ident_src="git config mosaic.gitIdentity"
|
||||
fi
|
||||
if [[ -n "$_ident" ]]; then
|
||||
local _idpfx=""
|
||||
case "$host" in
|
||||
git.uscllc.com) _idpfx=gitea-usc ;;
|
||||
git.mosaicstack.dev) _idpfx=gitea-mosaicstack ;;
|
||||
esac
|
||||
if [[ -n "$_idpfx" ]]; then
|
||||
local _idtok="$HOME/.config/mosaic/secrets/gitea-tokens/${_idpfx}-${_ident}.token"
|
||||
local _idtok=""
|
||||
if _idtok="$(gitea_identity_token_slot "$_ident" "$host" 2>/dev/null)"; then
|
||||
if [[ -r "$_idtok" ]]; then
|
||||
cat "$_idtok"
|
||||
return 0
|
||||
@@ -1465,6 +1486,81 @@ raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
# resolve_gitea_principal — identity-first acting-principal resolution shared by
|
||||
# the git wrappers (#1280). The defect this fixes: wrappers resolved their
|
||||
# acting principal from tea's login list FIRST, and that list enumerates
|
||||
# whatever logins happen to be configured on the host — it knows nothing about
|
||||
# which seat is calling — so a wrapper invoked with MOSAIC_GIT_IDENTITY=fargo
|
||||
# still wrote under whichever account tea held (mos-dt-0), and the correct
|
||||
# identity-aware code sat behind arms that only ran when the tea path failed.
|
||||
# Precedence here is the contract:
|
||||
# 1. an explicit login override ($1, the wrapper's --login) — operator intent
|
||||
# beats environment;
|
||||
# 2. MOSAIC_GIT_IDENTITY env, else per-worktree `git config mosaic.gitIdentity`
|
||||
# (mirroring get_gitea_token exactly, so resolver and token resolution can
|
||||
# never disagree) — binds only on hosts with a per-slot token scheme;
|
||||
# 3. the tea login list — LAST resort, never the first.
|
||||
#
|
||||
# Prints exactly one line, three tab-separated fields (machine-readable for
|
||||
# wrapper dispatch and tests):
|
||||
# mode "login" | "identity" | "default"
|
||||
# principal login name (login) | identity name (identity) | tea login or "" (default)
|
||||
# source "tea-login:<name>" | "identity-slot:<path>" | "tea-default" | "host-credential"
|
||||
#
|
||||
# Fails LOUD (nonzero, empty stdout, stderr diagnostic) when an explicit
|
||||
# override cannot be honored — a refusal is a good day; silently falling
|
||||
# through to whoever tea has configured is the exact defect this resolves:
|
||||
# - login mode: no host-bound token for that tea login. The existence check
|
||||
# runs the same tea-config lookup tea itself uses; the token VALUE is
|
||||
# discarded (never printed, never used).
|
||||
# - identity mode: no per-slot token file for that identity on a recognized
|
||||
# host — the diagnostic names the identity, its source, and the expected
|
||||
# slot path. An identity requested on a host with NO per-slot scheme does
|
||||
# not bind (matching get_gitea_token's containment) and falls to default.
|
||||
#
|
||||
# NEVER prints a token value — principal names and slot paths only.
|
||||
# $1 = explicit login override ("" when absent), $2 = host (default: the
|
||||
# origin remote's host).
|
||||
resolve_gitea_principal() {
|
||||
local login_override="${1:-}" host="${2:-}" ident ident_src slot login
|
||||
[[ -n "$host" ]] || { host=$(get_remote_host) || return 1; }
|
||||
|
||||
if [[ -n "$login_override" ]]; then
|
||||
get_gitea_token_for_login "$login_override" "$host" >/dev/null || {
|
||||
echo "Error: --login '$login_override' has no host-matched token on host '$host' (tea config lookup); refusing to fall back to any other principal (#1280 identity-first resolution)." >&2
|
||||
return 1
|
||||
}
|
||||
printf 'login\t%s\ttea-login:%s\n' "$login_override" "$login_override"
|
||||
return 0
|
||||
fi
|
||||
|
||||
ident="${MOSAIC_GIT_IDENTITY:-}"
|
||||
ident_src="MOSAIC_GIT_IDENTITY"
|
||||
if [[ -z "$ident" ]]; then
|
||||
ident="$(git config --get mosaic.gitIdentity 2>/dev/null || true)"
|
||||
ident_src="git config mosaic.gitIdentity"
|
||||
fi
|
||||
if [[ -n "$ident" ]] && slot="$(gitea_identity_token_slot "$ident" "$host" 2>/dev/null)"; then
|
||||
if [[ -r "$slot" ]]; then
|
||||
printf 'identity\t%s\tidentity-slot:%s\n' "$ident" "$slot"
|
||||
return 0
|
||||
fi
|
||||
echo "Error: git identity '$ident' requested (via $ident_src) for host '$host', but no per-slot token at $slot (#1280 identity-first resolution)." >&2
|
||||
echo " Refusing to fall back to the tea login list or shared credentials. Provision the per-slot token, or unset the identity." >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# No override requested: tea's login list is the LAST resort. Absence is
|
||||
# not an error here — callers fall back to the host credential, exactly as
|
||||
# they did before this resolver existed (preserved behavior).
|
||||
if login=$(get_gitea_login_for_host "$host" 2>/dev/null); then
|
||||
printf 'default\t%s\ttea-default\n' "$login"
|
||||
else
|
||||
printf 'default\t\thost-credential\n'
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
# Resolve HTTPS basic auth credentials for a Gitea host from ~/.git-credentials.
|
||||
# Prints "username:password" for direct curl -u consumption. Callers must not log it.
|
||||
get_gitea_basic_auth() {
|
||||
|
||||
@@ -76,27 +76,36 @@ fi
|
||||
detect_platform >/dev/null
|
||||
|
||||
# Resolve and cache the Gitea REST endpoint + token for the current remote,
|
||||
# bound to a SPECIFIC login identity ($1). Populates GITEA_API_ROOT (…/api/v1),
|
||||
# GITEA_API_BASE (…/api/v1/repos/<slug>), and GITEA_API_TOKEN.
|
||||
# bound to a SPECIFIC acting principal ($1) selected identity-first (#1280):
|
||||
# an explicit --login wins, else MOSAIC_GIT_IDENTITY / git config
|
||||
# mosaic.gitIdentity binds the per-slot credential, else the tea login list
|
||||
# (last resort). Populates GITEA_API_ROOT (…/api/v1), GITEA_API_BASE
|
||||
# (…/api/v1/repos/<slug>), and GITEA_API_TOKEN.
|
||||
#
|
||||
# The token is resolved for the EFFECTIVE login (the --login override when
|
||||
# given, otherwise the detected default) so that the single credential used for
|
||||
# the write ALSO drives the /user identity read and the read-back — write token
|
||||
# and read-back token are the same identity by construction (this is the
|
||||
# credential-ordering fix: a --login override is no longer written under one
|
||||
# credential and verified under a different default one). Falls back to the
|
||||
# host-scoped credential ONLY when NO --login override was supplied (the
|
||||
# best-effort default path). When $2 is "explicit" the login came from a
|
||||
# caller-supplied --login: that exact login's token MUST resolve, and we FAIL
|
||||
# CLOSED rather than silently downgrading the write to the host default
|
||||
# identity — otherwise a caller relying on a dedicated per-role credential would
|
||||
# be told the write succeeded as requested while it was attributed to the shared
|
||||
# default. Returns non-zero (clear stderr) on any resolution failure.
|
||||
# The token is resolved for the EFFECTIVE principal so that the single
|
||||
# credential used for the write ALSO drives the /user identity read and the
|
||||
# read-back — write token and read-back token are the same identity by
|
||||
# construction (this is the credential-ordering fix: a --login override is no
|
||||
# longer written under one credential and verified under a different default
|
||||
# one). When $2 is "identity" the principal ($1) is a requested git identity:
|
||||
# the token MUST resolve from that identity's per-slot token (get_gitea_token's
|
||||
# identity arm), failing closed rather than borrowing the tea default login —
|
||||
# the tea login list must never shadow a requested identity (#1280). When $2
|
||||
# is "explicit" the principal came from a caller-supplied --login: that exact
|
||||
# login's token MUST resolve, and we FAIL CLOSED rather than silently
|
||||
# downgrading the write to the host default identity. Otherwise the best-effort
|
||||
# default path applies (per-login token, else the host-scoped credential).
|
||||
# Returns non-zero (clear stderr) on any resolution failure.
|
||||
gitea_resolve_api_for_login() {
|
||||
local effective_login="$1" override_explicit="${2:-}" host configured_url repo
|
||||
|
||||
host=$(get_remote_host)
|
||||
if [[ -n "$override_explicit" ]]; then
|
||||
if [[ "$override_explicit" == "identity" ]]; then
|
||||
GITEA_API_TOKEN=$(get_gitea_token "$host") || {
|
||||
echo "Error: could not resolve the per-slot token for requested git identity '$effective_login' on host '$host'; refusing to fall back to the tea login list or shared credentials (comment write/read-back, #1280)." >&2
|
||||
return 1
|
||||
}
|
||||
elif [[ -n "$override_explicit" ]]; then
|
||||
GITEA_API_TOKEN=$(get_gitea_token_for_login "$effective_login" "$host") || {
|
||||
echo "Error: could not resolve a host-matched Gitea token for --login '$effective_login' on host '$host'; refusing to fall back to the host default identity or a cross-host credential (comment write/read-back)" >&2
|
||||
return 1
|
||||
@@ -318,23 +327,31 @@ if [[ "$PLATFORM" == "github" ]]; then
|
||||
gh issue comment "$ISSUE_NUMBER" --body "$COMMENT"
|
||||
echo "Added comment to GitHub issue #$ISSUE_NUMBER"
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
# Resolve the login this comment should be attributed to: the --login
|
||||
# override when given, otherwise the detected default for this repo's host.
|
||||
# A --login override always wins. Otherwise name this repo host's login only
|
||||
# as a best effort: the login name merely selects a per-login token, and
|
||||
# gitea_resolve_api_for_login falls back to the host credential
|
||||
# (get_gitea_token) when no tea login is named, so the default credential
|
||||
# still resolves even when the host tea has no matching login entry.
|
||||
EFFECTIVE_LOGIN="$LOGIN_OVERRIDE"
|
||||
[[ -n "$EFFECTIVE_LOGIN" ]] || EFFECTIVE_LOGIN=$(get_gitea_login 2>/dev/null || true)
|
||||
# Resolve the acting principal identity-first (#1280): an explicit --login
|
||||
# wins; otherwise MOSAIC_GIT_IDENTITY / git config mosaic.gitIdentity
|
||||
# selects the principal when a per-slot token exists (fail-loud when it
|
||||
# does not); the tea login list is the LAST resort — it knows nothing about
|
||||
# which seat is calling, so resolving from it first wrote under whichever
|
||||
# account tea had configured (the #1280 family).
|
||||
principal_host=$(get_remote_host)
|
||||
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)"
|
||||
|
||||
# Bind the REST endpoint + token to the effective login, then derive the
|
||||
# Bind the REST endpoint + token to the resolved principal, then derive the
|
||||
# acting identity from that SAME credential (GET /user). The write below and
|
||||
# its read-back both use this credential, so the write is verified against
|
||||
# the identity that actually performed it. Passing "explicit" when --login
|
||||
# was supplied forbids the host-default fallback: an unresolvable explicit
|
||||
# override fails closed instead of writing under the default identity.
|
||||
gitea_resolve_api_for_login "$EFFECTIVE_LOGIN" "${LOGIN_OVERRIDE:+explicit}" || exit 1
|
||||
# the identity that actually performed it.
|
||||
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" identity || exit 1
|
||||
elif [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" explicit || exit 1
|
||||
else
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" "" || exit 1
|
||||
fi
|
||||
ACTING_LOGIN=$(gitea_authenticated_login) || exit 1
|
||||
|
||||
comment_id=$(gitea_create_comment_verified "$ISSUE_NUMBER" "$COMMENT" "$ACTING_LOGIN") || {
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#!/bin/bash
|
||||
# issue-create.sh - Create issues on Gitea or GitHub
|
||||
# Usage: issue-create.sh -t "Title" [-b "Body"] [-l "label1,label2"] [-m "milestone"]
|
||||
# Usage: issue-create.sh -t "Title" [-b "Body"] [-l "label1,label2"] [-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 issue 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
|
||||
|
||||
@@ -16,6 +25,14 @@ INTERACTIVE=false
|
||||
|
||||
# get_remote_host and get_gitea_token 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.
|
||||
PRINCIPAL_MODE=""
|
||||
PRINCIPAL_NAME=""
|
||||
|
||||
gitea_issue_create_api() {
|
||||
local host repo token url payload
|
||||
host=$(get_remote_host) || {
|
||||
@@ -26,10 +43,19 @@ gitea_issue_create_api() {
|
||||
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 [[ "$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" ]]; then
|
||||
echo "Warning: API fallback currently applies title/body only; labels/milestone require authenticated tea setup." >&2
|
||||
@@ -67,6 +93,7 @@ Options:
|
||||
-b, --body BODY Issue body/description
|
||||
-l, --labels LABELS Comma-separated labels (e.g., "bug,feature")
|
||||
-m, --milestone NAME Milestone name to assign
|
||||
--login NAME Act as this Gitea tea login (wins over MOSAIC_GIT_IDENTITY)
|
||||
-i, --interactive Prompt for missing issue fields
|
||||
-h, --help Show this help message
|
||||
|
||||
@@ -97,6 +124,10 @@ while [[ $# -gt 0 ]]; do
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--login)
|
||||
LOGIN_OVERRIDE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--interactive)
|
||||
INTERACTIVE=true
|
||||
shift
|
||||
@@ -134,13 +165,37 @@ case "$PLATFORM" in
|
||||
"${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 issues 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: create through the REST API
|
||||
# with the per-slot token and never invoke tea — the identity arm
|
||||
# must be REACHED, not sit behind a tea failure (#1280).
|
||||
gitea_issue_create_api
|
||||
exit $?
|
||||
fi
|
||||
|
||||
if command -v tea >/dev/null 2>&1; then
|
||||
REPO_SLUG=$(get_repo_slug)
|
||||
GITEA_LOGIN_NAME=$(get_gitea_login) || {
|
||||
echo "Warning: could not resolve Gitea login for tea; trying Gitea API fallback..." >&2
|
||||
gitea_issue_create_api
|
||||
exit $?
|
||||
}
|
||||
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_issue_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_issue_create_api
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
#!/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"]
|
||||
# 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
|
||||
|
||||
@@ -19,6 +28,15 @@ 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) || {
|
||||
@@ -29,10 +47,19 @@ gitea_pr_create_api() {
|
||||
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 [[ "$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
|
||||
@@ -76,6 +103,7 @@ Options:
|
||||
-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
|
||||
@@ -116,6 +144,10 @@ while [[ $# -gt 0 ]]; do
|
||||
MILESTONE="$2"
|
||||
shift 2
|
||||
;;
|
||||
--login)
|
||||
LOGIN_OVERRIDE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-i|--issue)
|
||||
ISSUE="$2"
|
||||
shift 2
|
||||
@@ -174,15 +206,41 @@ case "$PLATFORM" in
|
||||
"${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)
|
||||
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 $?
|
||||
}
|
||||
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
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
#!/bin/bash
|
||||
# pr-merge.sh - Merge pull requests on Gitea or GitHub
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--co-author-trailers --escalate-to PRINCIPAL]
|
||||
# Usage: pr-merge.sh -n PR_NUMBER [-m squash] [-d] [--expect-head SHA] [--co-author-trailers --escalate-to PRINCIPAL] [--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 credential (per-slot token, fail-loud when
|
||||
# absent); the shared host credential is the last resort. The merge is
|
||||
# performed with the resolved credential only — never a cross-principal
|
||||
# fallback (an HTTP 401 from the identity-bound token is a hard stop).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -16,6 +23,7 @@ DRY_RUN=false
|
||||
EXPECT_HEAD=""
|
||||
CO_AUTHOR_TRAILERS=false
|
||||
ESCALATE_TO=""
|
||||
LOGIN_OVERRIDE=""
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
@@ -31,6 +39,7 @@ Options:
|
||||
--expect-head SHA Refuse unless the PR head matches this full commit SHA
|
||||
--co-author-trailers Build verified trailers from linked PR commit authors
|
||||
--escalate-to NAME Named principal for an unresolved-author BLOCK
|
||||
--login NAME Act as this Gitea tea login (wins over MOSAIC_GIT_IDENTITY)
|
||||
-h, --help Show this help message
|
||||
|
||||
Examples:
|
||||
@@ -39,6 +48,7 @@ Examples:
|
||||
$(basename "$0") -n 42 -d # Squash merge and delete branch
|
||||
$(basename "$0") -n 42 --expect-head 0123456789abcdef0123456789abcdef01234567
|
||||
$(basename "$0") -n 42 --co-author-trailers --escalate-to tl-mosaic
|
||||
$(basename "$0") -n 42 --login fred-ms # Merge under the fred-ms tea login
|
||||
EOF
|
||||
exit "${1:-1}"
|
||||
}
|
||||
@@ -82,6 +92,14 @@ while [[ $# -gt 0 ]]; do
|
||||
ESCALATE_TO="$2"
|
||||
shift 2
|
||||
;;
|
||||
--login|-l)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
echo "Error: --login requires one tea login name." >&2
|
||||
exit 1
|
||||
fi
|
||||
LOGIN_OVERRIDE="$2"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage 0
|
||||
;;
|
||||
@@ -572,9 +590,22 @@ PY
|
||||
merge_gitea_with_api() {
|
||||
local host="$1" token attempt_rc
|
||||
|
||||
if ! token=$(get_gitea_token "$host"); then
|
||||
echo "Error: Could not resolve the required Gitea token; refusing merge without changing principals." >&2
|
||||
return 1
|
||||
# Identity-first principal resolution (#1280): an explicit --login wins
|
||||
# over MOSAIC_GIT_IDENTITY (operator intent beats environment); otherwise
|
||||
# get_gitea_token resolves the identity's per-slot token when an identity
|
||||
# is requested (fail-loud when absent) and the shared host credential only
|
||||
# when no identity is set. No cross-principal fallback: whatever resolves
|
||||
# here is the ONLY credential the merge is attempted with.
|
||||
if [[ -n "$LOGIN_OVERRIDE" ]]; then
|
||||
if ! token=$(get_gitea_token_for_login "$LOGIN_OVERRIDE" "$host"); then
|
||||
echo "Error: --login '$LOGIN_OVERRIDE' has no host-matched token on host '$host'; refusing to merge under any other principal (#1280 identity-first resolution)." >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
if ! token=$(get_gitea_token "$host"); then
|
||||
echo "Error: Could not resolve the required Gitea token; refusing merge without changing principals." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
if [[ -z "$token" ]]; then
|
||||
echo "Error: Required Gitea token resolved empty; refusing merge without changing principals." >&2
|
||||
@@ -602,10 +633,25 @@ if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Error: Cannot determine host from origin remote URL" >&2
|
||||
exit 1
|
||||
}
|
||||
# Report the acting principal the merge WOULD use, resolved the same
|
||||
# way the real merge resolves it (#1280) — a dry run that names a
|
||||
# different principal than the merge would act as is a lie.
|
||||
if ! principal_resolved="$(resolve_gitea_principal "$LOGIN_OVERRIDE" "$HOST")"; then
|
||||
# Fail-loud diagnostic already printed (unresolvable --login or a
|
||||
# requested identity with no per-slot token).
|
||||
exit 1
|
||||
fi
|
||||
DRY_PRINCIPAL_MODE="$(printf '%s' "$principal_resolved" | cut -f1)"
|
||||
DRY_PRINCIPAL_NAME="$(printf '%s' "$principal_resolved" | cut -f2)"
|
||||
case "$DRY_PRINCIPAL_MODE" in
|
||||
login) DRY_PRINCIPAL_DESC="tea login '$DRY_PRINCIPAL_NAME'" ;;
|
||||
identity) DRY_PRINCIPAL_DESC="git identity '$DRY_PRINCIPAL_NAME' (per-slot credential)" ;;
|
||||
*) DRY_PRINCIPAL_DESC="default host credential" ;;
|
||||
esac
|
||||
if [[ "$CO_AUTHOR_TRAILERS" == true ]]; then
|
||||
echo "Dry run: would verify PR commit authors and merge PR #$PR_NUMBER on $HOST with authenticated Gitea API message fields (base=$BASE_BRANCH, method=squash)."
|
||||
echo "Dry run: would verify PR commit authors and merge PR #$PR_NUMBER on $HOST as $DRY_PRINCIPAL_DESC with authenticated Gitea API message fields (base=$BASE_BRANCH, method=squash)."
|
||||
else
|
||||
echo "Dry run: would merge PR #$PR_NUMBER on $HOST with the authenticated exact-head Gitea API path (base=$BASE_BRANCH, method=squash)."
|
||||
echo "Dry run: would merge PR #$PR_NUMBER on $HOST as $DRY_PRINCIPAL_DESC with the authenticated exact-head Gitea API path (base=$BASE_BRANCH, method=squash)."
|
||||
fi
|
||||
else
|
||||
echo "Dry run: would merge PR #$PR_NUMBER on $PLATFORM (base=$BASE_BRANCH, method=squash)."
|
||||
|
||||
@@ -76,7 +76,7 @@ while [[ $# -gt 0 ]]; do
|
||||
echo " -n, --number PR number (required)"
|
||||
echo " -a, --action Review action: approve, request-changes, comment (required)"
|
||||
echo " -c, --comment Review comment (required for request-changes)"
|
||||
echo " -l, --login Override the detected Gitea tea login (approve/request-changes only)"
|
||||
echo " -l, --login Override the detected Gitea tea login (all actions; wins over MOSAIC_GIT_IDENTITY)"
|
||||
echo " -r, --repo Explicit owner/repo slug (skips git-remote slug inference)"
|
||||
echo " -H, --host Explicit Gitea host (skips remote-host inference)"
|
||||
echo " -h, --help Show this help"
|
||||
@@ -346,7 +346,14 @@ gitea_resolve_api_for_login() {
|
||||
else
|
||||
host=$(get_remote_host)
|
||||
fi
|
||||
if [[ -n "$override_explicit" ]]; then
|
||||
if [[ "$override_explicit" == "identity" ]]; then
|
||||
# Requested git identity (#1280): the per-slot token MUST resolve via
|
||||
# get_gitea_token's identity arm; never borrow the tea default login.
|
||||
GITEA_API_TOKEN=$(get_gitea_token "$host") || {
|
||||
echo "Error: could not resolve the per-slot token for requested git identity '$effective_login' on host '$host'; refusing to fall back to the tea login list or shared credentials (review write/read-back, #1280)." >&2
|
||||
return 1
|
||||
}
|
||||
elif [[ -n "$override_explicit" ]]; then
|
||||
GITEA_API_TOKEN=$(get_gitea_token_for_login "$effective_login" "$host") || {
|
||||
echo "Error: could not resolve a host-matched Gitea token for --login '$effective_login' on host '$host'; refusing to fall back to the host default identity or a cross-host credential (review write/read-back)" >&2
|
||||
return 1
|
||||
@@ -676,29 +683,32 @@ if [[ "$PLATFORM" == "github" ]]; then
|
||||
;;
|
||||
esac
|
||||
elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
# Resolve the acting principal ONCE for every action, 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 (fail-loud when it does not); the tea login list is
|
||||
# the LAST resort — it enumerates whatever logins this host happens to hold
|
||||
# and knows nothing about which seat is calling, so resolving from it first
|
||||
# wrote under whichever account tea had configured (the #1280 family).
|
||||
principal_host="${HOST_OVERRIDE:-$(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)"
|
||||
case $ACTION in
|
||||
approve)
|
||||
# Best-effort host for the tea-login GUESS only (gitea_resolve_api_for_login
|
||||
# below re-derives the real host from HOST_OVERRIDE/remote independently and
|
||||
# is authoritative). Prefer an explicit -H/--host; otherwise best-effort
|
||||
# git-remote inference, tolerating its ABSENCE (a bare `get_remote_host` here
|
||||
# under `set -e`, with no origin and no -H, previously killed the script
|
||||
# SILENTLY — exit 1, zero output — even though -r/-H are exactly the flags
|
||||
# that support running with no usable origin at all).
|
||||
host="${HOST_OVERRIDE:-$(get_remote_host 2>/dev/null || true)}"
|
||||
# A --login override always wins. Otherwise name this host's login
|
||||
# only as a best effort: the login name merely selects a per-login
|
||||
# token, and gitea_resolve_api_for_login falls back to the host
|
||||
# credential (get_gitea_token) when no tea login is named — so a host
|
||||
# tea's login list need not enumerate exotic (e.g. ported) hosts for
|
||||
# the default credential to resolve. The single resolved token is
|
||||
# then used for the write, the /user identity, and the read-back.
|
||||
EFFECTIVE_LOGIN="$LOGIN_OVERRIDE"
|
||||
[[ -n "$EFFECTIVE_LOGIN" ]] || EFFECTIVE_LOGIN=$(get_gitea_login_for_host "$host" 2>/dev/null || true)
|
||||
# Bind the REST endpoint + token to the effective login, then derive
|
||||
# the acting identity from that SAME credential so the review submit
|
||||
# and its read-back verify against the identity that performed them.
|
||||
gitea_resolve_api_for_login "$EFFECTIVE_LOGIN" "${LOGIN_OVERRIDE:+explicit}" || exit 1
|
||||
# Identity-first principal resolution (#1280): PRINCIPAL_MODE /
|
||||
# PRINCIPAL_NAME were resolved once above from --login >
|
||||
# MOSAIC_GIT_IDENTITY / git config > tea login list (last resort).
|
||||
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" identity || exit 1
|
||||
elif [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" explicit || exit 1
|
||||
else
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" "" || exit 1
|
||||
fi
|
||||
ACTING_LOGIN=$(gitea_authenticated_login) || exit 1
|
||||
head_sha=$(gitea_pr_head_sha "$PR_NUMBER") || exit 1
|
||||
# The review body (if any) travels with the review itself in the REST
|
||||
@@ -715,24 +725,16 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
echo "Error: Comment required for request-changes"
|
||||
exit 1
|
||||
fi
|
||||
# Best-effort host for the tea-login GUESS only (gitea_resolve_api_for_login
|
||||
# below re-derives the real host from HOST_OVERRIDE/remote independently and
|
||||
# is authoritative). Prefer an explicit -H/--host; otherwise best-effort
|
||||
# git-remote inference, tolerating its ABSENCE (a bare `get_remote_host` here
|
||||
# under `set -e`, with no origin and no -H, previously killed the script
|
||||
# SILENTLY — exit 1, zero output — even though -r/-H are exactly the flags
|
||||
# that support running with no usable origin at all).
|
||||
host="${HOST_OVERRIDE:-$(get_remote_host 2>/dev/null || true)}"
|
||||
# A --login override always wins. Otherwise name this host's login
|
||||
# only as a best effort: the login name merely selects a per-login
|
||||
# token, and gitea_resolve_api_for_login falls back to the host
|
||||
# credential (get_gitea_token) when no tea login is named — so a host
|
||||
# tea's login list need not enumerate exotic (e.g. ported) hosts for
|
||||
# the default credential to resolve. The single resolved token is
|
||||
# then used for the write, the /user identity, and the read-back.
|
||||
EFFECTIVE_LOGIN="$LOGIN_OVERRIDE"
|
||||
[[ -n "$EFFECTIVE_LOGIN" ]] || EFFECTIVE_LOGIN=$(get_gitea_login_for_host "$host" 2>/dev/null || true)
|
||||
gitea_resolve_api_for_login "$EFFECTIVE_LOGIN" "${LOGIN_OVERRIDE:+explicit}" || exit 1
|
||||
# Identity-first principal resolution (#1280): PRINCIPAL_MODE /
|
||||
# PRINCIPAL_NAME were resolved once above from --login >
|
||||
# MOSAIC_GIT_IDENTITY / git config > tea login list (last resort).
|
||||
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" identity || exit 1
|
||||
elif [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" explicit || exit 1
|
||||
else
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" "" || exit 1
|
||||
fi
|
||||
ACTING_LOGIN=$(gitea_authenticated_login) || exit 1
|
||||
head_sha=$(gitea_pr_head_sha "$PR_NUMBER") || exit 1
|
||||
review_id=$(gitea_submit_review_verified "$PR_NUMBER" "REQUEST_CHANGES" "$COMMENT" "$ACTING_LOGIN" "$head_sha") || {
|
||||
@@ -746,24 +748,16 @@ elif [[ "$PLATFORM" == "gitea" ]]; then
|
||||
echo "Error: Comment required"
|
||||
exit 1
|
||||
fi
|
||||
# Best-effort host for the tea-login GUESS only (gitea_resolve_api_for_login
|
||||
# below re-derives the real host from HOST_OVERRIDE/remote independently and
|
||||
# is authoritative). Prefer an explicit -H/--host; otherwise best-effort
|
||||
# git-remote inference, tolerating its ABSENCE (a bare `get_remote_host` here
|
||||
# under `set -e`, with no origin and no -H, previously killed the script
|
||||
# SILENTLY — exit 1, zero output — even though -r/-H are exactly the flags
|
||||
# that support running with no usable origin at all).
|
||||
host="${HOST_OVERRIDE:-$(get_remote_host 2>/dev/null || true)}"
|
||||
# A --login override always wins. Otherwise name this host's login
|
||||
# only as a best effort: the login name merely selects a per-login
|
||||
# token, and gitea_resolve_api_for_login falls back to the host
|
||||
# credential (get_gitea_token) when no tea login is named — so a host
|
||||
# tea's login list need not enumerate exotic (e.g. ported) hosts for
|
||||
# the default credential to resolve. The single resolved token is
|
||||
# then used for the write, the /user identity, and the read-back.
|
||||
EFFECTIVE_LOGIN="$LOGIN_OVERRIDE"
|
||||
[[ -n "$EFFECTIVE_LOGIN" ]] || EFFECTIVE_LOGIN=$(get_gitea_login_for_host "$host" 2>/dev/null || true)
|
||||
gitea_resolve_api_for_login "$EFFECTIVE_LOGIN" "${LOGIN_OVERRIDE:+explicit}" || exit 1
|
||||
# Identity-first principal resolution (#1280): PRINCIPAL_MODE /
|
||||
# PRINCIPAL_NAME were resolved once above from --login >
|
||||
# MOSAIC_GIT_IDENTITY / git config > tea login list (last resort).
|
||||
if [[ "$PRINCIPAL_MODE" == "identity" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" identity || exit 1
|
||||
elif [[ "$PRINCIPAL_MODE" == "login" ]]; then
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" explicit || exit 1
|
||||
else
|
||||
gitea_resolve_api_for_login "$PRINCIPAL_NAME" "" || exit 1
|
||||
fi
|
||||
ACTING_LOGIN=$(gitea_authenticated_login) || exit 1
|
||||
comment_id=$(gitea_create_comment_verified "$PR_NUMBER" "$COMMENT" "$ACTING_LOGIN") || {
|
||||
echo "Error: could not create and verify a comment on Gitea PR #$PR_NUMBER via a provider-returned created id (#865)." >&2
|
||||
|
||||
@@ -0,0 +1,255 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for detect-platform.sh's resolve_gitea_principal() — the
|
||||
# identity-first acting-principal resolution shared by the git wrappers
|
||||
# (mosaicstack/stack #1280).
|
||||
#
|
||||
# The contract under test (precedence: --login > MOSAIC_GIT_IDENTITY /
|
||||
# git config mosaic.gitIdentity > tea login list, which is the LAST resort):
|
||||
# 1. identity env + per-slot token present -> mode=identity, principal=
|
||||
# identity name, source names the identity's slot PATH (never a token
|
||||
# value).
|
||||
# 2. identity env + per-slot token ABSENT -> FAIL LOUD: nonzero, empty
|
||||
# stdout, stderr naming the identity and the expected slot path.
|
||||
# 3. identity env + --login -> --login wins (login mode resolves even when
|
||||
# the identity has no slot — operator intent beats environment).
|
||||
# 4. identity unset + no --login -> default mode: the tea login list
|
||||
# resolves the principal exactly as before (preserved behavior).
|
||||
# 5. no identity + no host-matching tea login -> default/host-credential
|
||||
# (preserved behavior; absence is not an error on the default path).
|
||||
# 6. identity on an UNRECOGNIZED host (no per-slot scheme) -> does not bind;
|
||||
# default mode (containment, mirroring get_gitea_token).
|
||||
# 7. --login with no host-bound token for that login -> FAIL LOUD, stderr
|
||||
# naming the login and the host.
|
||||
# 8. git config mosaic.gitIdentity is honored when the env var is unset.
|
||||
# 9. The resolver NEVER emits a token value — stdout/stderr of every
|
||||
# successful resolution must not contain the slot file's contents.
|
||||
#
|
||||
# Uses a stubbed tea binary, stubbed tea config.yml, stubbed credentials.json
|
||||
# and stubbed per-slot token files under a fake HOME. NEVER reads real secrets.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/gitea-principal-resolution}"
|
||||
FAKE_HOME="$WORK_DIR/home"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$FAKE_HOME/.config/tea" "$REPO_DIR" "$BIN_DIR"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{
|
||||
"gitea": {
|
||||
"mosaicstack": {
|
||||
"url": "https://git.mosaicstack.dev",
|
||||
"token": "shared-mosaicstack-token"
|
||||
},
|
||||
"usc": {
|
||||
"url": "https://git.uscllc.com",
|
||||
"token": "shared-usc-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
# tea's own config store: the source get_gitea_token_for_login reads. Logins
|
||||
# "alice" (mosaicstack) and "bob-usc" (usc) carry sentinel token values that
|
||||
# the assertions prove are NEVER emitted by the resolver.
|
||||
cat > "$FAKE_HOME/.config/tea/config.yml" <<'YAML'
|
||||
logins:
|
||||
- name: alice
|
||||
url: https://git.mosaicstack.dev
|
||||
token: SECRET-alice-tea-token
|
||||
- name: bob-usc
|
||||
url: https://git.uscllc.com
|
||||
token: SECRET-bob-usc-tea-token
|
||||
YAML
|
||||
|
||||
# Stubbed tea: only what login resolution needs (`login list --output json`).
|
||||
cat > "$BIN_DIR/tea" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$*" == "login list --output json" ]]; then
|
||||
cat <<'JSON'
|
||||
[
|
||||
{"name":"alice","url":"https://git.mosaicstack.dev","default":true},
|
||||
{"name":"bob-usc","url":"https://git.uscllc.com"}
|
||||
]
|
||||
JSON
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$BIN_DIR/tea"
|
||||
|
||||
# Per-slot identity token with a sentinel value the assertions prove is never
|
||||
# emitted (proving "token came from the identity's slot BY PATH, not by value").
|
||||
echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token"
|
||||
|
||||
fail=0
|
||||
assert_eq() {
|
||||
local desc="$1" expected="$2" actual="$3"
|
||||
if [[ "$expected" != "$actual" ]]; then
|
||||
echo "FAIL: $desc — expected '$expected', got '$actual'" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
assert_contains() {
|
||||
local desc="$1" haystack="$2" needle="$3"
|
||||
if [[ "$haystack" != *"$needle"* ]]; then
|
||||
echo "FAIL: $desc — missing '$needle' in: $haystack" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
assert_not_contains() {
|
||||
local desc="$1" haystack="$2" needle="$3"
|
||||
if [[ "$haystack" == *"$needle"* ]]; then
|
||||
echo "FAIL: $desc — must not contain '$needle', got: $haystack" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
# Runs resolve_gitea_principal for $1=login_override $2=host inside REPO_DIR
|
||||
# (per-worktree git config resolves there) under a fake HOME, stubbed tea, and
|
||||
# stubbed credentials. Extra env (e.g. MOSAIC_GIT_IDENTITY) via $@.
|
||||
call_resolver() {
|
||||
local login="$1" host="$2"; shift 2
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:$PATH" \
|
||||
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" \
|
||||
DETECT_PLATFORM_SH="$SCRIPT_DIR/detect-platform.sh" "$@" \
|
||||
bash -c 'source "$DETECT_PLATFORM_SH"; resolve_gitea_principal "$1" "$2"' _ "$login" "$host"
|
||||
)
|
||||
}
|
||||
|
||||
field() { printf '%s' "$1" | cut -f"$2"; }
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Identity env + slot present -> identity mode, slot named BY PATH, and no
|
||||
# token value ever emitted.
|
||||
# ---------------------------------------------------------------------------
|
||||
git -C "$REPO_DIR" config --unset mosaic.gitIdentity 2>/dev/null || true
|
||||
out=$(call_resolver "" "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentX)
|
||||
assert_eq "identity mode" "identity" "$(field "$out" 1)"
|
||||
assert_eq "identity principal" "agentX" "$(field "$out" 2)"
|
||||
assert_eq "identity slot source" \
|
||||
"identity-slot:$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token" \
|
||||
"$(field "$out" 3)"
|
||||
assert_not_contains "identity stdout leaks token" "$out" "SECRET"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Identity env + slot ABSENT -> fail loud: nonzero, empty stdout, stderr
|
||||
# naming the identity and the expected slot path.
|
||||
# ---------------------------------------------------------------------------
|
||||
stderr_file="$WORK_DIR/stderr.tmp"
|
||||
set +e
|
||||
out=$(call_resolver "" "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: missing slot — expected nonzero return, got 0 (stdout='$out')" >&2
|
||||
fail=1
|
||||
fi
|
||||
if [[ -n "$out" ]]; then
|
||||
echo "FAIL: missing slot — expected empty stdout, got '$out'" >&2
|
||||
fail=1
|
||||
fi
|
||||
err=$(cat "$stderr_file")
|
||||
assert_contains "missing slot names identity" "$err" "agentNoSlot"
|
||||
assert_contains "missing slot names slot path" "$err" \
|
||||
"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"
|
||||
assert_not_contains "missing-slot stderr leaks token" "$err" "SECRET"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Identity + --login -> --login wins. Also wins when the identity has NO
|
||||
# slot (no identity check may veto an explicit login).
|
||||
# ---------------------------------------------------------------------------
|
||||
out=$(call_resolver "alice" "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentX)
|
||||
assert_eq "login beats identity (mode)" "login" "$(field "$out" 1)"
|
||||
assert_eq "login beats identity (principal)" "alice" "$(field "$out" 2)"
|
||||
assert_eq "login source" "tea-login:alice" "$(field "$out" 3)"
|
||||
out=$(call_resolver "alice" "git.mosaicstack.dev" MOSAIC_GIT_IDENTITY=agentNoSlot)
|
||||
assert_eq "login beats slot-less identity" "login" "$(field "$out" 1)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. No identity, no --login -> default mode via the tea login list
|
||||
# (preserved behavior).
|
||||
# ---------------------------------------------------------------------------
|
||||
out=$(call_resolver "" "git.mosaicstack.dev")
|
||||
assert_eq "default mode" "default" "$(field "$out" 1)"
|
||||
assert_eq "default principal" "alice" "$(field "$out" 2)"
|
||||
assert_eq "default source" "tea-default" "$(field "$out" 3)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 5. No identity, no --login, no host-matching tea login -> default with the
|
||||
# host credential (absence is not an error on the default path).
|
||||
# ---------------------------------------------------------------------------
|
||||
out=$(call_resolver "" "git.unknown.test")
|
||||
assert_eq "no-match default mode" "default" "$(field "$out" 1)"
|
||||
assert_eq "no-match default principal" "" "$(field "$out" 2)"
|
||||
assert_eq "no-match default source" "host-credential" "$(field "$out" 3)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 6. Identity on an UNRECOGNIZED host -> does not bind; default mode
|
||||
# (containment, mirroring get_gitea_token's scope).
|
||||
# ---------------------------------------------------------------------------
|
||||
out=$(call_resolver "" "github.com" MOSAIC_GIT_IDENTITY=agentX)
|
||||
assert_eq "unrecognized host falls to default" "default" "$(field "$out" 1)"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 7. --login with no host-bound token for that login -> fail loud, stderr
|
||||
# naming the login and the host.
|
||||
# ---------------------------------------------------------------------------
|
||||
: > "$stderr_file"
|
||||
set +e
|
||||
out=$(call_resolver "ghost-login" "git.mosaicstack.dev" 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: unknown --login — expected nonzero return, got 0 (stdout='$out')" >&2
|
||||
fail=1
|
||||
fi
|
||||
err=$(cat "$stderr_file")
|
||||
assert_contains "unknown login names login" "$err" "ghost-login"
|
||||
assert_contains "unknown login names host" "$err" "git.mosaicstack.dev"
|
||||
# A cross-host login (exists, but for usc) must ALSO fail loud for mosaicstack.
|
||||
set +e
|
||||
out=$(call_resolver "bob-usc" "git.mosaicstack.dev" 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: cross-host --login — expected nonzero return, got 0" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 8. git config mosaic.gitIdentity honored when env is unset.
|
||||
# ---------------------------------------------------------------------------
|
||||
git -C "$REPO_DIR" config mosaic.gitIdentity agentX
|
||||
out=$(call_resolver "" "git.mosaicstack.dev")
|
||||
assert_eq "git-config identity mode" "identity" "$(field "$out" 1)"
|
||||
assert_eq "git-config identity principal" "agentX" "$(field "$out" 2)"
|
||||
git -C "$REPO_DIR" config --unset mosaic.gitIdentity
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 9. Cross-host slot layout: the usc slot path is chosen for the usc host.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo -n "SECRET-agentX-usc-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentX.token"
|
||||
out=$(call_resolver "" "git.uscllc.com" MOSAIC_GIT_IDENTITY=agentX)
|
||||
assert_eq "usc identity mode" "identity" "$(field "$out" 1)"
|
||||
assert_eq "usc slot source" \
|
||||
"identity-slot:$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-usc-agentX.token" \
|
||||
"$(field "$out" 3)"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "resolve_gitea_principal identity-first resolution regression passed"
|
||||
fi
|
||||
|
||||
exit "$fail"
|
||||
@@ -0,0 +1,244 @@
|
||||
#!/usr/bin/env bash
|
||||
# Load-bearing regression harness for pr-create.sh identity-first principal
|
||||
# resolution (mosaicstack/stack #1280).
|
||||
#
|
||||
# The failure this harness is written down to catch: `MOSAIC_GIT_IDENTITY=fargo
|
||||
# pr-create.sh …` produces a PR attributed to `mos-dt-0` (whichever account the
|
||||
# tea login list happens to hold). Before #1280 the identity-aware code existed
|
||||
# but sat on the API arm that only ran when the tea path FAILED — tea succeeded,
|
||||
# so the identity arm never executed, and every test that did not check ORDERING
|
||||
# passed. This harness checks ordering directly:
|
||||
#
|
||||
# 1. identity set + slot present -> the PR is created via the REST API with
|
||||
# the identity's per-slot token (asserted by sentinel value AT the fake
|
||||
# provider), and tea's `pr create` is NEVER invoked.
|
||||
# 2. identity set + slot ABSENT -> nonzero, stderr naming the identity and
|
||||
# the expected slot path; neither tea `pr create` nor any API request
|
||||
# fires. No silent fallback to the tea login list.
|
||||
# 3. identity set + --login -> --login wins: tea runs WITH the explicit
|
||||
# --login, no API request.
|
||||
# 4. nothing set -> preserved behavior: tea path with the tea-list login.
|
||||
#
|
||||
# Uses a stubbed tea, a stubbed curl provider, stubbed credentials.json and
|
||||
# per-slot token under a fake HOME. NEVER reads real secrets or hits a live
|
||||
# forge — all assertions are against the stubs' logs.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-create-identity-first}"
|
||||
FAKE_HOME="$WORK_DIR/home"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
TOOLS_DIR="$WORK_DIR/tools"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
LOG_FILE="$WORK_DIR/calls.log"
|
||||
CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json"
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$FAKE_HOME/.config/tea" \
|
||||
"$REPO_DIR" "$TOOLS_DIR/git" "$TOOLS_DIR/_lib" "$BIN_DIR"
|
||||
|
||||
# Fixture: the real scripts under test, copied so sibling stubs (and the
|
||||
# ../_lib credential loader) resolve inside the fixture tree.
|
||||
cp "$SCRIPT_DIR/pr-create.sh" "$TOOLS_DIR/git/pr-create.sh"
|
||||
cp "$SCRIPT_DIR/detect-platform.sh" "$TOOLS_DIR/git/detect-platform.sh"
|
||||
cp "$SCRIPT_DIR/../_lib/credentials.sh" "$TOOLS_DIR/_lib/credentials.sh"
|
||||
chmod +x "$TOOLS_DIR/git/pr-create.sh"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{
|
||||
"gitea": {
|
||||
"mosaicstack": {
|
||||
"url": "https://git.mosaicstack.dev",
|
||||
"token": "shared-mosaicstack-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$FAKE_HOME/.config/tea/config.yml" <<'YAML'
|
||||
logins:
|
||||
- name: alice
|
||||
url: https://git.mosaicstack.dev
|
||||
token: SECRET-alice-tea-token
|
||||
YAML
|
||||
|
||||
echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token"
|
||||
|
||||
: > "$LOG_FILE"
|
||||
|
||||
# Stubbed tea: records every invocation; `login list` feeds login resolution;
|
||||
# `api --login <n> /user` feeds get_gitea_authenticated_user; `pr create` marks
|
||||
# the marker file (its presence fails the identity-mode assertions).
|
||||
cat > "$BIN_DIR/tea" <<SH
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf 'TEA: %s\n' "\$*" >> "$LOG_FILE"
|
||||
if [[ "\$*" == "login list --output json" ]]; then
|
||||
cat <<'JSON'
|
||||
[
|
||||
{"name":"alice","url":"https://git.mosaicstack.dev","default":true}
|
||||
]
|
||||
JSON
|
||||
exit 0
|
||||
fi
|
||||
if [[ "\${1:-}" == "api" ]]; then
|
||||
printf '%s\n' '{"login":"alice"}'
|
||||
exit 0
|
||||
fi
|
||||
if [[ "\$*" == pr\ create* ]]; then
|
||||
echo "TEA-PR-CREATE-INVOKED" >> "$LOG_FILE"
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$BIN_DIR/tea"
|
||||
|
||||
# Stubbed provider: records the URL and the Authorization header VALUE it
|
||||
# received, answers 201 with a created-PR object. The sentinel token values are
|
||||
# synthetic fixtures — asserting them at the provider proves WHICH slot's
|
||||
# credential carried the write.
|
||||
cat > "$BIN_DIR/curl" <<SH
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
url=""
|
||||
auth=""
|
||||
while [[ \$# -gt 0 ]]; do
|
||||
case "\$1" in
|
||||
-H)
|
||||
case "\$2" in
|
||||
Authorization*) auth="\$2" ;;
|
||||
esac
|
||||
shift 2
|
||||
;;
|
||||
*) [[ -n "\$1" && "\$1" != -* ]] && url="\$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
printf 'CURL-URL: %s\nCURL-AUTH: %s\n' "\$url" "\$auth" >> "$LOG_FILE"
|
||||
cat <<'JSON'
|
||||
{"number": 1299, "html_url": "https://git.mosaicstack.dev/mosaicstack/stack/pulls/1299"}
|
||||
JSON
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$BIN_DIR/curl"
|
||||
|
||||
fail=0
|
||||
assert_contains() {
|
||||
local desc="$1" needle="$2"
|
||||
if ! grep -qF -- "$needle" "$LOG_FILE"; then
|
||||
echo "FAIL: $desc — log does not contain '$needle':" >&2
|
||||
cat "$LOG_FILE" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
assert_not_contains() {
|
||||
local desc="$1" needle="$2"
|
||||
if grep -qF -- "$needle" "$LOG_FILE"; then
|
||||
echo "FAIL: $desc — log must not contain '$needle':" >&2
|
||||
cat "$LOG_FILE" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
EXTRA_ARGS=""
|
||||
run_pr_create() {
|
||||
# "$@" carries ONLY environment assignments (VAR=value); EXTRA_ARGS (if
|
||||
# set) carries wrapper arguments, so `env` never mistakes a wrapper flag
|
||||
# like --login for one of its own.
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
# shellcheck disable=SC2086 # EXTRA_ARGS is deliberately word-split wrapper args
|
||||
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:$PATH" \
|
||||
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" "$@" \
|
||||
bash "$TOOLS_DIR/git/pr-create.sh" -t "Test PR" -B next -H fix/test $EXTRA_ARGS
|
||||
)
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. HAPPY PATH (the load-bearing ordering test): identity set + slot present
|
||||
# -> REST API with the per-slot token; tea `pr create` NEVER invoked.
|
||||
# ---------------------------------------------------------------------------
|
||||
set +e
|
||||
out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentX 2>"$WORK_DIR/stderr-1.tmp")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
echo "FAIL: identity happy path — expected rc=0, got $rc" >&2
|
||||
cat "$WORK_DIR/stderr-1.tmp" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_contains "identity happy path reaches the API" "CURL-URL: https://git.mosaicstack.dev/api/v1/repos/mosaicstack/stack/pulls"
|
||||
assert_contains "identity happy path carries the slot token" "CURL-AUTH: Authorization: token SECRET-agentX-slot-token"
|
||||
assert_not_contains "identity happy path must NOT invoke tea pr create" "TEA-PR-CREATE-INVOKED"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. Identity set + slot ABSENT -> fail loud BEFORE any write: nonzero, stderr
|
||||
# naming identity + slot path, no tea pr create, no API request.
|
||||
# ---------------------------------------------------------------------------
|
||||
: > "$LOG_FILE"
|
||||
set +e
|
||||
out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$WORK_DIR/stderr-2.tmp")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: missing slot — expected nonzero return, got 0 (stdout='$out')" >&2
|
||||
fail=1
|
||||
fi
|
||||
err=$(cat "$WORK_DIR/stderr-2.tmp")
|
||||
if [[ "$err" != *"agentNoSlot"* ]]; then
|
||||
echo "FAIL: missing slot — stderr does not name the identity:" >&2
|
||||
echo "$err" >&2
|
||||
fail=1
|
||||
fi
|
||||
if [[ "$err" != *"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"* ]]; then
|
||||
echo "FAIL: missing slot — stderr does not name the expected slot path:" >&2
|
||||
echo "$err" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_not_contains "missing slot must not reach tea pr create" "TEA-PR-CREATE-INVOKED"
|
||||
assert_not_contains "missing slot must not reach the API" "CURL-URL"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. Identity set + --login -> --login wins: tea runs WITH the explicit login.
|
||||
# ---------------------------------------------------------------------------
|
||||
: > "$LOG_FILE"
|
||||
EXTRA_ARGS="--login alice"
|
||||
set +e
|
||||
out=$(run_pr_create MOSAIC_GIT_IDENTITY=agentX 2>"$WORK_DIR/stderr-3.tmp")
|
||||
rc=$?
|
||||
set -e
|
||||
EXTRA_ARGS=""
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
echo "FAIL: login override — expected rc=0, got $rc" >&2
|
||||
cat "$WORK_DIR/stderr-3.tmp" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_contains "login override drives tea with the explicit login" "TEA: pr create --repo mosaicstack/stack --login alice"
|
||||
assert_not_contains "login override must not hit the API" "CURL-URL"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 4. Nothing set -> preserved behavior: tea path with the tea-list login.
|
||||
# ---------------------------------------------------------------------------
|
||||
: > "$LOG_FILE"
|
||||
set +e
|
||||
out=$(run_pr_create 2>"$WORK_DIR/stderr-4.tmp")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
echo "FAIL: default path — expected rc=0, got $rc" >&2
|
||||
cat "$WORK_DIR/stderr-4.tmp" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_contains "default path still uses the tea-list login" "TEA: pr create --repo mosaicstack/stack --login alice"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "pr-create identity-first happy-path regression passed"
|
||||
fi
|
||||
|
||||
exit "$fail"
|
||||
@@ -0,0 +1,247 @@
|
||||
#!/usr/bin/env bash
|
||||
# Regression harness for pr-merge.sh identity-first principal resolution
|
||||
# (mosaicstack/stack #1280).
|
||||
#
|
||||
# Covers:
|
||||
# 1. --dry-run reports the acting principal the merge WOULD use, resolved the
|
||||
# same way the real merge resolves it: --login > MOSAIC_GIT_IDENTITY /
|
||||
# git config mosaic.gitIdentity > shared host credential. (The pre-#1280
|
||||
# deployed copy reported a tea login that the merge would not act as.)
|
||||
# 2. --dry-run fails closed when the requested principal has no credential:
|
||||
# unknown --login, or an identity with no per-slot token (stderr names
|
||||
# the login / the identity and its slot path).
|
||||
# 3. The real merge POST carries the resolved principal's credential and no
|
||||
# other: --login merges with that login's tea-config token; an identity
|
||||
# merges with the per-slot token; an unresolvable --login never reaches
|
||||
# the provider.
|
||||
#
|
||||
# Fixture pattern from test-pr-merge-head-pin.sh: the scripts under test are
|
||||
# copied into a fixture tree with stubbed pr-metadata.sh / ci-queue-wait.sh
|
||||
# siblings; the provider is a stubbed curl that records the credential it
|
||||
# received. NEVER reads real secrets or hits a live forge.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
WORK_DIR="${MOSAIC_TEST_WORK_DIR:-$PWD/.mosaic-test-work/pr-merge-principal-resolution}"
|
||||
FAKE_HOME="$WORK_DIR/home"
|
||||
REPO_DIR="$WORK_DIR/repo"
|
||||
TOOLS_DIR="$WORK_DIR/tools"
|
||||
BIN_DIR="$WORK_DIR/bin"
|
||||
LOG_FILE="$WORK_DIR/calls.log"
|
||||
CREDENTIALS_FILE="$FAKE_HOME/.config/mosaic/credentials.json"
|
||||
SHA=0123456789abcdef0123456789abcdef01234567
|
||||
|
||||
rm -rf "$WORK_DIR"
|
||||
mkdir -p "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens" "$FAKE_HOME/.config/tea" \
|
||||
"$REPO_DIR" "$TOOLS_DIR/git" "$TOOLS_DIR/_lib" "$BIN_DIR"
|
||||
|
||||
cp "$SCRIPT_DIR/pr-merge.sh" "$TOOLS_DIR/git/pr-merge.sh"
|
||||
cp "$SCRIPT_DIR/detect-platform.sh" "$TOOLS_DIR/git/detect-platform.sh"
|
||||
cp "$SCRIPT_DIR/../_lib/credentials.sh" "$TOOLS_DIR/_lib/credentials.sh"
|
||||
chmod +x "$TOOLS_DIR/git/pr-merge.sh"
|
||||
|
||||
git -C "$REPO_DIR" init -q
|
||||
git -C "$REPO_DIR" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
|
||||
|
||||
# Stubbed siblings pr-merge.sh resolves relative to its own SCRIPT_DIR.
|
||||
cat > "$TOOLS_DIR/git/pr-metadata.sh" <<SH
|
||||
#!/usr/bin/env bash
|
||||
printf '%s\n' '{"baseRefName":"next","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"mosaicstack/stack","title":"Test PR","author":{"login":"contributor"}}'
|
||||
SH
|
||||
cat > "$TOOLS_DIR/git/ci-queue-wait.sh" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$TOOLS_DIR/git/pr-metadata.sh" "$TOOLS_DIR/git/ci-queue-wait.sh"
|
||||
|
||||
cat > "$CREDENTIALS_FILE" <<'JSON'
|
||||
{
|
||||
"gitea": {
|
||||
"mosaicstack": {
|
||||
"url": "https://git.mosaicstack.dev",
|
||||
"token": "shared-mosaicstack-token"
|
||||
}
|
||||
}
|
||||
}
|
||||
JSON
|
||||
|
||||
cat > "$FAKE_HOME/.config/tea/config.yml" <<'YAML'
|
||||
logins:
|
||||
- name: fred-ms
|
||||
url: https://git.mosaicstack.dev
|
||||
token: SECRET-fred-ms-tea-token
|
||||
YAML
|
||||
|
||||
echo -n "SECRET-agentX-slot-token" > "$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentX.token"
|
||||
|
||||
: > "$LOG_FILE"
|
||||
|
||||
# Stubbed tea for login-list resolution only.
|
||||
cat > "$BIN_DIR/tea" <<'SH'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
if [[ "$*" == "login list --output json" ]]; then
|
||||
cat <<'JSON'
|
||||
[
|
||||
{"name":"fred-ms","url":"https://git.mosaicstack.dev","default":true}
|
||||
]
|
||||
JSON
|
||||
exit 0
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$BIN_DIR/tea"
|
||||
|
||||
# Stubbed provider. pr-merge passes curl config on STDIN with -K -; the stub
|
||||
# reads stdin, records the Authorization header it received, answers 200.
|
||||
cat > "$BIN_DIR/curl" <<SH
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
url=""
|
||||
out_file=""
|
||||
stdin_config=""
|
||||
if [[ ! -t 0 ]]; then
|
||||
stdin_config="\$(cat || true)"
|
||||
fi
|
||||
while [[ \$# -gt 0 ]]; do
|
||||
case "\$1" in
|
||||
-o) out_file="\$2"; shift 2 ;;
|
||||
-K|-w|--max-filesize|--max-time|--connect-timeout|-sS) shift 2 ;;
|
||||
*) [[ -n "\$1" && "\$1" != -* && -z "\$url" ]] && url="\$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
auth="\$(printf '%s' "\$stdin_config" | grep -o 'Authorization: token [^"]*' || true)"
|
||||
printf 'CURL-URL: %s\nCURL-AUTH: %s\n' "\$url" "\$auth" >> "$LOG_FILE"
|
||||
[[ -n "\$out_file" ]] && printf '{}' > "\$out_file"
|
||||
printf '200\n'
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$BIN_DIR/curl"
|
||||
|
||||
fail=0
|
||||
assert_contains_log() {
|
||||
local desc="$1" needle="$2"
|
||||
if ! grep -qF -- "$needle" "$LOG_FILE"; then
|
||||
echo "FAIL: $desc — log does not contain '$needle':" >&2
|
||||
cat "$LOG_FILE" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
assert_not_contains_log() {
|
||||
local desc="$1" needle="$2"
|
||||
if grep -qF -- "$needle" "$LOG_FILE"; then
|
||||
echo "FAIL: $desc — log must not contain '$needle':" >&2
|
||||
cat "$LOG_FILE" >&2
|
||||
fail=1
|
||||
fi
|
||||
}
|
||||
|
||||
run_pr_merge() {
|
||||
local extra_args="$1"; shift
|
||||
(
|
||||
cd "$REPO_DIR"
|
||||
# shellcheck disable=SC2086 # extra_args is deliberately word-split wrapper args
|
||||
env -i HOME="$FAKE_HOME" PATH="$BIN_DIR:$PATH" \
|
||||
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_SYSTEM=/dev/null \
|
||||
MOSAIC_CREDENTIALS_FILE="$CREDENTIALS_FILE" "$@" \
|
||||
bash "$TOOLS_DIR/git/pr-merge.sh" -n 42 $extra_args
|
||||
)
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. --dry-run reports the resolved acting principal truthfully.
|
||||
# ---------------------------------------------------------------------------
|
||||
out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentX)
|
||||
if [[ "$out" != *"as git identity 'agentX' (per-slot credential)"* ]]; then
|
||||
echo "FAIL: dry-run identity — principal not reported: $out" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
out=$(run_pr_merge "--dry-run --login fred-ms" MOSAIC_GIT_IDENTITY=agentX)
|
||||
if [[ "$out" != *"as tea login 'fred-ms'"* ]]; then
|
||||
echo "FAIL: dry-run login override — login not reported (must beat env identity): $out" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
out=$(run_pr_merge "--dry-run")
|
||||
if [[ "$out" != *"as default host credential"* ]]; then
|
||||
echo "FAIL: dry-run default — not reported: $out" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 2. --dry-run fails closed when the requested principal has no credential.
|
||||
# ---------------------------------------------------------------------------
|
||||
stderr_file="$WORK_DIR/stderr.tmp"
|
||||
set +e
|
||||
out=$(run_pr_merge "--dry-run --login ghost" 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]] || [[ "$(cat "$stderr_file")" != *"ghost"* ]]; then
|
||||
echo "FAIL: dry-run unknown --login — expected fail-loud naming 'ghost', rc=$rc" >&2
|
||||
cat "$stderr_file" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
: > "$stderr_file"
|
||||
set +e
|
||||
out=$(run_pr_merge "--dry-run" MOSAIC_GIT_IDENTITY=agentNoSlot 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
err=$(cat "$stderr_file")
|
||||
if [[ "$rc" -eq 0 ]] || [[ "$err" != *"agentNoSlot"* ]] \
|
||||
|| [[ "$err" != *"$FAKE_HOME/.config/mosaic/secrets/gitea-tokens/gitea-mosaicstack-agentNoSlot.token"* ]]; then
|
||||
echo "FAIL: dry-run identity without slot — expected fail-loud naming identity + slot path, rc=$rc" >&2
|
||||
echo "$err" >&2
|
||||
fail=1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 3. The real merge POST carries the resolved principal's credential ONLY.
|
||||
# ---------------------------------------------------------------------------
|
||||
: > "$LOG_FILE"
|
||||
set +e
|
||||
out=$(run_pr_merge "--login fred-ms" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
echo "FAIL: merge with --login — expected rc=0, got $rc" >&2
|
||||
cat "$stderr_file" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_contains_log "merge --login uses the login token" "CURL-AUTH: Authorization: token SECRET-fred-ms-tea-token"
|
||||
assert_not_contains_log "merge --login must not use the identity slot token" "SECRET-agentX-slot-token"
|
||||
assert_not_contains_log "merge --login must not use the shared token" "shared-mosaicstack-token"
|
||||
|
||||
: > "$LOG_FILE"
|
||||
set +e
|
||||
out=$(run_pr_merge "" MOSAIC_GIT_IDENTITY=agentX 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -ne 0 ]]; then
|
||||
echo "FAIL: merge with identity — expected rc=0, got $rc" >&2
|
||||
cat "$stderr_file" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_contains_log "merge identity uses the per-slot token" "CURL-AUTH: Authorization: token SECRET-agentX-slot-token"
|
||||
assert_not_contains_log "merge identity must not use the shared token" "shared-mosaicstack-token"
|
||||
|
||||
: > "$LOG_FILE"
|
||||
set +e
|
||||
out=$(run_pr_merge "--login ghost" 2>"$stderr_file")
|
||||
rc=$?
|
||||
set -e
|
||||
if [[ "$rc" -eq 0 ]]; then
|
||||
echo "FAIL: merge with unknown --login — expected nonzero, got 0" >&2
|
||||
fail=1
|
||||
fi
|
||||
assert_not_contains_log "merge with unknown --login must not reach the provider" "CURL-URL"
|
||||
|
||||
if [[ "$fail" -eq 0 ]]; then
|
||||
echo "pr-merge identity-first principal resolution regression passed"
|
||||
fi
|
||||
|
||||
exit "$fail"
|
||||
@@ -25,7 +25,7 @@
|
||||
"lint": "eslint src",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"test": "vitest run --passWithNoTests && pnpm run test:framework-shell",
|
||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh"
|
||||
"test:framework-shell": "bash framework/tools/quality/scripts/check-test-enumeration.sh && bash framework/tools/quality/scripts/test-check-test-enumeration.sh && python3 src/lease-broker/daemon_deadline_unittest.py && python3 src/lease-broker/normative_fragments_unittest.py && python3 src/lease-broker/promotion_binding_unittest.py && python3 src/lease-broker/promotion_trigger_unittest.py && python3 src/lease-broker/receipt_challenge_unittest.py && python3 src/lease-broker/context_recovery_unittest.py && python3 src/lease-broker/recovery_runtime_unittest.py && python3 src/lease-broker/recovery_b1_adversarial_unittest.py && python3 src/lease-broker/receipt_observer_client_unittest.py && python3 src/lease-broker/invariant_r_unittest.py && python3 src/lease-broker/framework_skill_portability_unittest.py && python3 src/mutator-gate/runtime_tools_unittest.py && python3 src/mutator-gate/runtime_launch_guard_unittest.py && python3 src/mutator-gate/version_coupling_unittest.py && python3 framework/tools/lease-broker/check-runtime-launches.py --root ../.. && bash framework/tools/codex/test-pr-diff-context.sh && bash framework/tools/qa/test-deps-preflight.sh && bash framework/tools/git/test-pr-review-gitea-comment.sh && bash framework/tools/git/test-pr-review-repo-host-override.sh && bash framework/tools/git/test-ci-queue-wait-branch-absent.sh && bash framework/tools/git/test-ci-queue-wait-tristate.sh && bash framework/tools/git/test-ci-queue-wait-github-checks.sh && bash framework/tools/git/test-pr-merge-queue-branch.sh && bash framework/tools/git/test-pr-merge-head-pin.sh && bash framework/tools/git/test-pr-merge-message-field.sh && bash framework/tools/git/test-git-credential-mosaic.sh && bash framework/tools/git/test-gitea-token-identity.sh && bash framework/tools/git/test-gitea-principal-resolution.sh && bash framework/tools/git/test-pr-create-identity-first.sh && bash framework/tools/git/test-pr-merge-principal-resolution.sh && bash framework/tools/woodpecker/test-terminal-green-contract.sh && bash framework/tools/_scripts/test-install-ordering-guard.sh && bash framework/tools/_scripts/test-mosaic-init-rce.sh && bash framework/tools/tmux/agent-send.test.sh && bash framework/tools/wake/test-wake-store-ack.sh && bash framework/tools/wake/test-wake-store-enqueue-race.sh && bash framework/tools/wake/test-wake-digest-hmac.sh && bash framework/tools/wake/test-wake-digest-quarantine.sh && bash framework/tools/wake/test-wake-detector.sh && bash framework/tools/wake/test-wake-fn-oracle.sh && bash framework/tools/wake/test-wake-reconcile.sh && bash framework/tools/wake/test-wake-beacon.sh && bash framework/tools/wake/test-wake-preimage.sh && bash framework/tools/wake/test-wake-install.sh && bash framework/tools/fleet/test-start-agent-session.sh && bash framework/tools/glpi/test-list-http-status.sh && bash framework/tools/orchestrator/test-board-roll.sh && bash framework/tools/woodpecker/test-ci-wait-exit-matrix.sh && bash framework/tools/_scripts/test-fleet-transport-check.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mosaicstack/brain": "workspace:*",
|
||||
|
||||
Reference in New Issue
Block a user