Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/upstream-sync.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Upstream Sync

# Creates upstream-sync/YYYYMMDD from origin/main, merges (or rebases) upstream/main
# into it, pushes the branch and opens a PR against main. This workflow never
# pushes to main. See FORK_GOVERNANCE.md and script/upstream-sync.

on:
workflow_dispatch:
inputs:
mode:
description: "Integrate upstream/main with merge or rebase"
type: choice
options:
- merge
- rebase
default: merge
schedule:
# Monthly, first day of the month at 06:00 UTC. Remove if maintainers
# prefer manual dispatch only.
- cron: "0 6 1 * *"

permissions:
contents: write
pull-requests: write

concurrency:
group: upstream-sync
cancel-in-progress: false

jobs:
sync:
runs-on: ubuntu-24.04
steps:
- name: Check out main
# actions/checkout v7.0.1, SHA-pinned per audit requirement PA-62
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1
with:
ref: main
fetch-depth: 0
persist-credentials: false

- name: Configure git identity
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

- name: Sync upstream into upstream-sync branch
env:
GH_TOKEN: ${{ github.token }}
UPSTREAM_URL: https://github.com/github/github-mcp-server.git
MODE: ${{ github.event.inputs.mode || 'merge' }}
run: |
set -euo pipefail
script/upstream-sync --mode "${MODE:-merge}" --push --pr --report-conflicts
152 changes: 152 additions & 0 deletions FORK_GOVERNANCE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
# Fork governance — `lutzkind/github-mcp-server`

Applies to the maintained fork `lutzkind/github-mcp-server` of
`github/github-mcp-server` (upstream `main`:
`85598ba6e1256f7ebf4867b95d63b833c4549264`, tag `v1.12.2`).
Verified against the live GitHub API and remotes on **2026-09-19**; fork `main`
was `655a7f1310d16c615f3e808a3a89fa9fcda09f51`. This file closes audit finding
PA-62 ("Guarded work merged to main; ~1153-commit upstream drift").

## 1. Canonical branch model

| Ref | Role | Rules |
|---|---|---|
| `main` | **Canonical fork branch and default branch** | Guarded work is merged here (`655a7f13`). Integration via PR only; direct pushes discouraged. |
| `upstream-sync/YYYYMMDD` | Upstream integration branch | Created by `script/upstream-sync` / `Upstream Sync` workflow; PRs target `main`; automation never pushes `main`. |

`origin/HEAD -> origin/main`; the fork default branch is `main` (verified via
`gh api repos/lutzkind/github-mcp-server`).

> Note: `docs/repository-hygiene.md` documents the *branch/file hygiene MCP tools
> exposed by the server*; it is not fork governance. This file is.

## 2. Ownership map (what is local vs upstream)

| Divergent area | Owner | Authoritative source | Primary paths |
|---|---|---|---|
| Guarded mutation surface: preview/target binding, `expected_*` guards, target attestations, post-write branch verification retries | **fork maintainers** | fork `main` (guarded patch set, §3) | `pkg/github/repositories.go`, `pkg/github/retry.go`, `pkg/github/dependencies.go`, `pkg/github/mutation_guard_test.go`, `pkg/github/__toolsnaps__/apply_repository_changes.snap`, `pkg/github/__toolsnaps__/create_repository.snap`, `pkg/github/__toolsnaps__/create_or_update_file*.snap`, `pkg/github/__toolsnaps__/push_files*.snap`, `pkg/github/__toolsnaps__/manage_branch.snap` |
| Atomic multi-file patch commits (`file_patches.go`) and pull-request comment hardening | **fork maintainers** | fork `main` | `pkg/github/file_patches.go`, `pkg/github/file_patches_test.go`, `pkg/github/file_patches_handler_test.go`, `pkg/github/pullrequests.go`, `pkg/github/pullrequests_test.go`, `pkg/github/issues_test.go` |
| Base tool surface, toolsets, OAuth, remote server, docs, CI | upstream `github` | upstream `main` | everything not listed above |
| Shared generated artifacts (`__toolsnaps__` not listed above, `README.md`, `docs/`) | upstream `github` | upstream `main` | regenerate instead of hand-merging |

## 3. Guarded patch set (preserved on `main`)

Fork-only commits on `main` (verified 2026-09-19, all present):

