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
190 changes: 190 additions & 0 deletions .pi/skills/sync-upstream-fork/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
---
name: sync-upstream-fork
description: Synchronize a maintained GitHub fork with an upstream branch or release tag while preserving downstream customizations, package identities, release boundaries, and vendored consumer artifacts. Use for "upstream sync", "fork 최신화", "fork rebase/merge", integrating an upstream release, canonicalizing fork branches, or planning recurring upstream maintenance.
version: 1
created: 2026-08-05
updated: 2026-08-05
---

# Sync Upstream Fork

## Goal

Integrate reviewed upstream history without rewriting shared downstream history or silently dropping fork-owned behavior.

## This repository

- Downstream: `aeei/docusaurus-theme`
- Upstream: `PaloAltoNetworks/docusaurus-openapi-docs`
- Canonical branch: `main`
- Baseline SSOT: `UPSTREAM.md`
- Upstream-owned: OpenAPI plugin, schema renderer, demo, and their package versions
- AEEI-owned: `packages/docusaurus-theme/`, `examples/docs-starter/`, Base Nova audits/visual tests, legal notices, and `@aeei/docusaurus-theme` release
- Shared reconciliation: root manifests, `yarn.lock`, workflows, and top-level docs
- Consumer artifact: Deck repository-owned `docs/manual/vendor/docusaurus-theme.tgz`

Re-query the upstream latest GitHub release and default-branch head on every run. Never assume the baseline recorded in `UPSTREAM.md` is still current.

## Non-negotiable rules

- Read repository instructions and `UPSTREAM.md` before mutation.
- Use an isolated clean worktree. Never stash, reset, clean, force-push, or move unrelated user changes.
- Never rebase a shared canonical branch onto upstream. Merge an exact upstream tag/commit through a sync PR.
- Do not call an unsigned tag signed. Record its actual object type and verification result.
- Never resolve lockfiles with a blind `ours` or `theirs`. Regenerate them from the combined manifests.
- Preserve package identity and downstream release boundaries. Never reuse a package version for different archive bytes.
- Keep upstream sync, downstream feature work, package publication, and consumer artifact updates in separate reviewable stages.

## Workflow

### 1. Establish repository truth

1. Find repo root, current branch, dirty state, remotes, and GitHub fork metadata.
2. Resolve the canonical downstream branch from repository policy—not from stale GitHub defaults.
3. Identify the upstream repository, branch, exact release tag/commit, and current integrated baseline.
4. If duplicate downstream branches have unrelated histories:
- compare semantic contents and commit ancestry;
- archive stale heads with annotated tags;
- switch default/protection/workflow targets to the canonical branch;
- delete stale branches only after explicit authorization and recovery-tag verification.

Recommended evidence:

```bash
git status --short
git remote -v
gh api repos/OWNER/REPO --jq '{fork,parent:.parent.full_name,default_branch}'
git rev-list --left-right --count DOWNSTREAM...UPSTREAM
git merge-base DOWNSTREAM UPSTREAM
```

### 2. Analyze divergence before choosing a strategy

Run [`scripts/analyze-fork.sh`](scripts/analyze-fork.sh) with downstream and upstream refs.

Review:

- ahead/behind counts;
- common-base identity;
- changed-file overlap;
- `git merge-tree` predicted conflicts;
- functional/security upstream commits versus dependency-only commits;
- branch, workflow, version, and package-name drift.

Decision:

- **Tag merge**: default for maintained forks with shared history.
- **Selective cherry-pick**: only when fork policy intentionally tracks a documented subset.
- **Rebase**: only for an unpublished private patch queue and explicit user approval.

### 3. Record ownership

Create or verify a path ownership map. Use [`references/ownership-template.md`](references/ownership-template.md).

Typical classes:

- upstream-owned source;
- downstream-owned source;
- shared reconciliation surfaces;
- generated files and lockfiles;
- release metadata and consumer artifacts.

Do not resolve a conflict until its owner and preserved behavior are known.

### 4. Merge on an integration branch

```bash
git switch -c sync/upstream-vX.Y.Z origin/CANONICAL
git merge --no-ff upstream-tag-or-commit
```

For each conflict:

