Skip to main content

merge-pull-request

Safely construct, inspect, validate, sign, and optionally push a maintainer GitHub pull-request merge using the repository-local vendored tool. Use when asked to merge a pull request or perform a maintainer merge workflow.

소스 정보

저장소
torrust/torrust-tracker
최근 소스 활동
2026년 10월 14일 11:44
감지된 SKILL.md 언어
영어
스타
533
포크
56

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
merge-pull-request
description
Safely construct, inspect, validate, sign, and optionally push a maintainer GitHub pull-request merge using the repository-local vendored tool. Use when asked to merge a pull request or perform a maintainer merge workflow.
metadata
{"author":"torrust","version":"1.1"}
# Merging a Pull Request Use this workflow only when a maintainer has selected an already reviewed pull request for merging. It constructs a local merge commit for inspection. It does not replace maintainer judgment, review, branch protection, or explicit authorization. The repository-local entry point is: ```sh ./contrib/dev-tools/git/merge-pull-request.sh <pull-request-number> ``` It wraps the vendored `github-merge.py` tool, fixes the target to `torrust/torrust-tracker:develop`, and creates temporary branches named: - `pull/<number>/base` - `pull/<number>/head` - `pull/<number>/merge` - `pull/<number>/local-merge` The full provenance, license, deterministic test boundary, and EPIC #2003 relationship are in [`contrib/dev-tools/git/README-github-merge.md`](../../../../../contrib/dev-tools/git/README-github-merge.md). ## Mandatory Guardrails - Verify the target is `develop` and the Git working tree is clean before starting. Preserve unrelated work with a commit or a named stash; never use `git reset --hard` to discard it. - Run the repository-local wrapper, not a personal path outside this repository. - Never start the merge tool without an interactive terminal: its prompts are read from standard input, which must be a terminal. The vendored tool never ends at end of input, so a run with redirected, closed, or piped input loops forever reprinting its prompt; the wrapper refuses to start in that case. Use `--dry-run` for non-interactive validation. - Inspect the temporary merge and run the required validation before considering a signature. - Never disable a repository ruleset to get a pull request merged, even temporarily. A forgotten disabled ruleset silently removes protections; see [`docs/github-rulesets.md`](../../../../../docs/github-rulesets.md) for the active rulesets and the override policy. - Never type `s` to sign or `push` to push unless an authorized maintainer has explicitly confirmed that action in the current request. - If GPG reports a timeout while signing, stop the failed attempt. Do not bypass signing or use `--no-gpg-sign`; ask the maintainer whether they prefer to retry the signed commit manually or have the agent rerun the same command while they enter the passphrase directly in the terminal prompt. Do not retry until the maintainer chooses, and never request or handle the passphrase in chat. ## Prerequisites 1. Confirm the upstream remote and target branch: ```sh git remote -v git switch develop git fetch <upstream-remote> git pull --ff-only <upstream-remote> develop git status --short --branch ``` Replace `<upstream-remote>` with the contributor-local remote name that points to `torrust/torrust-tracker`; do not assume it is named `torrust`. 1. Configure the required local Git values. Use a fine-grained GitHub token with access to the upstream repository only when unauthenticated API access is insufficient; do not expose it in chat, commits, or command output. ```sh git config githubmerge.repository torrust/torrust-tracker git config user.signingkey <gpg-key-id> git config user.ghtoken <github-token> ``` Signing configuration is repository-local: set `user.signingkey` in this repository and leave the global scope unset, which is also what the wrapper's preflight message tells you to do. The vendored tool's own missing-key message suggests the global scope; disregard it. `user.ghtoken` is optional. `githubmerge.host` defaults to `git@github.com`; SSH credentials must permit fetching the upstream repository and pushing only after authorization. The wrapper passes `develop` directly, so `githubmerge.branch` is not required. Optional settings supported by the vendor tool are `githubmerge.testcmd`, `githubmerge.merge-author-email`, and `githubmerge.pushmirrors` (the latter applies only to its historical `master` behavior and is not used by this `develop` wrapper). 1. Authenticate the GitHub CLI (`gh auth status`) with an account that can edit pull requests in `torrust/torrust-tracker`. The tool uses it for one write: refreshing a pull request's recorded base (see [Preflight and Merge Inspection](#preflight-and-merge-inspection)). Without it, the tool refuses a pull request whose recorded base is stale and prints the command to run instead. 1. Confirm the installed hooks and signing setup. Hooks are installed with `./contrib/dev-tools/git/install-git-hooks.sh`. A real signing attempt requires an available GPG agent and pinentry session. ## Preflight and Merge Inspection First perform the deterministic, non-destructive preflight: ```sh ./contrib/dev-tools/git/merge-pull-request.sh --dry-run <pull-request-number> ``` It validates the argument, clean tree, `githubmerge.repository`, current `develop` branch, and `user.signingkey` without contacting GitHub, creating branches, merging, signing, or pushing. If it passes and an authorized maintainer wants an inspection attempt, run: ```sh ./contrib/dev-tools/git/merge-pull-request.sh <pull-request-number> ``` Do not rebase the pull-request branch merely because `develop` advanced (see the [ADR to stop requiring pull-request rebases](../../../../../docs/adrs/20261013161419_stop_requiring_pull_request_rebases.md)). GitHub accepts the pushed merge only while the pull request's recorded base (`base.sha`) is the current `develop` tip, and it does not reliably update that record when `develop` advances. Its own test merge (`refs/pull/<n>/merge`) is not the condition. Before building anything, the tool refuses a pull request whose recorded target is not `develop`, exiting with code `4` and "ERROR: … targets '…', not 'develop'. Refusing to merge it into 'develop' or to edit its base.", because a base edit would change that pull request's target. It then compares the recorded base with the fetched `develop` tip. When it is stale, the tool refreshes it itself with a no-op base edit and prints `Refreshed the recorded base of ...`: ```sh gh api -X PATCH repos/torrust/torrust-tracker/pulls/<pull-request-number> -f base=develop ``` The edit adds no commit, timeline event, or CI run, and keeps the reviewed commit ids. If the edit fails or the base is still stale, the tool exits with code `4` and prints that command; run it, or use GitHub's "Update branch" or a rebase, then retry. When GitHub's test merge is based on an older tip, the tool skips its "merge differs from github" comparison and says so. When a direct push is still rejected with `GH013` and "Changes must be made through a pull request", `develop` most likely advanced between the check and the push: return to `develop`, pull, and run the tool again, which refreshes the recorded base. If the recorded base is current and the same rejection persists, the repository rule rejects this tool's direct push to `develop`; it is not a stale-branch failure. Do not force-push `develop` or retry the signed merge. Stop and obtain maintainer approval for a branch-protection-compatible merge workflow before proceeding. The vendor tool fetches the pull request and upstream base, checks out its temporary branches, and creates an unsigned local merge with `git merge --commit --no-edit --no-ff --no-gpg-sign`. Inspect the displayed commit graph, merge title, PR description, and `git diff HEAD~`. If no `githubmerge.testcmd` is configured, it starts an interactive shell for testing; exit that shell only after inspection is complete. Before starting the real tool, run the repository quality gate on clean `develop`. This detects a mutating hook action before it can run inside the temporary merge. A hook must leave the merge tree unchanged; a hook that rewrites files is a failed precondition, not a change to include in the merge. ```sh TORRUST_GIT_HOOKS_LOG_DIR=.tmp ./contrib/dev-tools/git/hooks/pre-commit.sh --format=json ``` If it formats `project-words.txt`, review and commit that canonical change separately, then repeat the gate from clean `develop`. After the temporary merge is constructed, run the gate again and confirm `git diff --exit-code` succeeds before signing. Review any warning that the local merge differs from GitHub's merge; continue only with explicit maintainer judgment. The vendor tool then adds review ACKs and the `Tree-SHA512` value to the merge message. ## Symbolic Links in the Merge The tool refuses a merge that carries a symbolic link, unless the link is declared. The wrapper passes `--symlinks .symlinks.json`, and that argument is what enables the mechanism at all: run the tool without it and every symbolic link refuses, whatever the tree contains. The path is this repository's convention, stated in that one wrapper line; the tool holds no default and attaches no meaning to the name. Where the declaration comes from decides what you will see. It is read from the merged result the tool has just built, not from your working copy, so a `.symlinks.json` you hold locally but have not merged grants nothing, and two maintainers merging the same pull request reach the same verdict. Every commit the merge introduces is checked against that one declaration rather than only the merged tip, because a link that appears in one commit and disappears in a later one still resolves on every checkout of the commit that carries it. Each admitted link is printed before you are asked to sign, so read them as part of the inspection: ```text Accepted symlink: '.dockerignore' -> '.containerignore': Docker reads only .dockerignore, while Podman and Buildah prefer .containerignore. ``` An undeclared link is a hard stop rather than a warning. The tool prints `ERROR: File '<path>' was a symlink in commit <hash>` and exits with code `4` before the tree hash is computed, so there is nothing left to inspect or sign. The named commit is the one that carries the link, which may be an intermediate commit of the pull request rather than its tip. The same stop applies to a link declared with a different target, and to a declared target that is absolute or contains a `..` segment, which no declaration can admit. Removing a declared link takes two changes. The pull request that deletes the link keeps its entry in `.symlinks.json`, because its own pre-deletion commits are judged against the final declaration; the tool reports the retained entry as stale rather than refusing it. A later change drops the entry once no commit a merge introduces still carries the link. A stale-entry report on a removal pull request is the expected outcome, not a defect. This repository declares no symbolic link today, so the check currently admits nothing and refuses anything that appears. The declaration format and the full rule set are in [`contrib/dev-tools/git/README-github-merge.md`](../../../../../contrib/dev-tools/git/README-github-merge.md). ## Hook Side Effects and Recovery The temporary `git merge --commit` runs installed `pre-commit` hooks. The current hook invokes `format-project-words.sh`, which may rewrite `project-words.txt` and intentionally abort with a non-zero exit. A mutating hook action therefore blocks the temporary merge: the merge tree no longer matches the expected canonical tree and must not be signed as-is. When a merge attempt fails or is rejected, first inspect `git status --short`. The wrapper's clean-tree check means pre-existing unrelated work was rejected before the attempt. The vendored tool calls `git merge --abort` after a hook failure and restores its temporary checkout; do not use a hard reset. Then return safely to the target and remove only the named temporary state: ```sh git merge --abort 2>/dev/null || true git switch develop git branch -D pull/<number>/head pull/<number>/base pull/<number>/merge pull/<number>/local-merge 2>/dev/null || true git status --short --branch ``` If a pull request causes the dictionary formatter to abort the temporary merge, ask the PR author to commit the canonical dictionary formatting, or prepare an approved follow-up commit; do not retry a non-canonical merge. The vendor tool also performs this branch cleanup in its `finally` block, but verify it after every failure. If a failure happens after local `develop` was reset to the signed merge, use `git reflog` to identify the pre-merge tip and ask an authorized maintainer before changing it. ## Push Failure Diagnosis and Recovery A failed final push is not evidence that the pull request needs a rebase. Distinguish a stale recorded base (which the tool checks and refreshes before constructing the merge) from SSH authentication and server-side failures before changing the source branch or rebuilding the signed merge. When a push fails after the tool has signed and reset local `develop` to the inspected merge: **Step 1: Stop.** Do not start a second merge attempt, force-push `develop`, or discard the signed local merge. **Step 2: Inspect remote state.** Fetch the upstream remote and inspect the remote branch and PR state: ```sh git fetch <upstream-remote> git log -1 --oneline <upstream-remote>/develop gh pr view <pull-request-number> --repo torrust/torrust-tracker --json state,mergedAt,mergeCommit ``` If the remote tip is the signed merge commit or the pull request is merged, the push completed despite its client-side error. Do not retry anything; fast-forward local `develop` only after reviewing the remote result. **Step 3: Classify an unchanged remote tip.** Use the failed output: | Failure | Meaning | Recovery | | ------------------------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Host key verification failed` | The SSH host identity is not trusted locally. It is neither a rebase nor a repository-permission failure. | Stop and verify the host key against GitHub's published fingerprints before updating the maintainer machine's SSH configuration. | | Permission denied for an SSH public key | The key or SSH-agent identity is not authorized for the upstream push. It is not a stale branch. | Verify the maintainer's SSH identity and upstream access; do not substitute a token or expose a credential in chat. | | `GH013` / "Changes must be made through a pull request" | Repository policy rejected a direct merge push. | Follow the recorded-base guidance above, then obtain approval for a branch-protection-compatible merge workflow if the recorded base is current. | | Remote `Internal Server Error` / HTTP `5xx` | GitHub or its Git service rejected the request transiently; the remote branch may still be unchanged. | Re-fetch and recheck the PR. With explicit maintainer approval, retry the same final push once; if it fails again, stop and report GitHub's request ID and timestamp. | **Step 4: Retry only with approval.** Only after the remote remains unchanged and an authorized maintainer explicitly approves, retry the exact final push the tool attempted: ```sh git push <upstream-remote> develop ``` This publishes the already inspected and signed merge. It is a recovery continuation of the maintainer-approved tool invocation, not a new merge or permission to push unrelated commits. The repository-local wrapper and `contrib/dev-tools/git/github-merge.py` are authoritative. Do not invoke a personal copy or alias merely because it was once identical: the tracked tool can contain repository-specific fixes and the current workflow documentation applies only to it. ## Signing and Push Confirmation After successful inspection and validation, the tool prompts for `s` or `x`. Enter `x` unless the maintainer has explicitly approved signing this exact inspected merge. After a successful signature, it resets local `develop` to the signed temporary merge and deletes the temporary branches. It then prompts for `push` or `x`. Enter `push` only after separate, explicit maintainer confirmation to publish the signed merge to the displayed remote and branch. Entering `x` leaves the signed local commit unpushed; report its commit ID and wait for maintainer direction. Never push directly as an autonomous agent. ## Verification Boundaries Run the deterministic wrapper coverage before changing repository-specific behavior: ```sh bash contrib/dev-tools/git/tests/test-merge-pull-request.sh python3 contrib/dev-tools/git/tests/test-github-merge-symlinks.py ``` Manual verification remains required for an authorized disposable pull request: prerequisite discovery, non-destructive inspection and rejection, hook-side-effect recovery in an isolated checkout, and signed completion with an explicit push confirmation. The tests intentionally do not exercise GitHub networking, credentials, interactive shells, GPG pinentry, real merges, or pushes because they cannot be safely deterministic. ## Relationship to EPIC #2003 Issue #2022 makes the current workflow reproducible now. It does not choose the automation architecture proposed for evaluation in EPIC #2003. A future approved decision may migrate this workflow to Rust or replace it with another approved architecture; keep repository-specific integration narrow and preserve vendor provenance until that decision is implemented.
GitHub에서 보기