docs: make ngit canonical and gate mirror publication

This commit is contained in:
archipelago
2026-10-05 11:47:12 -04:00
parent 833c939220
commit 138a541d01
4 changed files with 217 additions and 1 deletions
+31
View File
@@ -40,3 +40,34 @@ acceptance. In particular, paid-file recovery must not send another payment,
and app cleanup must preserve wallets, persistent data and uninstall decisions.
Do not mark the new paid-file incident resolved merely because the earlier
Framework LND startup incident was closed.
## Gitea and ngit mirror parity
Nostr Git (`ngit`) is the canonical contribution and review platform. Gitea
(`origin`) mirrors accepted code on `main` and release tags. Both are required
publication mirrors; duplicate PRs and proposal branches on Gitea are not required.
For every change, including fixes and release preparation:
- Review and merge once. Push the exact same resulting commits to both mirrors;
never independently squash, rebase or merge the same change on each platform.
- Open new contributions and PRs on ngit; review and merge there, then mirror the
exact accepted main commits to Gitea. Record the ngit proposal and resulting
merge commit in the release ledger. Existing Gitea PRs must be reviewed and
explicitly linked to their ngit replacement or accepted result before closing;
do not abandon contributions or mark unmerged changes as merged. PR numbers,
reviews and discussions remain platform-specific; matching Git refs does not
prove their synchronization.
- Push main and release tags to both mirrors. Preserve commit history
and annotated tag objects/signatures. Do not resolve drift by force pushing,
deleting remote refs, or rewriting published history without explicit approval.
- After publishing source, run `python3 scripts/check-git-mirrors.py --local`.
Include each additional shared branch or release tag with repeated `--ref`
arguments (full `refs/heads/...` or `refs/tags/...` names).
- Before OTA, catalog or ISO publication, require matching reviewed local and
remote main and release tag refs, and record ngit PR dispositions in the
release acceptance ledger. A failed push, unavailable mirror, missing ref or
mismatch blocks publication; never describe a partial push as synchronized.
Run `--all` for a complete advertised branch/tag audit; a main-only pass must
never be described as full historical mirror parity. Proposal-only branches
may intentionally differ. Existing unrelated drift
must be inventoried explicitly rather than silently overwritten.
+38 -1
View File
@@ -64,12 +64,49 @@ App submissions must:
## Pull requests
1. Open one focused PR per behavior or documentation change.
Contributions, PRs and reviews live on **ngit**. Clone the canonical repository:
```text
nostr://npub1w3sqdkrhn0gyuvsex32effzgnfpyde6qrrc4u467flg5e9txh4wsfn5vjg/relay.ngit.dev/archy
```
Gitea is a conventional Git mirror of accepted `main` commits and release tags.
You do not need to open a duplicate Gitea PR. Existing Gitea contributions will
be reviewed and linked to their ngit replacement or accepted result before
closure.
1. Open one focused ngit PR per behavior or documentation change.
2. Explain what changed, why it changed, and how it was verified.
3. Include screenshots for UI changes.
4. Link relevant issues or docs.
5. Keep generated catalog changes in sync with manifest changes.
### Maintainer publication gate
Merge once through the ngit contribution workflow, then push the exact same
accepted commits to Gitea. Do not independently merge or squash on each mirror.
Publish identical release tag objects, including annotations and signatures.
After pushing main, verify:
```bash
python3 scripts/check-git-mirrors.py --local
```
Before publishing release artifacts, also check the actual release tag:
```bash
python3 scripts/check-git-mirrors.py --local --ref refs/tags/v1.9.0
```
Use the release's actual tag name. Missing refs, inaccessible mirrors or differing
object IDs block publication. Record the ngit PR disposition and resulting merge
commit in the release acceptance ledger. Resolve drift deliberately; do not
force-push or delete published history without explicit approval.
The checker is read-only. `--all` audits every advertised branch and tag; ngit
proposal branches may intentionally differ from Gitea. A main-only pass proves
only main parity, and no Git ref check verifies PR discussions or review state.
Suggested commit format:
```text
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""Read-only check of advertised Git refs; does not inspect PR metadata."""
import argparse
import re
import subprocess
import sys
def git(*args):
result = subprocess.run(['git', *args], capture_output=True, text=True,
timeout=120)
if result.returncode:
# Transport errors can contain credential-bearing remote URLs.
raise ValueError('Git lookup failed; check mirror access privately')
return result.stdout
def parse_refs(output):
return {ref: sha for sha, ref in (line.split() for line in output.splitlines())
if ref.startswith(('refs/heads/', 'refs/tags/'))}
def compare(left, right, refs):
failures = []
for ref in sorted(refs):
if ref not in left or ref not in right:
failures.append(f'{ref}: missing from at least one side')
elif left[ref] != right[ref]:
failures.append(f'{ref}: different object IDs')
return failures
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--remotes', nargs=2, default=['origin', 'ngit'])
parser.add_argument('--ref', action='append', default=[],
help='additional full branch/tag ref; main is always checked')
parser.add_argument('--all', action='store_true', help='audit all advertised branches/tags')
parser.add_argument('--local', action='store_true', help='also require local refs to match')
args = parser.parse_args()
try:
configured = set(git('remote').splitlines())
if any(remote not in configured for remote in args.remotes):
raise ValueError('Both arguments must name configured remotes')
refs = {'refs/heads/main', *args.ref}
for ref in refs:
if not re.match(r'^refs/(heads|tags)/', ref):
raise ValueError('Use full refs/heads/... or refs/tags/... names')
git('check-ref-format', ref)
left, right = [parse_refs(git('ls-remote', remote)) for remote in args.remotes]
if args.all:
refs.update(left)
refs.update(right)
# Compare both annotated tag objects and their peeled target commits.
refs.update(ref + '^{}' for ref in list(refs)
if ref + '^{}' in left or ref + '^{}' in right)
failures = compare(left, right, refs)
if args.local:
local = parse_refs(git('show-ref', '--dereference'))
failures += ['local: ' + error for error in compare(left, local, refs)]
if failures:
print('\n'.join(failures), file=sys.stderr)
return 1
print(f'PASS: {len(refs)} refs match on both mirrors'
+ (' and locally' if args.local else '')
+ '; PR metadata not checked.')
return 0
except (ValueError, subprocess.TimeoutExpired, OSError):
print('FAIL: unable to validate refs; check arguments and mirror access privately.',
file=sys.stderr)
return 1
if __name__ == '__main__':
sys.exit(main())
+73
View File
@@ -0,0 +1,73 @@
"""Exercise mirror gate against disposable local Git remotes."""
import pathlib
import subprocess
import sys
import tempfile
import unittest
CHECK = pathlib.Path(__file__).resolve().parents[1] / 'check-git-mirrors.py'
class MirrorTests(unittest.TestCase):
def test_publication_drift_and_annotated_tags(self):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
work = root / 'work'
work.mkdir()
def git(*args):
return subprocess.run(['git', *args], cwd=work, check=True,
capture_output=True, text=True).stdout
def check(*args, passes=True):
result = subprocess.run([sys.executable, str(CHECK), *args],
cwd=work, capture_output=True, text=True)
self.assertEqual(result.returncode, 0 if passes else 1,
result.stdout + result.stderr)
git('init', '-b', 'main')
git('config', 'user.name', 'Mirror test')
git('config', 'user.email', 'fixture@example.invalid')
git('config', 'commit.gpgsign', 'false')
git('config', 'tag.gpgsign', 'false')
for remote in ('origin', 'ngit'):
git('init', '--bare', str(root / remote))
git('remote', 'add', remote, str(root / remote))
git('commit', '--allow-empty', '-m', 'first')
check(passes=False) # Empty mirrors cannot pass.
git('push', 'origin', 'main')
check(passes=False) # Partial publication.
git('push', 'ngit', 'main')
check('--local')
git('commit', '--allow-empty', '-m', 'second')
check() # Remote equality alone does not imply local publication.
check('--local', passes=False)
git('push', 'origin', 'main')
check(passes=False)
git('push', 'ngit', 'main')
git('tag', '-a', 'v-test', '-m', 'original')
git('push', 'origin', 'refs/tags/v-test')
check('--ref', 'refs/tags/v-test', passes=False)
git('push', 'ngit', 'refs/tags/v-test')
check('--local', '--ref', 'refs/tags/v-test')
# Same commit, different annotation must fail too.
git('tag', '-f', '-a', 'v-test', '-m', 'different annotation')
check('--local', '--ref', 'refs/tags/v-test', passes=False)
git('push', '--force', 'ngit', 'refs/tags/v-test')
check('--all', passes=False)
git('tag', '-d', 'v-test')
git('fetch', 'origin', 'refs/tags/v-test:refs/tags/v-test')
git('push', '--force', 'ngit', 'refs/tags/v-test')
git('push', 'origin', 'HEAD:refs/heads/extra')
check('--local')
check('--all', passes=False)
git('push', 'ngit', 'HEAD:refs/heads/extra')
check('--all')
check('--ref', 'main', passes=False)
check('--remotes', 'origin', 'missing', passes=False)
git('remote', 'set-url', 'ngit', str(root / 'does-not-exist'))
check(passes=False)
if __name__ == '__main__':
unittest.main()