Add docs/architecture/jenkins-job-mapping-design.md: declarative versioned
mapping config (exact-match repo/branch entries, no globs, fail-closed load
on malformed/duplicate entries), resolution semantics for multibranch/
single/parameterized-view job types with URL-encoded branch and PR-<n>
addressing, branch-pinned-over-repo-wide precedence, fork PRs resolving via
base repo only, explicit machine-checkable no-match payload (never guess or
probe job names), config location in the jenkins-mcp package (no secrets,
env-overridable path), a read-only jenkins_resolve_job tool surface, and a
mocked-config/mocked-Jenkins testing strategy.
Design only; no implementation, no code behavior changed, no Jenkins write
actions introduced.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Harden gitea_auth.api_request: add a per-request timeout (env
GITEA_HTTP_TIMEOUT), convert timeouts and DNS/network failures
(URLError/TimeoutError) into clear RuntimeErrors, give 502/503/504 an
explicit 'upstream unavailable' message, convert malformed success JSON
into a clean error, and redact credential-like substrings from all error
text. Preserves the success path and existing 429 retry/backoff.
Add shared gitea_auth.api_get_all: page-based pagination that tolerates
missing/malformed metadata (relies on page length, not Link/X-Total-Count
headers), honors an optional overall limit, and caps pages. Wire it into
the read-only list tools gitea_list_issues, gitea_list_prs, and
gitea_list_labels (return shape unchanged).
Add tests/test_api_reliability.py (18 cases) and update the three list-tool
tests to the new call path. No auth/profile/merge/review/tracker behavior
changed. No modular #65 refactor.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add docs/developer-testing-guidelines.md covering test commands, targeted
runs, syntax/diff checks, adding MCP tool tests, safe API/auth mocking,
profile/allowed-operation gate tests, self-review/self-merge gate tests,
no-secret regression expectations, unit vs future Docker integration tests,
and read-only vs mutating tool expectations. Link it from the README Tests
section and note the suite table is non-exhaustive.
Documentation only; no code behavior changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Split the one-shot label backfill into reusable, mode-selected operations while
preserving the original default behavior:
- --create-labels : idempotent label creation only (create_labels()).
- --apply-mapping : one-off MAPPING labeling only (apply_mapping(); PUT replaces
each issue's set).
- --add-label <issue> <label> : ad-hoc single-issue labeling (add_label(); POST
appends the label, does not replace; refuses an undefined label).
- default (no mode) : create labels then apply MAPPING — identical to the prior
behavior. --dry (and --dry-run) still print without writing.
Extracted create_labels / apply_mapping / add_label / _labels_by_name helpers;
LABELS, MAPPING, and the api() wrapper are unchanged. No auth/network behavior
change; MAPPING remains the same one-off backfill data.
Tests: extend tests/test_manage_labels.py with a TestModes suite — create-only
(no PUT), apply-only (no label creation), add-label appends (POST, not PUT),
unknown-label no-op, dry no-op, non-numeric issue exits. Existing default/dry/
mapping/constant tests unchanged and still pass.
py_compile clean; full suite 319 passed / 0 failures; git diff --check clean;
no secrets.
Closes#6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Issue #9 requested getAuthenticatedUser and getCurrentUser in addition to whoami.
This adds the two aliased MCP tools and their corresponding unit tests.
Root cause: macOS Sequoia+ blocks Python.app from executing files carrying the
com.apple.provenance extended attribute. Files written by an agent/IDE terminal
get it (shell scripts and pre-session files do not). This is a macOS security
feature, not a bug in our code — so the fix is an operator workaround, not a
code change to the tools.
- scripts/clear-provenance: recursively removes ONLY com.apple.provenance under
a path (default: repo root); tolerates files without it; leaves other xattrs
intact; supports --dry-run. Advises running from a Full-Disk-Access terminal.
- README Troubleshooting section documenting the symptom, the helper, manual
xattr equivalents, and the Full Disk Access alternative.
Narrow + macOS-specific; no auth/release/worktree/tracker/MCP behavior changed.
Tests: tests/test_clear_provenance.py (6 cases) — dry-run default/explicit path,
missing-path error, bad-flag/too-many-args exit 2, and that only
com.apple.provenance is targeted (not a blanket xattr clear). Dry-run only; no
real xattr mutation.
bash -n clean; py_compile mcp_server.py clean; full suite 319 passed / 0
failures; git diff --check clean; no secrets.
Closes#3.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Automate the documented release-tag checklist (#48) without bypassing safety
gates.
scripts/release-tag:
- Requires a SemVer tag (vMAJOR.MINOR.PATCH); validates before any git/network.
- Fetch/prune first, then refuses: dirty worktree, non-master branch, local
master != remote master, HEAD not on remote master, and an existing local or
remote tag of the same name.
- Runs the full suite by default; --skip-tests is an explicit opt-out that warns.
- Creates an ANNOTATED tag (git tag -a), never lightweight.
- Safe by default: no push unless --push; --dry-run prints planned actions and
changes nothing. Supports --notes-file <path> for the annotation message.
- Prints: commit, tag, tests_run, tag_created, tag_pushed.
- Env injection points for testing/CI: RELEASE_TAG_REMOTE, RELEASE_TAG_TEST_CMD.
tests/test_release_tag.py (14 cases): valid SemVer dry-run; invalid version;
dirty worktree; non-master; master/remote mismatch; existing tag; missing
notes-file; annotated-not-lightweight; no-push-without-flag; push-only-with-flag;
notes-file message; --skip-tests warns; default runs tests (fail blocks tag,
pass tags). Each test builds a throwaway repo with a LOCAL bare remote (cloned,
not pushed) and stubs the test command — no network, no real tags, no pushing
from the project repo.
Docs: reference scripts/release-tag from the runbook, SKILL, and the release-tag
template (script preferred; manual steps are the fallback).
Full suite 305 passed / 0 failures; bash -n clean; git diff --check clean; no
secrets.
Closes#50. Refs #48.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Documents and enforces rules for closed-not-merged PR reconciliation, direct-master-push prevention, and issue label cleanup.
Rules added:
- Explicit definitions for Merged, Landed, Closed-not-merged, and Reconciled.
- A PR is done only when Gitea reports it merged or reconciliation proves content is present on master.
- Direct push to master is forbidden except as a documented recovery exception.
- PRs closed but not merged trigger the reconciliation process.
- Branch and worktree cleanup is forbidden until merge or reconciliation is confirmed.
- Final reports require PR metadata and Git content verification.
Closes#51.
Formalize the branch↔issue relationship and add a release/version-tagging policy.
Branch/issue linkage:
- scripts/worktree-start now validates branch names: implementation branches
must match (fix|feat|docs|chore)/issue-<number>-<slug>; review branches
review/pr-<number>-<slug>. Untraceable names are rejected with a clear error
(exit 2). New --allow-unlinked override for genuine exceptions. --dry-run
preserved.
- Documented issue → branch → worktree → PR → cleanup traceability in the
runbook and the portable SKILL, including the claim-comment convention and
Closes #n / Refs #n PR-body usage.
- Noted that Gitea exposes no native issue→branch API field (only a PR head
branch), so linkage is enforced via branch name + claim comment + PR body +
cleanup.
Versioning / tagging policy (docs only; no release automation yet):
- SemVer vMAJOR.MINOR.PATCH (v0.x.y while unstable) with PATCH/MINOR/MAJOR bump
rules.
- Annotated tags only, from the exact commit on remote master, only after the
full suite passes, with release notes referencing merged PRs/issues. Never tag
feature branches, dirty worktrees, unreviewed/self-authored work, or commits
not on remote master.
- Release runbook in the runbook + SKILL, plus a new
skills/llm-project-workflow/templates/release-tag.md prompt template.
Tests: worktree-start branch validation — accepts fix/feat/docs/chore/issue-*
and review/pr-*, rejects fix/random-name / my-branch / non-numeric issue,
honors --allow-unlinked, preserves --dry-run. Full suite 291 passed / 0 failures;
bash -n clean; git diff --check clean; no secrets.
Release-tag automation (a scripts/release-tag helper) intentionally deferred to a
later issue to keep this diff narrow and testable.
Closes#48. Refs #38, #39, #46.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Extract the project's operating rules into a reusable, project-agnostic skill
so any repo can adopt the same safe LLM workflow.
- skills/llm-project-workflow/SKILL.md: issue-first; isolated branch worktrees
(main checkout = orchestration only); distinct author/reviewer identities and
profile safety (secrets by reference only; stop if authenticated user == PR
author); branch naming; start/review/merge/cleanup workflows; fail-closed
cases; recovery patterns; and an "Adapting to a project" table for the
forge-specific names.
- templates/: copy/paste prompts for start-issue, review-pr, merge-pr,
recover-bad-state, worktree-cleanup.
- Link the skill from README.md and docs/llm-workflow-runbooks.md (the runbook
is framed as the Gitea-specific application of the portable skill).
Docs-only; no code, no secrets, safe placeholder examples only. No change to
MCP runtime, Gitea API, credential storage, or worktree helpers.
Checks: full suite 287 passed / 0 failures; git diff --check clean; secret scan
of skills/ clean.
Closes#46. Refs #38, #39.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gitea_edit_pr called _auth() (and resolved the remote) before checking whether
any editable field was provided, so a pure validation error (no fields) surfaced
as a RuntimeError "no credentials" in environments without Gitea auth — making
test_edit_pr_no_fields_raises depend on credentials/network/env.
Move the payload build + no-fields ValueError ahead of _resolve/_auth/URL setup.
Behavior is unchanged when fields are provided (same _resolve → _auth → audited
PATCH path). No change to auth, retry/backoff, audit, config profiles, or
worktree helpers.
Tests: add test_edit_pr_no_fields_validates_before_auth asserting the no-fields
path raises ValueError and calls neither get_auth_header nor api_request (even
with auth mocked to None). Existing edit-PR tests unchanged.
Full suite passes with no Gitea credentials (287 passed, 0 failures) — the
no-fields test no longer depends on the environment.
Closes#43.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Finishes the isolated-worktree standard begun in #38 (which merged the
branches/ gitignore, runbook, and scripts/worktree-start). Adds the two
remaining helpers and their tests.
- scripts/worktree-review: isolated DETACHED review worktree under
branches/review-<branch> (fetch/prune first, refuse to overwrite, print path,
--dry-run). Detached so a reviewer cannot accidentally commit and review work
never blocks the author's implementation folder.
- scripts/worktree-clean: the only deleting helper — removes a branches/
worktree after merge/close, refuses a dirty worktree (no --force), optionally
safe-deletes a merged branch (git branch -d), fetch/prune first, --dry-run.
Deletes nothing unless explicitly invoked.
- tests/test_worktrees.py: path generation + refuse-to-overwrite for all three
helpers via --dry-run (no real worktrees/branches/network/deletions).
- runbook: reference worktree-review / worktree-clean and the --dry-run flag.
Checks: bash -n clean on all three scripts; git diff --check clean; full suite
286 passed, 0 failures.
Closes#39. Follow-up to #38.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Make the interactive profile menu feel like a real terminal menu, via a new
injectable MenuIO abstraction (no menu logic change, no auth/secret-storage
change).
- Single-key top-level actions in a TTY (termios/tty raw read); no Enter
needed. Non-TTY / test runs fall back to line input.
- Enter backs out: Enter (or 0) on the main menu quits; Enter cancels any
submenu/profile prompt and returns.
- Profile chooser: everywhere a profile is needed, show a numbered list and
pick by key (1-9), with an explicit 'm) type a name manually' path and Enter
to cancel. Empty config handled gracefully.
- Clear screen before redrawing the main menu and chooser — TTY only; never
emits clear codes in non-TTY/test runs.
- Result actions (validate/test-auth/whoami/eligibility) print a concise result
then pause for a keypress in a TTY; non-TTY never blocks.
Helpers: read_key (via default_io) / choose_menu_option / choose_profile /
clear_screen / pause_for_key, plus MenuIO(is_tty, clear_enabled). TTY detected
with sys.stdin.isatty() and sys.stdout.isatty(); stdlib only.
Safety unchanged: no tokens/passwords printed, no raw config dumps, no
.env.personal, no change to auth behavior or secret storage.
Tests: rewrote menu tests around a scripted _FakeIO (no real terminal): single-
key select + clear, main-menu Enter/0 quit, submenu Enter cancel (no change),
chooser lists/selects/no-profiles/manual/out-of-range, non-TTY line fallback,
clear-only-when-enabled, pause never hangs non-TTY, and add-flow proving the
token value never reaches disk or stdout.
Docs: runbook note on single-key nav / Enter back-out / numbered chooser.
scripts/gitea-config-menu unchanged.
Closes#36. Refs #31, #34.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add docs/llm-workflow-runbooks.md — the final roadmap #10 deliverable:
operational runbooks for LLM-operated Gitea workflows, built on the shipped
canonical profiles + interactive menu + gated review/merge + audit logging.
Covers:
- Principle: the profile is the role, not the LLM (task-scoped, not assigned).
- Canonical config: GITEA_MCP_CONFIG / GITEA_MCP_PROFILE, version, profiles,
keychain + env auth references, precedence, legacy env-only fallback.
- Interactive menu (python gitea_config.py menu): create author/reviewer
profiles, generate Claude/Gemini/Codex launcher snippets, validate auth,
check PR reviewer eligibility.
- Thin-launcher pattern: LLM configs carry only command/args + the two
GITEA_MCP_* vars — never raw tokens/passwords.
- Migration away from duplicated GITEA_USER_*/GITEA_PASS_*/GITEA_SITE_* blocks;
secrets referenced by keychain id or env var name only.
- Per-workflow runbooks (create issue/children, implement+PR, review/request-
changes/approve, merge, close-after-merge, stop-on-blocker) with safe prompts.
- Fail-closed behavior table (unknown identity/profile, self-author, moved head,
unexpected files, detected secrets, production/deploy) and no self-review/merge.
Docs-only: no implementation code. Safe placeholder examples only (no real
tokens, passwords, usernames, or private config). README links the new runbook.
Closes#17. Refs #10.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an interactive utility so users create/edit/validate canonical runtime
profiles and generate safe LLM launcher snippets without hand-editing JSON or
pasting tokens into Claude/Gemini/Codex configs.
Run: `python gitea_config.py menu` (or `python gitea_config_menu.py`).
gitea_config.py — pure, testable authoring helpers:
- is_valid_profile_name, build_profile, keychain_auth/env_auth, empty_config
- validate_config (reports missing base_url/auth, inline token/password — never
echoing the secret value)
- add_profile (preserves existing, rejects dup/invalid name/missing base_url),
upsert_profile, remove_profile
- save_config: mkdir parents + atomic temp-then-os.replace, pretty JSON
- launcher_entry: thin MCP entry (command/args + GITEA_MCP_CONFIG/PROFILE only)
- keychain_set: store a token via `security add-generic-password` (token passed
as an arg, never returned/printed/logged; injectable runner)
- `menu` __main__ dispatch
gitea_config_menu.py — interactive loop with fully injectable IO/secret/HTTP/
keychain so it is testable without a real terminal, keychain, or network:
- list / add / edit / remove / validate profiles
- test authentication + show authenticated user (calls /user only on request)
- reviewer-eligibility helper (authenticated user vs PR author, open state) —
read-only, never approves/merges
- launcher snippets for Claude / Gemini / Codex (no secrets)
Security: tokens are never written to profiles.json, launcher snippets, logs,
or errors — only keychain ids / env var names are stored. Backwards compatible:
menu is optional; env-only mode and MCP server startup are unchanged.
Tests: tests/test_config_menu.py (21 cases) — name validation, preserve-on-add,
dup/invalid/missing-field rejection, atomic write (+ replace-failure leaves the
original intact, no temp debris), keychain_set stores-without-printing, launcher
snippets secret-free, eligibility eligible/self-author/closed, and a full menu
add→list→quit flow proving the token value never reaches disk or stdout.
Stacked on #30 (canonical profiles); base branch feat/json-runtime-profiles.
Refs #10, #19. Closes#31.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Rework the JSON runtime-profile config from the earlier ad-hoc schema
(profiles + token_env) to the canonical single-file model in #19, so every LLM
launcher can reference one shared Gitea profiles file instead of duplicating
GITEA_USER_*/GITEA_PASS_* blocks or embedding tokens.
Canonical schema (gitea_config.py):
- top-level "version" (1) + "profiles" map.
- each profile: base_url, username, default_owner, execution_profile, and a
typed auth reference:
{ "type": "keychain", "id": "..." } -> macOS keychain (security(1))
{ "type": "env", "name": "..." } -> named environment variable
- inline "token"/"password" keys are rejected (never accepted or echoed).
- select via GITEA_MCP_CONFIG (path) + GITEA_MCP_PROFILE (name).
gitea_auth integration:
- get_profile() overlays env over the selected profile (env wins; JSON fills
the rest); profile_name <- execution_profile; token_source_name <- the
non-secret auth reference name (env var name or "keychain:<id>"); now also
surfaces username + default_owner.
- get_auth_header() resolves the profile's auth reference (env/keychain) as a
token fallback after explicit env tokens; a ConfigError there fails closed.
Security / safety:
- Secrets referenced only (keychain id / env name); token values never stored
in or returned as metadata. Errors never print file contents, tokens, or
passwords (JSONDecodeError context suppressed).
- Missing file / invalid JSON / unsupported version / unknown-or-unset profile
/ unresolvable secret reference all raise a clear, safe ConfigError.
- No network calls during config parsing; keychain lookup is on-demand and
injectable for tests.
- Backwards compatible: GITEA_MCP_CONFIG unset => legacy env-only mode
(existing get_profile/get_auth_header tests unchanged).
Docs: README canonical-profile + thin-launcher (Claude/Gemini/Codex) sections
and a migration note away from duplicated GITEA_PASS_* blocks; .env.example and
gitea-mcp.example.json updated to the canonical shape (safe placeholders only).
Tests: tests/test_config.py (31 cases) — legacy env-only, JSON selection,
multiple profiles, missing/unset profile, invalid JSON, unsupported version,
env-override precedence, keychain + env auth-reference parsing and resolution,
missing-secret errors, inline token/password redaction, and no-network parse.
Refs #10. Completes the closed#19 (env-based profiles) by adding the canonical
shared-file model. Supersedes this PR's earlier simpler JSON schema.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Let one MCP server select among named Gitea runtime profiles from a JSON file
instead of editing code or juggling many .env files:
GITEA_MCP_CONFIG=/path/to/gitea-mcp.json
GITEA_MCP_PROFILE=dev
- New gitea_config.py: load/validate the JSON, select the named profile, and
resolve its token by env-var reference. Profiles supply base_url,
profile_name, token_env, owner/repo, allowed/forbidden operations, and audit
label.
- gitea_auth.get_profile() now overlays env over the selected JSON profile:
explicit env vars win, the JSON profile fills only what env leaves unset.
- gitea_auth.get_auth_header() gains a JSON token_env fallback after explicit
env tokens (env still wins).
Security / safety:
- Tokens are referenced by env-var NAME (token_env); an inline "token" is
rejected and never echoed. The value is never stored in or returned as
profile metadata.
- Fail-safe errors: missing file / invalid JSON / unknown or unset selected
profile raise a clear ConfigError that never prints file contents or tokens
(JSONDecodeError context is suppressed so the raw file text can't surface).
- No network calls during config parsing.
- Real config files are gitignored (gitea-mcp*.json), example kept.
Backwards compatible: with GITEA_MCP_CONFIG unset, behaviour is exactly the
prior env-only behaviour (all existing get_profile/get_auth_header tests pass
unchanged).
Docs: README JSON-profiles section + env table rows, .env.example placeholders,
gitea-mcp.example.json.
Tests: tests/test_config.py (22 cases) — env-only, selection, multiple
profiles, env-override precedence, missing file, invalid JSON, missing/unset
profile, inline-token rejection + redaction, and no-network-during-parse.
Refs #10. Note: issue #19 (env-based profiles) was already implemented and
closed; this JSON-file capability is adjacent new scope tracked under the
roadmap rather than reopening #19.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add durable, opt-in audit logging for every mutating Gitea MCP action so an
operator can see which execution profile and authenticated Gitea user
performed (or was blocked from / failed) each mutation.
- New gitea_audit.py: pure, no-network module — recursive secret redaction
(token/password/authorization keys; token/Basic/Bearer value runs),
build_event (timestamp, action, result, profile, audit label, authenticated
username, repo, issue/PR, target branch, head SHA, redacted request
metadata), and an append-only JSON Lines sink.
- mcp_server.py: _audit helper + _audited context manager (simple mutations)
and an _audit_pr_result decorator (gated review/merge tools, reading their
own result dict) wired into create_issue, create_pr, edit_pr, close_issue,
commit_files, delete_branch, create_label, set_issue_labels, mark_issue
(label/unlabel), gitea_submit_pr_review, and gitea_merge_pr.
- Outcomes recorded as allowed/blocked/failed/succeeded; blocked and failed
eligibility checks are logged, not just successes.
Off by default: records are written only when GITEA_AUDIT_LOG is set. When it
is unset every audit path short-circuits — no records, no extra API calls — so
existing tool behaviour and API call sequences are unchanged. Auditing never
raises; sink writes are best-effort. Tokens are never written.
Docs: README env table + audit note, .env.example placeholder.
Tests: tests/test_audit.py (19 cases) — redaction, event build, sink writes,
per-tool success/failure/blocked records, secret-free output, off-by-default
no-op, and audit-failure-never-breaks-action.
Closes#18
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
api_request now retries HTTP 429 responses instead of failing immediately:
- Parse and honor a valid Retry-After header (seconds or HTTP-date).
- Fall back to full-jitter capped exponential backoff when the header is
missing or invalid.
- Bound retries by max_retries and delay by max_delay (env-overridable via
GITEA_MAX_RETRIES / GITEA_RETRY_BASE_DELAY / GITEA_RETRY_MAX_DELAY) — no
infinite loops.
- Non-429 errors and successful responses are unchanged.
Sleep, randomness, and clock are injectable so retry timing is tested
deterministically. Adds tests/test_retry_backoff.py (23 cases).
Closes#27
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Stale documentation only — no behavior change. The module docstring and the
--merge / --merge-method argparse help text no longer advertise a working CLI
merge. They now state CLI merge is disabled/fail-closed and that merge is
handled solely by the gated gitea_merge_pr MCP workflow (#16).
CLI merge remains disabled: --merge still fails closed with no API call, and
review_pr.py contains no /merge call. No MCP merge gate was changed.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Reviewer found the MCP merge surface is gated but two local CLI scripts remain
ungated merge paths that LLM automations in this project have been using.
Close them (Option B — minimal safe fix; full gated CLI merge left to a
follow-up):
- review_pr.py: `--merge` now fails closed BEFORE any API call with a clear
message directing callers to the gated `gitea_merge_pr` MCP workflow. The
review-only path is unchanged. The merge execution block was removed.
- merge_pr.py: main() is now a fail-closed no-op — reads no credentials and
makes no merge API call; prints that merge is only available via the gated
workflow.
- README: the `review_pr.py` row and Quick Examples no longer advertise a CLI
`--merge` path; added an audit-logging clarification that #16 returns
structured gate/merge results but does not add durable audit logging, which
is tracked by #18.
Tests updated/added:
- test_review_pr.py: `--merge` fails closed with no API call; message points to
the gated workflow.
- test_merge_pr.py: merge fails closed with no API call, even with
--force/--do/--title/--message; message points to the gated workflow.
- test_mcp_server.py: README no longer advertises the ungated CLI merge example.
The gated MCP `gitea_merge_pr` is unchanged and still gated; `gitea_review_pr`
still fails closed on merge=True; `gitea_submit_pr_review` still cannot merge.
No secrets, auth headers, raw env, or credential paths are exposed. No
Jenkins/Ops/GlitchTip/Release/deploy/CI behavior added. #17/#18 not started.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the ungated gitea_merge_pr with a gated merge workflow. This is now
the only merge path the MCP server exposes; the merge API is called only
after every safety gate passes.
Gates (fail-closed at each step):
1. Merge method is merge | squash | rebase.
2. Explicit confirmation: confirmation must equal "MERGE PR <n>" (without it,
zero API calls are made).
3. Reuse gitea_check_pr_eligibility (#14) with action 'merge': proves the
authenticated identity, the active profile (and that it allows merge), the
PR author, blocks self-merge, requires the PR open, and fails closed when
the PR is not mergeable or mergeability is unknown.
4. Optional expected_head_sha: refuse if the PR head moved.
5. Optional expected_changed_files: refuse if the PR's changed file set differs.
6. Redundant self-merge block (auth user == PR author).
The force/ignore-checks option was removed — Gitea's own mergeable signal
(which reflects branch-protection required reviews/checks) must be positive,
so required approval/check state is honoured, never bypassed.
Output reports performed, authenticated user, profile name, PR author, PR
number, head SHA checked, merge method, gates passed/blocked, and merge
result / merge commit — never a token, auth header, or credential. Error text
is scrubbed via _redact.
Surface audit: no ungated merge path remains. The /merge endpoint appears only
inside gitea_merge_pr; gitea_review_pr fails closed on merge=True before any
API call; gitea_submit_pr_review has no merge parameter and 'merge' is not a
reviewable action. Tests assert all three.
Tests cover: merge succeeds only when all gates pass; self-author blocked;
unknown identity/profile blocked; profile without merge permission blocked;
missing/wrong confirmation blocked (no API call); head-SHA mismatch blocked;
changed-files mismatch blocked; closed PR blocked; non-mergeable blocked;
unknown mergeability fail-closed; no merge call when gates fail; invalid merge
method rejected; output and error redaction; and the no-ungated-merge-path audit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add gitea_submit_pr_review, the only tool that submits a Gitea PR review.
It performs a review mutation (comment / approve / request_changes) only
after every safety gate passes, and never merges.
Gates (fail-closed at each step):
1. Validate action is comment | approve | request_changes.
2. Reuse gitea_check_pr_eligibility (#14) for authenticated-user lookup,
active-profile lookup, PR-author lookup, self-approval block, and the
profile-allowed-operation check. approve requires 'approve' eligibility,
request_changes requires 'request_changes', comment requires 'review'.
3. Redundant self-approval block (auth user == PR author).
4. Optional expected_head_sha: refuse if the PR head has moved.
5. Only then POST /repos/{owner}/{repo}/pulls/{n}/reviews (formal review
endpoint, so approvals/change-requests carry real review state).
Output reports action, whether performed, authenticated user, profile name,
PR author, PR number, head SHA checked, and reasons — never a token, auth
header, or credential. Error text is scrubbed via _redact as defence in depth.
Merge is intentionally not implemented (belongs to #16).
Tests cover: self-author approve blocked, approve/request_changes/comment
succeed only when eligible, unknown identity fail-closed, disallowed profile
op blocked, head-SHA mismatch blocked, no mutation when gates fail, invalid
action rejected, and secret redaction in output and error paths.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>