| Commit | Date | Subject |
|---|---|---|
| `aff33990` | 2026-07-23 | feat: add guarded create_repository capability |
| `7e7a5977` | 2026-07-24 | test: cover issue comment API behavior |
| `cb638104` | 2026-07-26 | bind repository mutations to verified previews |
| `10abed17` | 2026-07-27 | fix: retry GitHub branch verification after writes |
| `a348d71e` | 2026-07-30 | attach target attestations to GitHub mutations |
| `1fea55f0` | 2026-07-21 | test: make post-write verification fixtures target-aware (#2) |
| `e677885c` | 2026-07-21 | test: separate GitHub fields variant snapshots (#3) |
| `655a7f13` | 2026-07-30 | Merge GitHub MCP response binding hardening |

Combined fork delta vs the merge base: 27 files, ~+5.4k/−0.8k lines, dominated by
`pkg/github/repositories.go` (+~3.2k) and `pkg/github/file_patches.go` (new).
Invariant tests: `pkg/github/mutation_guard_test.go`,
`pkg/github/file_patches_test.go`, `pkg/github/file_patches_handler_test.go`.

## 4. Drift status and maintenance item (revalidated 2026-09-19)

- Merge base: `c36e4e44` ("MCP: name-based resolution for Projects fields (#2760)",
2026-07-10).
- Upstream-only commits since merge base: **172**; upstream `main` totals **1153**
commits (this is the "~1153-commit upstream drift" figure), latest tag `v1.12.2`.
- Fork-only commits: **8**, all merged on `main` (guarded patch set above).
- Operational maintenance item: run the Upstream Sync workflow (or the script)
and resolve conflicts per §5. The large upstream merge is intentionally **not**
performed by the PA-62 governance change; the gap closed here is the missing
governance/sync mechanism itself.

## 5. Conflict policy (upstream sync)

1. Integration only on `upstream-sync/YYYYMMDD`; automation never pushes `main`;
merging remains a human PR decision.
2. Upstream wins for everything except the guarded areas in §2. Expect heavy
conflicts in `pkg/github/repositories.go` (upstream churn to the same file).
3. When resolving `repositories.go`: keep upstream handler/tool wiring, then
re-apply the fork's preview binding, `expected_*` guards, attestations and
verification retries on top. Never drop a guard silently.
4. `__toolsnaps__` snapshots: prefer regenerating the affected snapshots with the
repo's snapshot tooling/tests over hand-merging, then diff for unintended tool
schema changes.
5. `pkg/github/dependencies.go`, `retry.go`, `file_patches.go` are fork-owned;
keep them unless upstream introduces an equivalent feature, in which case
upstream's implementation wins and fork tests are re-pointed at it.
6. Acceptance gate before merging the sync PR:
`./script/test` (Go race tests) and `./script/lint`, plus a
`go build ./cmd/github-mcp-server` with UI assets built.

## 6. Branch inventory and cleanup (2026-09-19)

Start: 272 remote branches. Deletion criteria (zero-loss only): tip fully
contained in `main`, verified with
`gh api repos/lutzkind/github-mcp-server/compare/main...<branch>` showing
`ahead_by: 0`, and no open PR on the branch. **15 branches qualified and were
deleted:**

| Branch | Tip (deleted) |
|---|---|
| `420-failed-api-calls-should-not-be-failures-not-errors` | `414309cc` |
| `add-lockdown-cache` | `efe9d40b` |
| `albarry-mcp` | `89e3afdb` |
| `categorise-error-types-in-monitors` | `23b16cfe` |
| `codex/isolation-auth-git-inspection-20260726` | `a348d71e` (tip is an ancestor of `main`) |
| `copilot/mcp-servers-plan-mode` | `3a6a6f66` |
| `full-go-version` | `02ef88d8` |
| `juruen/fix-log-file` | `bb9f2b8e` |
| `juruen/iologging` | `6d637779` |
| `next` | `3deaca89` |
| `o1-mise-instructions-update` | `2cece8f9` |
| `pagination-cursor-rethink` | `05e0f8f1` |
| `pre-minimal-changes` | `b2faa1c3` |
| `sammorrowdrums/add-agent-skills` | `b1575edf` |
| `sampling-context-reduction` | `bc4555f0` |

Remaining after cleanup: 257 refs (256 branches + `main`). **All remaining
branches contain unmerged commits and are retained** under the zero-loss rule.
Notable retained branches:

| Branch | Tip | Status | Disposition |
|---|---|---|---|
| `fix-fork-pr-review-comments` | `91b767e` | 1 commit ahead / 395 behind; "Fix add_comment_to_pending_review for fork PRs" (2026-01-06) | genuine fork candidate patch; rebase/evaluate or close |
| `codex/mcp-defect-fixes` | `0c09fd6` | 47 commits ahead / 5 behind (agent scratch from 2026-07) | retained; evaluate or delete in a follow-up |
| `copilot/*` (66), `sammorrowdrums/*` + `SamMorrowDrums/*` (27), `dependabot/*` (7), `tommy/*` (13), `tonytrg/*` (9), `juruen/*` (9), `wm/*` (8), other author/topic branches | — | contain unmerged commits | mirrored upstream contributor/topic refs; do **not** delete from the fork without confirming upstream state |

Policy going forward: branch deletion is allowed only with `ahead_by: 0` (or
after the unique commits are demonstrably merged/obsolete), no open PR, and a
recorded tip SHA in this file or the sync PR.

## 7. Upstream sync mechanism

- **Script**: `script/upstream-sync` — fetches `upstream/main`, creates
`upstream-sync/YYYYMMDD` from `origin/main`, attempts merge (`--mode rebase`
available), records conflicts in `UPSTREAM_SYNC_CONFLICTS.md` with
`--report-conflicts`, pushes with `--push`, opens a PR against `main` with
`--pr`. Refuses to run on `main`; refuses to overwrite an existing branch.
- **Workflow**: `.github/workflows/upstream-sync.yml` — `workflow_dispatch`
(merge/rebase choice) plus a monthly schedule; `contents: write` +
`pull-requests: write` only; `actions/checkout` pinned to
`3d3c42e5aac5ba805825da76410c181273ba90b1` (v7.0.1, matching the major the
repo already uses). The workflow only pushes `upstream-sync/<date>` and opens
a PR.
- **Upstream remote**: `https://github.com/github/github-mcp-server.git`, added
as remote `upstream` (the script adds/updates it idempotently).
- **Cadence**: monthly on the first business day (scheduled run), plus within
5 business days of an upstream release tag; dispatch manually any time.
- `workflow_dispatch` becomes available once this workflow is on the default
branch (`main`).

## 8. Known gaps / recommendations

- `main` is **not branch-protected** (verified `404` on 2026-09-19). Recommend a
ruleset requiring PR review and `Build and Test Go Project` checks.
- Actions on the fork must be allowed to write (`contents: write`) and create
PRs (repo setting "Allow GitHub Actions to create and approve pull requests")
for the sync workflow to open its PR; otherwise use the script locally.
- Existing upstream CI workflows use tag-pinned actions (`actions/checkout@v7`,
`setup-go@v6`, …). They are upstream-owned; SHA-pinning the new sync workflow
only is deliberate to avoid merge churn (PA-62 scope).
176 changes: 176 additions & 0 deletions script/upstream-sync
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
#!/usr/bin/env bash
#
# upstream-sync - prepare an upstream integration branch for lutzkind/github-mcp-server.
#
# This helper NEVER pushes to main. It:
# 1. fetches origin/main and upstream/main,
# 2. creates upstream-sync/YYYYMMDD from origin/main,
# 3. attempts to merge (default) or rebase onto upstream/main,
# 4. with --push, pushes the sync branch to origin,
# 5. with --pr, opens a pull request against main via gh.
#
# On conflict the default behaviour is to abort, leave the checkout on the sync
# branch, and exit 2. With --report-conflicts, the conflicted paths are recorded
# in UPSTREAM_SYNC_CONFLICTS.md and committed to the sync branch so the attempt
# can be reviewed in a PR (used by .github/workflows/upstream-sync.yml).
#
# Usage:
# script/upstream-sync [--mode merge|rebase] [--date YYYYMMDD]
# [--push] [--pr] [--report-conflicts]
# [--upstream-url URL] [--upstream-remote NAME]
#
# Environment:
# GH_TOKEN optional token for authenticated push and `gh pr create` (CI)
#
# Exit codes: 0 success, 2 merge/rebase conflict, 3 preflight failure.

set -euo pipefail

UPSTREAM_URL="${UPSTREAM_URL:-https://github.com/github/github-mcp-server.git}"
UPSTREAM_REMOTE="${UPSTREAM_REMOTE:-upstream}"
MODE=merge
DATE="$(date -u +%Y%m%d)"
PUSH=0
PR=0
REPORT=0

usage() {
sed -n '2,24p' "$0"
}

while [[ $# -gt 0 ]]; do
case "$1" in
--mode) MODE="${2:?--mode needs a value}"; shift 2 ;;
--date) DATE="${2:?--date needs a value}"; shift 2 ;;
--push) PUSH=1; shift ;;
--pr) PR=1; PUSH=1; shift ;;
--report-conflicts) REPORT=1; shift ;;
--upstream-url) UPSTREAM_URL="${2:?--upstream-url needs a value}"; shift 2 ;;
--upstream-remote) UPSTREAM_REMOTE="${2:?--upstream-remote needs a value}"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "unknown option: $1" >&2; usage >&2; exit 3 ;;
esac
done

