docs: make ngit canonical and gate mirror publication
This commit is contained in:
@@ -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.
|
and app cleanup must preserve wallets, persistent data and uninstall decisions.
|
||||||
Do not mark the new paid-file incident resolved merely because the earlier
|
Do not mark the new paid-file incident resolved merely because the earlier
|
||||||
Framework LND startup incident was closed.
|
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
@@ -64,12 +64,49 @@ App submissions must:
|
|||||||
|
|
||||||
## Pull requests
|
## 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.
|
2. Explain what changed, why it changed, and how it was verified.
|
||||||
3. Include screenshots for UI changes.
|
3. Include screenshots for UI changes.
|
||||||
4. Link relevant issues or docs.
|
4. Link relevant issues or docs.
|
||||||
5. Keep generated catalog changes in sync with manifest changes.
|
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:
|
Suggested commit format:
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
@@ -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())
|
||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user