1. inspect base/ours/theirs;
2. apply the ownership policy;
3. regenerate lock/generated output from combined source inputs;
4. add a regression contract when the conflict exposes an undocumented invariant.

Preserve the upstream merge commit. If GitHub disallows merge commits, change repository merge policy before merging the sync PR; do not flatten upstream history accidentally.

### 5. Enforce artifact immutability

Before and after sync:

1. build the downstream package from a clean dependency state;
2. create the real package archive, not only `--dry-run` output;
3. compare archive file lists, semantic content, and cryptographic hashes;
4. if bytes change, bump the downstream package version—even when only toolchain formatting changed;
5. rebuild and record the final hash;
6. update every repository-owned consumer archive, integrity, lockfile, and contract in a separate consumer PR.

Never publish or vendor two different archives under one version.

### 6. Validate both inheritance directions

Run all relevant checks:

- frozen dependency install;
- upstream unit/integration regression tests;
- downstream contracts and independent review;
- all package builds;
- starter/example production build;
- browser/accessibility/visual matrix;
- package contents, notices, provenance, and hash;
- LSP/type diagnostics;
- workflow lint and `git diff --check`.

Use [`references/release-gates.md`](references/release-gates.md) for release and consumer gates.

### 7. Deliver in ordered PRs

1. Canonical branch/settings PR and repository-setting changes.
2. Upstream sync PR with merge history preserved.
3. Fork-specific release workflow/policy PR.
4. Consumer vendored-artifact PR when archive bytes changed.
5. Documentation/skill update after the process is proven.

PR body must state:

- upstream tag and commit;
- previous baseline;
- ahead/behind and predicted conflicts;
- conflict resolutions;
- downstream package version/hash decision;
- validation evidence;
- residual external configuration.

### 8. Release safely

- Upstream release automation must not publish fork-owned or upstream packages accidentally.
- Prefer manual environment-gated npm trusted publishing for a fork-owned package.
- Build/package in an unprivileged job; hand off one verified tarball to the privileged publish job.
- Pin Node/npm and action SHAs.
- Verify tar manifest name/version, artifact SHA-512, registry `dist.integrity`, and exact source SHA.
- Reserve or verify the immutable source tag before npm publish. Support a documented exact-SHA retry/recovery path.
- Protect release tags from update, deletion, and non-fast-forward mutation.
- Treat npm/GitHub environment bindings as external release gates; never claim publish-ready until verified.

## Stop conditions

Stop and ask the user when:

- canonical branch or upstream source is ambiguous;
- upstream tag provenance is unexpected;
- conflict ownership is unclear;
- sync changes a public API or package identity without a migration decision;
- archive bytes change but no version bump/consumer update is approved;
- required credentials, trusted-publisher binding, environment approval, or tag policy is missing;
- any required validation fails.

## Final report

Report:

- canonical/downstream/upstream refs and commits;
- merge strategy and conflict list;
- preserved downstream surfaces;
- package version and archive hash before/after;
- PRs/settings changed;
- validation results;
- remaining external or operational risks.
16 changes: 16 additions & 0 deletions .pi/skills/sync-upstream-fork/references/ownership-template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Fork ownership template

Fill this before resolving upstream conflicts.

| Surface | Owner | Resolution rule | Required validation |
| ---------------------------- | ------------------- | --------------------------------------------------------------------------- | ------------------------------------ |
| Original upstream packages | Upstream | Take reviewed upstream tag unless downstream contract intentionally differs | Upstream tests/build |
| Fork-owned packages/features | Downstream | Preserve downstream behavior and package identity | Downstream contracts/browser/package |
| Root manifests | Shared | Reconcile all workspaces and engines explicitly | Frozen install/full build |
| Lockfiles | Generated | Regenerate from combined manifests; never blind ours/theirs | Frozen reinstall/no diff |
| Workflows | Shared | Preserve fork repo guards and adopt compatible upstream security fixes | actionlint/policy tests |
| Notices/provenance | Shared | Distinguish runtime version from copied-source snapshot | Legal/package contracts |
| Release versions/tags | Downstream | Never reuse version for changed bytes | Pack hash/registry/tag checks |
| Vendored consumer archive | Downstream consumer | Update exact archive, hash, integrity, and every consumer lockfile together | Clean consumer installs/builds |