if [[ "$MODE" != merge && "$MODE" != rebase ]]; then
echo "error: --mode must be 'merge' or 'rebase' (got '$MODE')" >&2
exit 3
fi

cd "$(git rev-parse --show-toplevel)"

if [[ -n "$(git status --porcelain)" ]]; then
echo "error: working tree is not clean; commit or stash first" >&2
exit 3
fi

BRANCH="upstream-sync/$DATE"
if [[ "$BRANCH" == "main" ]]; then
echo "error: refusing to operate on main" >&2
exit 3
fi

git fetch origin main --no-tags
git remote add "$UPSTREAM_REMOTE" "$UPSTREAM_URL" 2>/dev/null \
|| git remote set-url "$UPSTREAM_REMOTE" "$UPSTREAM_URL"
git fetch "$UPSTREAM_REMOTE" main --no-tags

if git show-ref --verify --quiet "refs/heads/$BRANCH"; then
echo "error: local branch $BRANCH already exists; delete it or pass --date" >&2
exit 3
fi

BASE_SHA="$(git rev-parse origin/main)"
UPSTREAM_SHA="$(git rev-parse "$UPSTREAM_REMOTE/main")"
echo "origin/main $BASE_SHA"
echo "upstream/main $UPSTREAM_SHA"
echo "sync branch $BRANCH (from origin/main, never main)"

