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
32 changes: 21 additions & 11 deletions deploy/publish_public_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,14 +61,17 @@ def prepare(root, candidate, gate_only=False):
selected += [source / ".github/workflows/publication-security.yml"]
else:
selected = []
for file in source.rglob("*"):
rel = file.relative_to(source)
if (file.is_file() and not file.is_symlink()
and not any(part in EXCLUDED for part in rel.parts)
and file.suffix in EXTENSIONS
and not path_problem(rel.as_posix())
and rel.name not in ("README.md", "README-public.md")):
selected.append(file)
for directory, dirs, files in os.walk(source, followlinks=False):
dirs[:] = [d for d in dirs if d not in EXCLUDED
and not (Path(directory) / d).is_symlink()]
for name in files:
file = Path(directory) / name
rel = file.relative_to(source)
if (file.is_file() and not file.is_symlink()
and file.suffix in EXTENSIONS
and not path_problem(rel.as_posix())
and rel.name not in ("README.md", "README-public.md")):
selected.append(file)
for file in selected:
if not file.is_file() or file.is_symlink():
raise RuntimeError("Missing or unsafe required publication source")
Expand Down Expand Up @@ -181,7 +184,8 @@ def await_merge(pr, request, timeout=900):
raise RuntimeError("Publication PR closed without merging")
sha = pull["head"]["sha"]
checks = request("GET", f"{API}/commits/{sha}/check-runs?per_page=100")["check_runs"]
trusted = {c["name"]: c for c in checks if c.get("app", {}).get("id") == 15368}
trusted = {c["name"]: c for c in sorted(checks, key=lambda c: c.get("id", 0))
if c.get("app", {}).get("id") == 15368}
for name in required:
check = trusted.get(name)
if check and check["status"] == "completed" and check["conclusion"] != "success":
Expand All @@ -207,17 +211,23 @@ def main():
parser.add_argument("--gate-only", action="store_true")
parser.add_argument("--with-tests", action="store_true")
parser.add_argument("--no-wait", action="store_true")
parser.add_argument("--check-drift", action="store_true")
args = parser.parse_args()
token = os.environ.get("PUBLIC_GITHUB_TOKEN")
if not args.dry_run and not token:
if not args.dry_run and not args.check_drift and not token:
raise SystemExit("PUBLIC_GITHUB_TOKEN must be provided through the environment")
with tempfile.TemporaryDirectory(prefix="aperod-publication-") as temp:
candidate = Path(temp) / "repo"
run("git", "clone", "--depth", "1", "-q",
"https://github.com/aperod-network/aperod-node", str(candidate))
head = git(candidate, "rev-parse", "HEAD").decode().strip()
tree = prepare(args.root.resolve(), candidate, args.gate_only)
if args.dry_run:
if args.check_drift:
changed = [x for x in git(candidate, "diff", "--cached", "--name-only", "-z").split(b"\0") if x]
print(f"Complete source-distribution drift: {len(changed)} files.")
if changed:
raise SystemExit(1)
elif args.dry_run:
verify(candidate, tree, with_tests=args.with_tests)
print("Dry-run verified; no objects uploaded.")
else:
Expand Down
43 changes: 43 additions & 0 deletions deploy/test_publish_public_repo.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,49 @@ def fixture(root):


class PublisherTests(unittest.TestCase):
def test_failed_required_check_never_merges(self):
methods = []
def request(method, url, data=None):
methods.append(method)
if url.endswith("/pulls/1"):
return {"state": "open", "head": {"sha": "reviewed"}}
return {"check_runs": [{"name": "Whole-tree publication security",
"status": "completed", "conclusion": "failure",
"app": {"id": 15368}}]}
with self.assertRaisesRegex(RuntimeError, "check failed"):
publisher.await_merge({"number": 1, "html_url": "test"}, request)
self.assertNotIn("PUT", methods)

def test_untrusted_status_provider_cannot_authorize_merge(self):
methods = []
def request(method, url, data=None):
methods.append(method)
if url.endswith("/pulls/1"):
return {"state": "open", "head": {"sha": "reviewed"}}
return {"check_runs": [{"name": name, "status": "completed",
"conclusion": "success", "app": {"id": 999}}
for name in ("Whole-tree publication security", "go build & vet & test")]}
with patch.object(publisher.time, "monotonic", side_effect=[0, 0, 2]), \
patch.object(publisher.time, "sleep"):
with self.assertRaisesRegex(RuntimeError, "did not complete"):
publisher.await_merge({"number": 1, "html_url": "test"}, request, timeout=1)
self.assertNotIn("PUT", methods)

def test_merge_is_bound_to_reviewed_head(self):
bodies = []
def request(method, url, data=None):
if method == "PUT":
bodies.append(data)
return {"merged": True}
if url.endswith("/pulls/1"):
return {"state": "open", "head": {"sha": "reviewed"}}
return {"check_runs": [{"name": name, "status": "completed",
"conclusion": "success", "app": {"id": 15368}}
for name in ("Whole-tree publication security", "go build & vet & test")]}
publisher.await_merge({"number": 1, "html_url": "test"}, request)
self.assertEqual(bodies[0]["sha"], "reviewed")
self.assertNotIn("force", bodies[0])

def test_policy_failure_prevents_every_network_request(self):
with tempfile.TemporaryDirectory() as temp:
root = Path(temp)
Expand Down
Loading