fix(framework): fail closed on missing base and overlapping pages
ci/woodpecker/pr/ci Pipeline failed

This commit is contained in:
jason.woltje committed 2026-11-05 12:22:33 -06:00
1 parent abab3a67e2
commit ed5332b73b
14 files changed
+222 -11

No files matched your search

@@ -219,7 +219,7 @@ scheduling may merge immediately and returns observed merged state rather than q
Schema 1/2 remain readable. Schema 3 adds optional `merge`, with strict key/value validation
through the shared declaration validator and repo-decl helper. Consumers apply the existing
branch gates to both versions 2 and 3. Absent merge policy means allowed=[squash], default=squash. The legacy merge interface retains its options and reads a declaration from the PR base commit; a missing or unreadable declaration uses the legacy path. It refuses when that policy selects a non-squash default, unless an explicit squash is allowed. Explicit `--json` calls use the adapter and require `--expect-head`.
branch gates to both versions 2 and 3. Absent merge policy means allowed=[squash], default=squash. The legacy merge interface retains its options and reads a declaration from the PR base commit; an absent declaration uses the legacy path; an unavailable base or a declaration that cannot be read or parsed refuses. It refuses when that policy selects a non-squash default, unless an explicit squash is allowed. Explicit `--json` calls use the adapter and require `--expect-head`.
```json
{
@@ -302,6 +302,7 @@ class Client:
def pages(self, path, key=None):
result = []
expected_total = 0
previous = None
for page in range(1, MAX_PAGES + 1):
query = ('&' if '?' in path else '?') + ('per_page' if self.provider == 'github' else 'limit') + '=100&page=' + str(page)
value = self.http(path + query)
@@ -313,7 +314,18 @@ class Client:
value = value.get(key)
if not isinstance(value, list):
raise Failure(6, 'malformed', 'Provider collection is not a list')
if self.provider in ('gitea', 'forgejo') and previous and value:
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 len(result) < expected_total:
raise Failure(6, 'incomplete', 'Provider returned fewer records than total_count')
return result
if any(row == old for row in current for old in before):
raise Failure(6, 'incomplete', 'Provider pages overlap; collection incomplete')
result.extend(value)
previous = value
if not value or self.provider == 'github' and len(value) < LIMIT:
if len(result) < expected_total:
raise Failure(6,'incomplete','Provider returned fewer records than total_count')
@@ -181,12 +181,25 @@ PR_AUTHOR="$(printf '%s' "$PR_METADATA" | python3 -c 'import json, sys; value=js
# Target branches are validated against the declaration, NEVER hardcoded;
# the legacy main/next check survives only for undeclared repos during the
# rollout window (4.3 irreversible class, loud warning).
# A missing or unreadable declaration at the observed base keeps legacy behavior.
# An absent declaration at the observed base keeps legacy behavior.
# Never load merge policy or transition data from the caller's working tree.
pinned_decl=""
pinned_dir=$(mktemp -d "${TMPDIR:-/tmp}/mosaic-merge-policy.XXXXXX")
MERGE_TEMP_DIRS+=("$pinned_dir")
if git show "$BASE_SHA:.mosaic/repo.json" > "$pinned_dir/repo.json" 2>/dev/null; then
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
echo "POLICY-REFUSED: cannot inspect pinned base declaration." >&2
exit 3
fi
if [[ -s "$pinned_dir/listing" ]]; then
if ! git show "$BASE_SHA:.mosaic/repo.json" > "$pinned_dir/repo.json" 2>/dev/null ||
! python3 -I -c 'import json,sys; d=json.load(open(sys.argv[1])); assert isinstance(d,dict)' "$pinned_dir/repo.json" 2>/dev/null; then
echo "POLICY-REFUSED: pinned base declaration cannot be read or parsed." >&2
exit 3
fi
pinned_decl="$pinned_dir/repo.json"
fi
# shellcheck source=packages/mosaic/framework/tools/git/repo-decl.sh
@@ -30,7 +30,7 @@ for flag in ['--config','-K']:
observed={'path':u.path,'method':method,'argv_leak':any(secret in x for x in a),'authenticated':config=='header = '+json.dumps('Authorization: token '+secret)+chr(10),'anonymous':anonymous}
with open(os.environ['TEST_AUTH_OBSERVATIONS'],'a') as f: f.write(json.dumps(observed)+chr(10))
if observed['argv_leak']: sys.exit(97)
head='a'*40; base='b'*40
head='a'*40; base=os.environ['TEST_AUTH_BASE']
pull=dict(number=1,state='open',draft=False,merged=False,mergeable=True,title='Title',head={'sha':head,'ref':'task','repo':{'full_name':'owner/repo'}},base={'sha':base,'ref':'main','repo':{'full_name':'owner/repo'}},user={'login':'author'})
if u.path=='/swagger.v1.json': body={'info':{'title':'Gitea API','version':'1.24.7'}}
elif u.path.endswith('/user'): body={'login':'fixture-user'}
@@ -111,6 +111,8 @@ class Auth(unittest.TestCase):
repo=self.root/'repo'; repo.mkdir()
subprocess.run(['git','init','-q','-b','task',str(repo)],check=True,env=self.env)
subprocess.run(['git','-C',str(repo),'remote','add','origin','https://forge.example.test/owner/repo'],check=True,env=self.env)
subprocess.run(['git','-C',str(repo),'-c','user.name=Fixture','-c','[email protected]','commit','--allow-empty','-qm','base'],check=True,env=self.env)
self.env['TEST_AUTH_BASE']=subprocess.run(['git','-C',str(repo),'rev-parse','HEAD'],check=True,env=self.env,capture_output=True,text=True).stdout.strip()
bin_dir=self.root/'bin';bin_dir.mkdir()
(bin_dir/'curl').write_text(LEGACY_CURL);(bin_dir/'curl').chmod(0o755)
(bin_dir/'tea').write_text('#!/bin/sh\nexit 1\n');(bin_dir/'tea').chmod(0o755)
@@ -30,6 +30,9 @@ entry=fixtures.get(method+' '+url)
if entry is None:
print('unexpected provider URL '+url,file=sys.stderr); sys.exit(98)
response=entry['response']; path=entry['path']
log=pathlib.Path(os.environ['FAKE_LOG'])
if response.get('after_merge') and log.exists() and any(json.loads(line)['method']=='POST' and json.loads(line)['path'].endswith('/pulls/1/merge') for line in log.read_text().splitlines()):
response=dict(response,data=response['after_merge'])
query=urllib.parse.urlsplit(url).query
if '--input' in args or '--data-binary' in args:
body=json.load(sys.stdin)
@@ -224,6 +227,42 @@ class CLI(unittest.TestCase):
def test_gitea_status_with_unpaginated_comments_and_protections(self):
self.gitea(); head='a'*40
self.fixtures['GET /repos/owner/repo/pulls/1']={'data':{'number':1,'state':'open','draft':False,'merged':False,'mergeable':True,'head':{'sha':head,'ref':'task','repo':{'full_name':'owner/repo'}},'base':{'sha':'b'*40,'ref':'next','repo':{'full_name':'owner/repo'}},'user':{'login':'author'}}}
values={'/repos/owner/repo/branch_protections':[{'branch_name':'next','enable_status_check':True,'status_check_contexts':['build'],'required_approvals':1}],'/repos/owner/repo/issues/1/comments':[{'id':11,'body':'issue'}],'/repos/owner/repo/pulls/1/reviews/1/comments':[{'id':22,'resolver':{'login':'reviewer'},'user':{'login':'reviewer'}}]}
for path,rows in values.items():
self.fixtures['GET '+path]={'data':rows}
self.fixtures['GET '+path+'?limit=100&page=2']={'data':rows}
self.fixtures['GET /repos/owner/repo/commits/'+head+'/statuses']={'data':[{'id':1,'context':'build','state':'success'}]}
self.fixtures['GET /repos/owner/repo/pulls/1/reviews']={'data':[{'id':1,'state':'APPROVED','commit_id':head,'user':{'login':'reviewer'}}]}
p,v=self.run_wrapper('pr-status','-n','1',host='git.example.test')
self.assertEqual(p.returncode,0,p.stdout+p.stderr)
self.assertTrue(v['data']['ready']); self.assertEqual(len(v['data']['issue_comments']),1)
self.assertEqual(len(v['data']['comments']),1)
# Exercise the actual JSON merge wrapper with a local pinned base and a
# fake queue guard. Provider reads and mutation remain fixture-only.
import shutil
repo=self.root/'repo'; repo.mkdir()
def git(*args):
return subprocess.run(['git','-C',str(repo),*args],env=self.env,capture_output=True,text=True,check=True).stdout.strip()
git('init','-q'); git('remote','add','origin','https://git.example.test/owner/repo')
git('-c','user.name=Fixture','-c','[email protected]','commit','--allow-empty','-qm','base')
pr=self.fixtures['GET /repos/owner/repo/pulls/1']['data']
pr['base'].update(sha=git('rev-parse','HEAD'),ref='main')
values['/repos/owner/repo/branch_protections'][0]['branch_name']='main'
self.fixtures['GET /repos/owner/repo']={'data':{'default_branch':'main','allow_squash_merge':True}}
self.fixtures['GET /user']={'data':{'login':'fixture-user'}}
self.fixtures['POST /repos/owner/repo/pulls/1/merge']={'data':None}
self.fixtures['GET /repos/owner/repo/pulls/1']['after_merge']=dict(pr,merged=True,merge_commit_sha='d'*40)
tools=self.root/'tools/git'; shutil.copytree(HERE,tools)
guard=tools/'ci-queue-wait.sh'; guard.write_text('#!/bin/sh\nexit 0\n'); guard.chmod(0o755)
self.write_responses('git.example.test')
p=subprocess.run(['bash',str(tools/'pr-merge.sh'),'--host','git.example.test','--repo','owner/repo','--json','-n','1','--expect-head',head],cwd=repo,env=self.env,capture_output=True,text=True)
self.assertEqual(p.returncode,0,p.stdout+p.stderr)
self.assertTrue(json.loads(p.stdout)['data']['merged'])
self.assertEqual(sum(call['method']=='POST' and call['path'].endswith('/pulls/1/merge') for call in self.calls()),1)
def test_watch_predicate_preserves_baseline_and_read_errors(self):
self.test_gitea_status_and_ci_wait_use_exact_head()
key='GET /repos/owner/repo/commits/'+'a'*40+'/statuses'
@@ -107,6 +107,80 @@ class Contract(unittest.TestCase):
self.assertEqual(len(result), 120)
self.assertEqual(self.api.reviews(result, 'a', 'author')['decision'], 'changes_requested')
def test_forge_duplicate_and_partial_pages(self):
import urllib.parse
for provider in ('gitea','forgejo'):
for first,second in [([{'id':1,'v':'old'},{'id':2}],[{'id':1,'v':'new'},{'id':2}]), ([{'branch_name':'next'}],[{'branch_name':'next'}])]:
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('/collection'),first)
self.assertEqual(calls,[1,2])
for first,second in [([{'id':1},{'id':2}],[{'id':2},{'id':3}]), ([{'name':'one'},{'name':'two'}],[{'name':'two'},{'name':'three'}])]:
c=self.api.Client('git.example.test','owner/repo'); c.provider=provider
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,caught.exception.kind),(6,'incomplete'))
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'}])]:
with self.subTest(provider=provider,first=first):
c=self.api.Client('git.example.test','owner/repo'); c.provider=provider
c.http=lambda path: first if 'page=1' in path else second if 'page=2' in path else []
with self.assertRaises(self.api.Failure) as caught: c.pages('/collection')
self.assertEqual((caught.exception.code,caught.exception.kind),(6,'incomplete'))
def test_forge_unpaginated_collections_status_and_merge_json(self):
import contextlib, copy, io, json, os, sys, urllib.parse
from unittest.mock import patch
api=self.api
for provider,version in [('gitea','1.24.7'),('forgejo','16.0.0-dev')]:
with self.subTest(provider=provider):
c=api.Client('git.example.test','owner/repo'); c.provider=provider; c.version=version
c.detect=lambda: None
head='a'*40
pr={'number':1,'state':'open','draft':False,'merged':False,'mergeable':True,'head':{'sha':head,'ref':'task','repo':{'full_name':'owner/repo'}},'base':{'sha':'b'*40,'ref':'next','repo':{'full_name':'owner/repo'}},'user':{'login':'author'}}
rules=[{'branch_name':'next','enable_status_check':True,'status_check_contexts':['build'],'required_approvals':1}]
issue=[{'id':11,'body':'issue'}]
comment=[{'id':22,'body':'review','resolver':{'login':'reviewer'},'user':{'login':'reviewer'}}]
state={'merged':False,'calls':[]}
def http(path,method='GET',body=None,public=False):
clean=path.split('?',1)[0]; state['calls'].append((clean,method,path))
if method=='POST': state['merged']=True; return None
if clean.endswith('/pulls/1'):
data=copy.deepcopy(pr)
if state['merged']: data.update(merged=True,merge_commit_sha='d'*40)
return data
if clean.endswith('/branch_protections'): return rules
if clean.endswith('/issues/1/comments'): return issue
if clean.endswith('/reviews/1/comments'): return comment
page=int(urllib.parse.parse_qs(urllib.parse.urlsplit(path).query).get('page',['1'])[0])
if clean.endswith('/statuses'): return [{'id':2,'context':'build','state':'success'}] if page==1 else []
if clean.endswith('/reviews'): return [{'id':1,'state':'APPROVED','commit_id':head,'user':{'login':'reviewer'}}] if page==1 else []
if clean==c.root: return {'allow_squash_merge':True}
raise AssertionError(path)
c.http=http
def run_main(operation):
out=io.StringIO()
args=['forge-api.py',operation,'--host','git.example.test','--repo','owner/repo','-n','1','--json','--expect-head',head]
with patch.object(sys,'argv',args), patch.dict(os.environ,{'MOSAIC_GIT_IDENTITY':'fixture'}), patch.object(api,'Client',return_value=c), patch.object(api,'declared_method',return_value='squash'), patch.object(api,'queue_guard'), contextlib.redirect_stdout(out):
code=api.main()
return code,json.loads(out.getvalue())
with self.subTest(operation='status'):
code,data=run_main('status'); self.assertEqual(code,0,data)
self.assertTrue(data['data']['ready']); self.assertEqual(data['data']['issue_comments'],issue)
self.assertEqual(len(data['data']['comments']),1)
with self.subTest(operation='merge'):
code,data=run_main('merge'); self.assertEqual(code,0,data)
self.assertTrue(data['data']['merged'])
self.assertEqual(sum(method=='POST' for _,method,_ in state['calls']),1)
for suffix in ('branch_protections','issues/1/comments','reviews/1/comments'):
self.assertTrue(any(path.endswith(suffix) and 'page=2' in url for path,_,url in state['calls']))
def test_gitea_empty_write_and_post_merge_moved_base(self):
import os, types
from unittest.mock import patch
@@ -150,6 +150,58 @@ class Policy(unittest.TestCase):
result=subprocess.run(['bash',str(git_tools/'pr-merge.sh'),'-n','1','--dry-run'],cwd=repo,env=env,capture_output=True,text=True)
self.assertNotEqual(result.returncode,0,result.stdout+result.stderr)
def test_legacy_pinned_base_availability_and_declaration(self):
import os, shutil
with tempfile.TemporaryDirectory() as directory:
root=pathlib.Path(directory); repo=root/'repo'; repo.mkdir()
git_tools=root/'tools/git'; git_tools.mkdir(parents=True)
structure=root/'tools/structure'; structure.mkdir()
for name in ['pr-merge.sh','repo-decl.sh','merge-policy.py']:
shutil.copy(HERE/name,git_tools/name)
shutil.copy(HERE.parent/'structure/validate-repo-json.sh',structure/'validate-repo-json.sh')
(git_tools/'detect-platform.sh').write_text('detect_platform() { echo github; }\nget_repo_owner() { echo owner; }\nget_repo_name() { echo repo; }\n')
def git(*args):
return subprocess.run(['git',*args],cwd=repo,capture_output=True,text=True,check=True).stdout.strip()
git('init','-q'); git('remote','add','origin','https://github.com/owner/repo')
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')
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():
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)
for head in ['feature','next']:
with self.subTest(head=head):
metadata['headRefName']=head
result=invoke()
self.assertEqual(result.returncode,3,result.stdout+result.stderr)
self.assertIn('fetch',result.stderr.lower())
self.assertIn('not available locally',result.stderr.lower())
metadata.update(headRefName='feature',baseRefName='next',baseRefOid=absent)
result=invoke(); self.assertEqual(result.returncode,0,result.stdout+result.stderr)
self.assertIn('Dry run:',result.stdout)
metadata['baseRefOid']=declared
bindir=root/'bin'; bindir.mkdir()
realgit=shutil.which('git')
fake=bindir/'git'; fake.write_text('#!/bin/sh\ncase "$1" in show) exit 1;; esac\nexec '+realgit+' "$@"\n'); fake.chmod(0o755)
env['PATH']=str(bindir)+os.pathsep+os.environ['PATH']
result=invoke(); self.assertEqual(result.returncode,3,result.stdout+result.stderr)
self.assertIn('declaration',result.stderr.lower())
fake.write_text('#!/bin/sh\ncase "$1" in ls-tree) exit 1;; esac\nexec '+realgit+' "$@"\n')
result=invoke(); self.assertEqual(result.returncode,3,result.stdout+result.stderr)
self.assertIn('inspect',result.stderr.lower())
env['PATH']=os.environ['PATH']
path.write_text('{invalid'); git('add','.mosaic/repo.json')
git('-c','user.name=Fixture','-c','[email protected]','commit','-qm','invalid')
metadata['baseRefOid']=git('rev-parse','HEAD')
result=invoke(); self.assertEqual(result.returncode,3,result.stdout+result.stderr)
def test_github_metadata_reads_pr_repository_identities(self):
import os, shutil
with tempfile.TemporaryDirectory() as directory:
@@ -18,12 +18,15 @@ OUT_LOG="$WORK_DIR/out.log"
rm -rf "$WORK_DIR"
mkdir -p "$FIXTURE_DIR"
git -C "$WORK_DIR" init -q
git -C "$WORK_DIR" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git -C "$WORK_DIR" rev-parse HEAD)"
cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh"
cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' '{"baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"mosaic-cli-p0","baseRepository":"mosaicstack/stack","headRefName":"mosaic-cli-p2-socket-res","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}'
printf '%s\n' '{"baseRefOid":"'"$MOSAIC_TEST_BASE_SHA"'","baseRefName":"mosaic-cli-p0","baseRepository":"mosaicstack/stack","headRefName":"mosaic-cli-p2-socket-res","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}'
SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
@@ -21,7 +21,7 @@ cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' '{"baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"next","headRefName":"fix/b1-fork-branch","headRefOid":"fedcba9876543210fedcba9876543210fedcba98","headRepository":"stack-mos-dt-0/stack"}'
printf '%s\n' '{"baseRefOid":"'"$MOSAIC_TEST_BASE_SHA"'","baseRefName":"next","headRefName":"fix/b1-fork-branch","headRefOid":"fedcba9876543210fedcba9876543210fedcba98","headRepository":"stack-mos-dt-0/stack"}'
SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
@@ -33,6 +33,8 @@ chmod +x "$FIXTURE_DIR"/*.sh
# A git repo with an origin remote, so the origin fallback resolves.
git init -q "$WORK_DIR/upstream"
git -C "$WORK_DIR/upstream" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git -C "$WORK_DIR/upstream" rev-parse HEAD)"
git -C "$WORK_DIR/upstream" remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
set +e
@@ -93,7 +93,7 @@ if [[ "$args" == *"/api/v1/repos/mosaicstack/stack/commits/0123456789abcdef01234
exit 0
fi
if [[ "$args" == *"/api/v1/repos/mosaicstack/stack/pulls/123"* && "$args" != *"/api/v1/repos/mosaicstack/stack/pulls/123/merge"* ]]; then
emit_response '{"number":123,"title":"mock","state":"open","user":{"login":"tester"},"head":{"ref":"feature/mock","sha":"0123456789abcdef0123456789abcdef01234567","repo":{"full_name":"mosaicstack/stack"}},"base":{"ref":"main","sha":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"},"labels":[],"assignees":[],"html_url":"https://git.mosaicstack.dev/mosaicstack/stack/pulls/123","mergeable":true}'
emit_response "$(printf '%s' '{"number":123,"title":"mock","state":"open","user":{"login":"tester"},"head":{"ref":"feature/mock","sha":"0123456789abcdef0123456789abcdef01234567","repo":{"full_name":"mosaicstack/stack"}},"base":{"ref":"main","sha":"BASE_SHA_FIXTURE"},"labels":[],"assignees":[],"html_url":"https://git.mosaicstack.dev/mosaicstack/stack/pulls/123","mergeable":true}' | sed "s/BASE_SHA_FIXTURE/$MOSAIC_TEST_BASE_SHA/")"
exit 0
fi
if [[ "$args" == *"-X POST"* && "$args" == *"/api/v1/repos/mosaicstack/stack/pulls/123/merge"* ]]; then
@@ -116,6 +116,8 @@ chmod +x "$MOCK_BIN/curl"
cd "$REPO_DIR"
git init -q
git -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git rev-parse HEAD)"
git remote add origin https://git.mosaicstack.dev/mosaicstack/stack.git
# HERMETICITY (#1007) — TWO mechanisms with DIFFERENT jobs; do not conflate them.
#
@@ -17,11 +17,13 @@ make_fixture() {
cp "$SCRIPT_DIR/pr-merge.sh" "$tools/pr-merge.sh"
cp "$SCRIPT_DIR/detect-platform.sh" "$tools/detect-platform.sh"
git -C "$root/repo" init -q
git -C "$root/repo" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
local base_sha; base_sha=$(git -C "$root/repo" rev-parse HEAD)
git -C "$root/repo" remote add origin "$remote"
local base_branch="${3:-main}"
cat > "$tools/pr-metadata.sh" <<SH
#!/usr/bin/env bash
printf '%s\n' '{"baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"$base_branch","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}'
printf '%s\n' '{"baseRefOid":"$base_sha","baseRefName":"$base_branch","headRefName":"fix/pinned","headRefOid":"$SHA","headRepository":"contributor/widgets-fork"}'
SH
cat > "$tools/ci-queue-wait.sh" <<'SH'
#!/usr/bin/env bash
@@ -12,6 +12,9 @@ failures=0
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR"
git -C "$WORK_DIR" init -q
git -C "$WORK_DIR" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git -C "$WORK_DIR" rev-parse HEAD)"
fail() {
echo "FAIL $1" >&2
@@ -62,7 +65,7 @@ case "${MOSAIC_TEST_COMMITS_MODE:?}" in
single) head_sha=1111111111111111111111111111111111111111 ;;
*) echo "unknown commits mode" >&2; exit 2 ;;
esac
printf '{"number":42,"title":"%s","author":"poster","baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$head_sha"
printf '{"number":42,"title":"%s","author":"poster","baseRefOid":"%s","baseRefName":"main","headRefName":"feature/fixture","headRefOid":"%s","headRepository":"acme/widgets"}\n' "$title" "$MOSAIC_TEST_BASE_SHA" "$head_sha"
SH
cat > "$case_dir/ci-queue-wait.sh" <<'SH'
@@ -269,6 +272,7 @@ SH
run_case() {
local case_dir="$1" mode="$2"
shift 2
cd "$WORK_DIR" || return
MOSAIC_TEST_COMMITS_MODE="$mode" \
MOSAIC_TEST_CURL_LOG="$case_dir/curl.log" \
MOSAIC_TEST_CURL_BOUNDS_LOG="$case_dir/curl-bounds.log" \
@@ -14,12 +14,15 @@ CALL_LOG="$WORK_DIR/queue-call.log"
rm -rf "$WORK_DIR"
mkdir -p "$FIXTURE_DIR"
git -C "$WORK_DIR" init -q
git -C "$WORK_DIR" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git -C "$WORK_DIR" rev-parse HEAD)"
cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh"
cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' '{"baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"main","baseRepository":"mosaicstack/stack","headRefName":"fix/no-ci-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}'
printf '%s\n' '{"baseRefOid":"'"$MOSAIC_TEST_BASE_SHA"'","baseRefName":"main","baseRepository":"mosaicstack/stack","headRefName":"fix/no-ci-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"mosaicstack/stack"}'
SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'
@@ -10,12 +10,15 @@ CALL_LOG="$WORK_DIR/queue-call.log"
rm -rf "$WORK_DIR"
mkdir -p "$FIXTURE_DIR"
git -C "$WORK_DIR" init -q
git -C "$WORK_DIR" -c user.name=Fixture -c user.email=[email protected] commit --allow-empty -qm base
export MOSAIC_TEST_BASE_SHA="$(git -C "$WORK_DIR" rev-parse HEAD)"
cp "$SCRIPT_DIR/pr-merge.sh" "$FIXTURE_DIR/pr-merge.sh"
cp "$SCRIPT_DIR/detect-platform.sh" "$FIXTURE_DIR/detect-platform.sh"
cat > "$FIXTURE_DIR/pr-metadata.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' '{"baseRefOid":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","baseRefName":"main","baseRepository":"mosaicstack/stack","headRefName":"fix/rm-03-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"contributor/widgets-fork"}'
printf '%s\n' '{"baseRefOid":"'"$MOSAIC_TEST_BASE_SHA"'","baseRefName":"main","baseRepository":"mosaicstack/stack","headRefName":"fix/rm-03-fixture","headRefOid":"0123456789abcdef0123456789abcdef01234567","headRepository":"contributor/widgets-fork"}'
SH
cat > "$FIXTURE_DIR/ci-queue-wait.sh" <<'SH'