Record intentional exceptions in the repository's `UPSTREAM.md` or equivalent SSOT.
35 changes: 35 additions & 0 deletions .pi/skills/sync-upstream-fork/references/release-gates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
# Release and consumer gates

## Package gate

- [ ] Exact package name and version verified inside the tar manifest.
- [ ] Real tarball generated from clean build output.
- [ ] SHA-256/SHA-512 recorded.
- [ ] Previous archive compared semantically and byte-for-byte.
- [ ] Version bumped when bytes changed.
- [ ] Licenses, notices, copied-source provenance, and package file list verified.
- [ ] No unrelated workspace package can be published by the workflow.

## Trusted publishing gate

- [ ] Workflow is manual-only and canonical-main-only.
- [ ] Exact workflow filename/repository/environment bound in npm trusted publisher settings.
- [ ] Build job has read-only contents and no persisted Git credentials or OIDC permission.
- [ ] Privileged job has no checkout/build/untrusted code execution.
- [ ] Protected environment reviewer and branch policy verified through GitHub API/UI.
- [ ] Exact version and operation confirmation required.
- [ ] Source SHA is a canonical-main ancestor and matches checked-out HEAD.
- [ ] Source tag is reserved/verified before npm publish.
- [ ] Tag update/deletion/non-fast-forward protections active.
- [ ] Registry `dist.integrity` equals the uploaded tarball.
- [ ] Exact-SHA retry/recovery path documented and tested.

## Consumer gate

- [ ] Repository-owned archive replaced with the reviewed package tarball.
- [ ] Consumer contract expected hash/version updated.
- [ ] Every consumer lockfile records identical local path, version, and integrity.
- [ ] Frozen installs pass with the CI package-manager major/version.
- [ ] Typechecks, production builds, runtime packaging, and browser audit pass.
- [ ] Behavior delta/spec updated when externally observable behavior changes.
- [ ] Consumer PR merged only after upstream package PR and validation are complete.
53 changes: 53 additions & 0 deletions .pi/skills/sync-upstream-fork/scripts/analyze-fork.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
set -euo pipefail

usage() {
echo "usage: $0 <downstream-ref> <upstream-ref>" >&2
exit 64
}

[[ $# -eq 2 ]] || usage

downstream=$1
upstream=$2

git rev-parse --is-inside-work-tree >/dev/null
git rev-parse --verify "${downstream}^{commit}" >/dev/null
git rev-parse --verify "${upstream}^{commit}" >/dev/null

base=$(git merge-base "$downstream" "$upstream")
read -r ahead behind < <(git rev-list --left-right --count "$downstream...$upstream")

tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT

git diff --name-only "$base..$downstream" | LC_ALL=C sort >"$tmp_dir/downstream-files"
git diff --name-only "$base..$upstream" | LC_ALL=C sort >"$tmp_dir/upstream-files"
comm -12 "$tmp_dir/downstream-files" "$tmp_dir/upstream-files" >"$tmp_dir/overlap-files"

printf 'downstream: %s (%s)\n' "$downstream" "$(git rev-parse "${downstream}^{commit}")"
printf 'upstream: %s (%s)\n' "$upstream" "$(git rev-parse "${upstream}^{commit}")"
printf 'merge-base: %s\n' "$base"
printf 'ahead: %s\n' "$ahead"
printf 'behind: %s\n' "$behind"
printf 'downstream changed files: %s\n' "$(wc -l <"$tmp_dir/downstream-files" | tr -d ' ')"
printf 'upstream changed files: %s\n' "$(wc -l <"$tmp_dir/upstream-files" | tr -d ' ')"
printf 'overlapping files: %s\n' "$(wc -l <"$tmp_dir/overlap-files" | tr -d ' ')"

if [[ -s "$tmp_dir/overlap-files" ]]; then
echo
echo 'overlap:'
cat "$tmp_dir/overlap-files"
fi

echo
echo 'merge-tree preview:'
set +e
git merge-tree --write-tree "$downstream" "$upstream"
merge_tree_status=$?
set -e
printf 'merge-tree exit: %s\n' "$merge_tree_status"

# A conflict preview is evidence, not a script failure. Missing refs and other
# preflight errors already fail above.
exit 0