fix(framework): read pinned policy from repo root and compare pages unordered
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
Codex committed 2026-11-05 13:16:32 -06:00
1 parent ed5332b73b
commit 01662b281f
4 files changed
+75 -7

No files matched your search

@@ -318,7 +318,11 @@ class Client:
by_id = all(isinstance(row, dict) and row.get('id') is not None for row in previous + value)
before = [row['id'] for row in previous] if by_id else previous
current = [row['id'] for row in value] if by_id else value
if current == before:
if by_id:
repeated = sorted(current, key=lambda item: json.dumps(item, sort_keys=True)) == sorted(before, key=lambda item: json.dumps(item, sort_keys=True))
else:
repeated = sorted(json.dumps(row, sort_keys=True) for row in current) == sorted(json.dumps(row, sort_keys=True) for row in before)
if repeated:
if len(result) < expected_total:
raise Failure(6, 'incomplete', 'Provider returned fewer records than total_count')
return result
@@ -644,7 +648,7 @@ def declared_method(client, p, requested):
revision = base['sha']
if run(['git', 'cat-file', '-e', revision + '^{commit}']).returncode:
raise Failure(3, 'policy', 'POLICY-REFUSED: fetch the pinned target revision before merging')
listing = run(['git', 'ls-tree', revision, '--', '.mosaic/repo.json'])
listing = run(['git', 'ls-tree', '--full-tree', revision, '--', '.mosaic/repo.json'])
if listing.returncode:
raise Failure(6, 'verification', 'Cannot inspect pinned target declaration')
if not listing.stdout.strip():
@@ -190,7 +190,7 @@ if ! git cat-file -e "$BASE_SHA^{commit}" 2>/dev/null; then
echo "POLICY-REFUSED: pinned base commit is not available locally; fetch it before merging." >&2
exit 3
fi
if ! git ls-tree "$BASE_SHA" -- .mosaic/repo.json > "$pinned_dir/listing" 2>/dev/null; then
if ! git ls-tree --full-tree "$BASE_SHA" -- .mosaic/repo.json > "$pinned_dir/listing" 2>/dev/null; then
echo "POLICY-REFUSED: cannot inspect pinned base declaration." >&2
exit 3
fi
@@ -125,6 +125,30 @@ class Contract(unittest.TestCase):
with self.assertRaises(self.api.Failure) as caught: c.pages('/collection')
self.assertEqual((caught.exception.code,caught.exception.kind),(6,'incomplete'))
def test_forge_reordered_repeated_pages_preserve_multiplicity(self):
import urllib.parse
cases=[
([{'id':21},{'id':'22'}],[{'id':'22'},{'id':21}]),
([{'name':'one','detail':{'a':1,'b':2}},{'name':'two'}],[{'name':'two'},{'detail':{'b':2,'a':1},'name':'one'}]),
]
for provider in ('gitea','forgejo'):
for first,second in cases:
with self.subTest(provider=provider,first=first):
c=self.api.Client('git.example.test','owner/repo'); c.provider=provider
calls=[]
def http(path):
page=int(urllib.parse.parse_qs(urllib.parse.urlsplit(path).query)['page'][0]); calls.append(page)
return first if page==1 else second
c.http=http
self.assertEqual(c.pages('/reviews/1/comments'),first)
self.assertEqual(calls,[1,2])
for first,second in [([{'id':1},{'id':1},{'id':2}],[{'id':1},{'id':2},{'id':2}]), ([{'name':'a'},{'name':'a'},{'name':'b'}],[{'name':'a'},{'name':'b'},{'name':'b'}])]:
with self.subTest(multiplicity=first):
c=self.api.Client('git.example.test','owner/repo'); c.provider='gitea'
c.http=lambda path: first if 'page=1' in path else second
with self.assertRaises(self.api.Failure) as caught: c.pages('/collection')
self.assertEqual(caught.exception.code,6)
def test_forge_partial_overlap_fails_closed(self):
for provider in ('gitea','forgejo'):
for first,second in [([{'id':1},{'id':2}],[{'id':2},{'id':3}]), ([{'name':'one'},{'name':'two'}],[{'name':'two'},{'name':'three'}])]:
@@ -69,6 +69,28 @@ class Policy(unittest.TestCase):
self.assertEqual(api.declared_method(c,p,None),'squash')
finally: os.chdir(old)
def test_adapter_reads_pinned_declaration_from_subdirectory(self):
import os, importlib.util
spec=importlib.util.spec_from_file_location('subdir_policy',HERE/'forge-api.py')
api=importlib.util.module_from_spec(spec); spec.loader.exec_module(api)
with tempfile.TemporaryDirectory() as directory:
root=pathlib.Path(directory); (root/'.mosaic').mkdir(); (root/'subdir').mkdir()
def git(*args):
return subprocess.run(['git',*args],cwd=root,capture_output=True,text=True,check=True).stdout.strip()
git('init','-q'); git('remote','add','origin','https://git.example.test/owner/stack.git')
doc=dict(schema_version=3,integration_trunk='next',release_branch='main',flow='trunk-release',canonical_remote='https://git.example.test/owner/stack',canonical_clone='host:/src/project',merge=dict(allowed_methods=['merge'],default_method='merge',shapes={'promotion':'merge'}))
(root/'.mosaic/repo.json').write_text(json.dumps(doc)); git('add','.mosaic/repo.json')
git('-c','user.name=Fixture','-c','[email protected]','commit','-qm','policy')
p={'head':{'ref':'next','repo':{'full_name':'owner/stack'}},'base':{'ref':'main','sha':git('rev-parse','HEAD'),'repo':{'full_name':'owner/stack'}}}
c=api.Client('git.example.test','owner/stack'); c.http=lambda path: {'default_branch':'main'}
old=os.getcwd()
try:
for cwd in (root,root/'subdir'):
with self.subTest(cwd=cwd):
os.chdir(cwd)
self.assertEqual(api.declared_method(c,p,None),'merge')
finally: os.chdir(old)
def test_branch_precheck_still_requires_absorb_mapping(self):
with tempfile.TemporaryDirectory() as directory:
path=pathlib.Path(directory)/'repo.json'
@@ -166,16 +188,34 @@ class Policy(unittest.TestCase):
git('-c','user.name=Fixture','-c','[email protected]','commit','--allow-empty','-qm','absent')
absent=git('rev-parse','HEAD')
(repo/'.mosaic').mkdir()
doc=dict(schema_version=3,integration_trunk='next',release_branch='main',flow='trunk-release',canonical_remote='https://github.com/owner/repo',canonical_clone='host:/src/repo',merge=dict(allowed_methods=['merge'],default_method='merge',shapes={'promotion':'merge'}))
path=repo/'.mosaic/repo.json'; path.write_text(json.dumps(doc)); git('add','.mosaic/repo.json')
doc=dict(schema_version=3,integration_trunk='next',release_branch='main',flow='trunk-release',canonical_remote='https://github.com/owner/repo',canonical_clone='host:/src/repo',merge=dict(allowed_methods=['squash','merge'],default_method='squash',shapes={'promotion':'merge'}))
path=repo/'.mosaic/repo.json'
merge=doc.pop('merge'); path.write_text(json.dumps(doc)); git('add','.mosaic/repo.json')
git('-c','user.name=Fixture','-c','[email protected]','commit','-qm','declared transition')
transition=git('rev-parse','HEAD')
doc['merge']=merge; path.write_text(json.dumps(doc)); git('add','.mosaic/repo.json')
git('-c','user.name=Fixture','-c','[email protected]','commit','-qm','declared')
declared=git('rev-parse','HEAD')
script=git_tools/'pr-metadata.sh'
metadata=dict(baseRefName='main',baseRefOid='f'*40,baseRepository='owner/repo',headRefName='feature',headRefOid='a'*40,headRepository='owner/repo')
env=dict(os.environ,HOME=str(root),GIT_CONFIG_GLOBAL='/dev/null',GIT_CONFIG_NOSYSTEM='1')
def invoke():
def invoke(cwd=repo):
script.write_text("#!/bin/sh\nprintf '%s\\n' "+repr(json.dumps(metadata))+"\n"); script.chmod(0o755)
return subprocess.run(['bash',str(git_tools/'pr-merge.sh'),'-n','1','--dry-run'],cwd=repo,env=env,capture_output=True,text=True)
return subprocess.run(['bash',str(git_tools/'pr-merge.sh'),'-n','1','--dry-run'],cwd=cwd,env=env,capture_output=True,text=True)
subdir=repo/'subdir'; subdir.mkdir()
metadata['baseRefOid']=declared
for head, code, message in [('feature',1,'transition'),('next',3,'does not select legacy squash')]:
with self.subTest(cwd='subdir', head=head):
metadata['headRefName']=head
metadata['baseRefOid']=transition if head=='feature' else declared
root_result=invoke()
sub_result=invoke(subdir)
self.assertEqual(root_result.returncode,code,root_result.stdout+root_result.stderr)
self.assertEqual(sub_result.returncode,code,sub_result.stdout+sub_result.stderr)
self.assertIn(message,root_result.stderr.lower())
self.assertIn(message,sub_result.stderr.lower())
self.assertEqual(sub_result.stderr,root_result.stderr)
metadata['baseRefOid']='f'*40
for head in ['feature','next']:
with self.subTest(head=head):
metadata['headRefName']=head