git checkout -b "$BRANCH" origin/main

CONFLICTS=""
if [[ "$MODE" == merge ]]; then
if ! git merge --no-edit "$UPSTREAM_REMOTE/main"; then
CONFLICTS="$(git diff --name-only --diff-filter=U | sort -u)"
git merge --abort
fi
else
if ! git rebase "$UPSTREAM_REMOTE/main"; then
CONFLICTS="$(git diff --name-only --diff-filter=U | sort -u)"
git rebase --abort
fi
fi

if [[ -n "$CONFLICTS" ]]; then
echo "upstream sync conflicts in:"
printf ' %s\n' $CONFLICTS
if [[ "$REPORT" == 1 ]]; then
{
echo "# Upstream sync conflicts - $DATE"
echo
echo "Automated ${MODE} of \`upstream/main\` ($UPSTREAM_SHA) into \`origin/main\` ($BASE_SHA)"
echo "conflicted. Resolve the paths below on this branch and delete this file,"
echo "then rerun \`script/test\` before merging."
echo
echo "Conflict policy: see FORK_GOVERNANCE.md. Guarded mutation code in"
echo "\`pkg/github/repositories.go\` must keep the fork preview/target binding"
echo "semantics; \`pkg/github/mutation_guard_test.go\` and \`pkg/github/file_patches_test.go\`"
echo "are the invariant tests."
echo
echo "## Conflicted paths"
echo
printf -- '- `%s`\n' $CONFLICTS
echo
echo "## Reproduction"
echo
echo '```'
echo "git fetch $UPSTREAM_REMOTE main"
echo "git checkout $BRANCH"
echo "git merge $UPSTREAM_REMOTE/main # or: git rebase $UPSTREAM_REMOTE/main"
echo '```'
} > UPSTREAM_SYNC_CONFLICTS.md
git add UPSTREAM_SYNC_CONFLICTS.md
git commit -m "chore(upstream-sync): record $DATE conflict set"
echo "conflict report committed on $BRANCH"
else
echo "aborted with no changes; rerun with --report-conflicts to record the list" >&2
exit 2
fi
fi

if [[ "$PUSH" == 1 ]]; then
ORIGIN_URL="$(git remote get-url origin)"
if [[ -n "${GH_TOKEN:-}" && "$ORIGIN_URL" == https://github.com/* ]]; then
git -c http.extraheader="AUTHORIZATION: bearer ${GH_TOKEN}" \
push origin "HEAD:refs/heads/${BRANCH}"
else
git push origin "HEAD:refs/heads/${BRANCH}"
fi
else
echo "not pushed (pass --push to publish $BRANCH)"
fi

if [[ "$PR" == 1 ]]; then
command -v gh >/dev/null 2>&1 || { echo "error: gh not found; push done, open the PR manually" >&2; exit 3; }
EXISTING="$(gh pr list --head "$BRANCH" --state open --json url --jq '.[0].url // empty')"
if [[ -n "$EXISTING" ]]; then
echo "open PR already exists: $EXISTING"
else
TITLE="chore(upstream-sync): integrate upstream/main ($DATE)"
if [[ -n "$CONFLICTS" ]]; then
TITLE="[conflicts] chore(upstream-sync): upstream/main ($DATE) needs manual resolution"
fi
gh pr create --base main --head "$BRANCH" --title "$TITLE" --body \
"Automated upstream sync branch \`$BRANCH\`.

- fork base: \`$BASE_SHA\` (origin/main)
- upstream: \`$UPSTREAM_SHA\` ($UPSTREAM_REMOTE/main)
- mode: \`$MODE\`
- conflicts: \`${CONFLICTS:-none}\`

Review FORK_GOVERNANCE.md for the conflict policy before merging.
This PR is the only integration path; automation never pushes to main."
fi
fi

echo "done: $BRANCH is at $(git rev-parse --short HEAD)"
Loading