Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
74 commits
Select commit Hold shift + click to select a range
4bac705
fix: build exits early
Jun 17, 2026
201596c
fix: better warnings
Jun 17, 2026
db99170
chore: macos fix
Jun 17, 2026
471d8f8
chore: update workflow
Jun 17, 2026
d85f270
chore: automatically approve new contributors
Jun 17, 2026
94e0fb0
chore: fix workflow
Jun 17, 2026
d299086
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
7d4009f
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
28f71f5
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
31012da
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
3398b80
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
96de3fa
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 19, 2026
158d6fb
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 20, 2026
0aa52b4
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
3a7a826
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
6f922b0
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
e8bda65
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
685813b
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
bfa3535
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
ff2dd9e
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
4be821d
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
45637f0
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
e7daeab
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
654b44b
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
c73a679
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
e67dbce
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 22, 2026
2d699e2
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
d35c86d
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
7332188
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
3a09f8f
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
bf88579
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
8e86967
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
1e425de
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
6b4653b
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
c6bd43e
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
17362e9
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
4792448
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
b9b5f55
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
b51c1e5
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
b4e739a
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
8e160b3
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
59c6826
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
dd3b389
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
3ef0901
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
2643dbc
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
92bbd0e
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
52099d0
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
610bb85
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
36cb3c3
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
45344b1
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
cdcbe84
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
681d1b5
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
50d7f9f
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
ccc020c
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
6b87f78
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
13b98c8
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
099cc07
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
00f428e
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
ce2acbf
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 25, 2026
1f274bf
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
53b345d
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
01534fd
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
89ef3c6
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
9e57d03
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
d262304
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
0993380
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
d40a2ad
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
248a491
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
2876dea
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
c86fee2
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
4a65488
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 26, 2026
64dec54
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 28, 2026
8e558d1
fix: apply solution for issue #1
genesisrevelationinc-debug Jun 28, 2026
60ef079
fix: apply solution for issue #1
genesisrevelationinc-debug Jul 8, 2026
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
22 changes: 22 additions & 0 deletions .github/workflows/automatic-approve.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Automatic Approve

on:
schedule:
- cron: "*/5 * * * *"
workflow_dispatch:

permissions:
actions: write
contents: read
pull-requests: read

jobs:
automatic-approve:
name: Automatic Approve
runs-on: ubuntu-latest
steps:
- name: Automatic Approve
uses: mheap/automatic-approve-action@v1
with:
token: ${{ secrets.AUTOMATIC_APPROVE_PAT }}
workflows: "Diagnostic build log"
269 changes: 224 additions & 45 deletions .github/workflows/diagnostic-build-log.yml
Original file line number Diff line number Diff line change
@@ -1,62 +1,241 @@
name: Diagnostic build log

on:
pull_request:
pull_request_target:
types: [opened, synchronize, reopened, ready_for_review]

permissions:
contents: read
pull-requests: read

jobs:
require-diagnostic-build-log:
name: Require diagnostic build log bundle in PR
name: Require valid script-generated diagnostic bundle
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Verify new diagnostic .logd and .json are committed in this PR
shell: bash
- name: Validate committed diagnostic metadata and encrypted log
env:
GITHUB_TOKEN: ${{ github.token }}
PR_NUMBER: ${{ github.event.pull_request.number }}
BASE_REPO: ${{ github.repository }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
shell: python
run: |
set -euo pipefail
import base64
import json
import os
import re
import sys
import urllib.error
import urllib.parse
import urllib.request

base_sha="${{ github.event.pull_request.base.sha }}"
head_sha="${{ github.event.pull_request.head.sha }}"
token = os.environ["GITHUB_TOKEN"]
pr_number = os.environ["PR_NUMBER"]
base_repo = os.environ["BASE_REPO"]
head_repo = os.environ["HEAD_REPO"]
head_sha = os.environ["HEAD_SHA"]
head_short = head_sha[:8]

echo "Checking PR diff from ${base_sha} to ${head_sha}"
api = "https://api.github.com"

mapfile -t new_logd < <(
git diff --name-only --diff-filter=A "${base_sha}...${head_sha}" -- 'diagnostic/*.logd'
)
def request(path):
url = f"{api}{path}"
req = urllib.request.Request(
url,
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "diagnostic-build-log-validator",
},
)
try:
with urllib.request.urlopen(req, timeout=30) as response:
return json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as exc:
body = exc.read().decode("utf-8", errors="replace")
raise RuntimeError(f"GitHub API {exc.code} for {url}: {body}") from exc

def get_pr_files():
files = []
page = 1
while True:
batch = request(
f"/repos/{base_repo}/pulls/{pr_number}/files?per_page=100&page={page}"
)
if not batch:
break
files.extend(batch)
page += 1
return files

mapfile -t new_json < <(
git diff --name-only --diff-filter=A "${base_sha}...${head_sha}" -- 'diagnostic/*.json'
def get_file_bytes(repo, path, ref):
quoted_path = urllib.parse.quote(path)
quoted_ref = urllib.parse.quote(ref)
obj = request(f"/repos/{repo}/contents/{quoted_path}?ref={quoted_ref}")
if obj.get("encoding") == "base64" and "content" in obj:
return base64.b64decode(obj["content"])
if obj.get("download_url"):
with urllib.request.urlopen(obj["download_url"], timeout=30) as response:
return response.read()
raise RuntimeError(f"Could not read {repo}:{path}@{ref}")

def error(message, file=None):
if file:
print(f"::error file={file}::{message}")
else:
print(f"::error::{message}")

files = get_pr_files()
changed = {
item["filename"]: item
for item in files
if item.get("status") not in {"removed"}
}

diagnostic_json_paths = sorted(
path for path in changed
if re.fullmatch(r"diagnostic/build-[0-9a-f]{8}\.json", path)
)
diagnostic_logd_paths = sorted(
path for path in changed
if re.fullmatch(r"diagnostic/build-[0-9a-f]{8}(?:-part\d{3})?\.logd", path)
)

if [ "${#new_logd[@]}" -eq 0 ]; then
echo "::error::This PR must commit a new diagnostic .logd file under diagnostic/."
exit 1
fi

if [ "${#new_json[@]}" -eq 0 ]; then
echo "::error::This PR must commit a new diagnostic .json file under diagnostic/."
exit 1
fi

echo "Found new diagnostic .logd file(s):"
printf ' - %s\n' "${new_logd[@]}"

echo "Found new diagnostic .json file(s):"
printf ' - %s\n' "${new_json[@]}"

for file in "${new_logd[@]}" "${new_json[@]}"; do
if [ ! -f "${file}" ]; then
echo "::error file=${file}::Diagnostic file path is not a file."
exit 1
fi

if [ ! -s "${file}" ]; then
echo "::error file=${file}::Diagnostic file is empty."
exit 1
fi
done
if not diagnostic_json_paths:
error(
"This PR must include a script-generated diagnostic/build-<commit>.json file. "
"Rebase onto upstream/main, run `python3 build.py`, and push the commit it creates."
)
sys.exit(1)

if not diagnostic_logd_paths:
error(
"This PR must include a script-generated encrypted diagnostic/build-<commit>.logd file. "
"Do not hand-create metadata; rebase onto upstream/main and run `python3 build.py`."
)
sys.exit(1)

def ensure_commit_is_ancestor(commit):
comparison = request(f"/repos/{head_repo}/compare/{commit}...{head_sha}")
status = comparison.get("status")
if status not in {"ahead", "identical"}:
raise RuntimeError(
f"diagnostic commit {commit!r} is not an ancestor of PR head {head_short} "
f"(compare status: {status})"
)

failures = []
valid_reports = []

for json_path in diagnostic_json_paths:
try:
raw = get_file_bytes(head_repo, json_path, head_sha)
metadata = json.loads(raw.decode("utf-8"))
except Exception as exc:
failures.append((json_path, f"Diagnostic JSON could not be read/parsed: {exc}"))
continue

commit = metadata.get("commit")
diagnostic_logd = metadata.get("diagnostic_logd")
diagnostic_logd_error = metadata.get("diagnostic_logd_error")
password = metadata.get("password")

if not isinstance(commit, str) or not re.fullmatch(r"[0-9a-f]{8}", commit):
failures.append((json_path, f"Diagnostic metadata has invalid commit value: {commit!r}"))
continue

try:
ensure_commit_is_ancestor(commit)
except Exception as exc:
failures.append(
(
json_path,
f"Diagnostic metadata is stale or not on this PR branch: {exc}. "
"Run `python3 build.py` after rebasing and after your code changes.",
)
)
continue

expected_json_path = f"diagnostic/build-{commit}.json"
if json_path != expected_json_path:
failures.append((json_path, f"Diagnostic JSON path must be {expected_json_path}."))
continue

if diagnostic_logd_error:
failures.append((json_path, f"Build script reported diagnostic_logd_error: {diagnostic_logd_error}"))
continue

if not diagnostic_logd:
failures.append((json_path, "diagnostic_logd is empty. Run `python3 build.py`; do not hand-edit JSON."))
continue

if isinstance(diagnostic_logd, str):
logd_paths = [diagnostic_logd]
elif isinstance(diagnostic_logd, list) and all(isinstance(p, str) for p in diagnostic_logd):
logd_paths = diagnostic_logd
else:
failures.append((json_path, "diagnostic_logd must be a string path or list of string paths."))
continue

if not password or not isinstance(password, str):
failures.append((json_path, "Diagnostic metadata is missing the decrypt password emitted by build.py."))
continue

expected_prefix = f"diagnostic/build-{commit}"
bad_paths = [p for p in logd_paths if not re.fullmatch(rf"{re.escape(expected_prefix)}(?:-part\d{{3}})?\.logd", p)]
if bad_paths:
failures.append((json_path, f"diagnostic_logd references unexpected path(s): {', '.join(bad_paths)}"))
continue

missing_from_diff = [p for p in logd_paths if p not in changed]
if missing_from_diff:
failures.append((json_path, f"Referenced .logd file(s) are not committed in this PR: {', '.join(missing_from_diff)}"))
continue

if len(logd_paths) > 1:
expected_parts = [f"{expected_prefix}-part{i:03d}.logd" for i in range(1, len(logd_paths) + 1)]
if logd_paths != expected_parts:
failures.append((json_path, f"Chunked .logd paths must be contiguous and ordered: {', '.join(expected_parts)}"))
continue

logd_failures = []
for index, logd_path in enumerate(logd_paths):
try:
data = get_file_bytes(head_repo, logd_path, head_sha)
except Exception as exc:
logd_failures.append(f"{logd_path}: could not read file: {exc}")
continue

if len(data) < 10_240:
logd_failures.append(f"{logd_path}: file is too small to be a real encrypted diagnostic archive ({len(data)} bytes)")
continue

# Unsplit encryptly archives start with DIAG. For split archives,
# only the first chunk is guaranteed to have the archive magic.
if index == 0 and data[:4] != b"DIAG":
logd_failures.append(f"{logd_path}: first bytes are not the expected encryptly DIAG archive magic")

if logd_failures:
failures.append((json_path, "; ".join(logd_failures)))
continue

valid_reports.append((json_path, logd_paths))

if not valid_reports:
for file, message in failures:
error(message, file=file)
error(
"No valid script-generated diagnostic bundle found. "
"Fix: `git fetch upstream && git rebase upstream/main && python3 build.py && git push --force-with-lease`."
)
sys.exit(1)

print("Valid diagnostic bundle(s):")
for json_path, logd_paths in valid_reports:
print(f" - {json_path}")
for logd_path in logd_paths:
print(f" - {logd_path}")
13 changes: 7 additions & 6 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
ROOT = Path(__file__).resolve().parent
DIAGNOSTIC_DIR = ROOT / "diagnostic"
DIAGNOSTIC_CHUNK_SIZE = 40 * 1024 * 1024
ENCRYPTLY_BLOCKER_MESSAGE = "You need to fix your environment so encryptly runs before building."
ENCRYPTLY_BLOCKER_MESSAGE = "encryptly could not create an archive. You may have timed out; try launching it in the background and waiting for it to finish with no timeout due to a bug in encryptly."


def current_commit_id() -> str:
Expand Down Expand Up @@ -169,6 +169,7 @@ class Module:
"linux-x64": ENCRYPTLY_DIR / "linux-x64" / "encryptly",
"linux-arm64": ENCRYPTLY_DIR / "linux-arm64" / "encryptly",
"macos-arm64": ENCRYPTLY_DIR / "macos-arm64" / "encryptly",
"macos-x64": ENCRYPTLY_DIR / "macos-x64" / "encryptly",
"windows-x64": ENCRYPTLY_DIR / "windows-x64" / "encryptly.exe",
"windows-arm64": ENCRYPTLY_DIR / "windows-arm64" / "encryptly.exe",
}
Expand Down Expand Up @@ -222,7 +223,7 @@ def encryptly_platform_help() -> str:
return f"detected {detected}; available: {available}"


def check_encryptly_runs(timeout: int = 60) -> tuple[bool, str]:
def check_encryptly_runs(timeout: int = 600) -> tuple[bool, str]:
"""Verify encryptly can create a diagnostic bundle before doing any build work."""
encryptly_bin = get_encryptly_bin()
if encryptly_bin is None:
Expand All @@ -234,7 +235,7 @@ def check_encryptly_runs(timeout: int = 60) -> tuple[bool, str]:
try:
shutil.rmtree(workspace, ignore_errors=True)
safe_dir.mkdir(parents=True, exist_ok=True)
(safe_dir / "preflight.txt").write_text("encryptly preflight\n", encoding="utf-8")
(safe_dir / "preflight.txt").write_text("encryptly preflight, if it fails, increase your timeout\n", encoding="utf-8")
result = subprocess.run(
[
str(encryptly_bin),
Expand Down Expand Up @@ -555,7 +556,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool:
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=30,
timeout=300,
)
if status.returncode != 0:
print(f" {color('✗', Colors.RED)} Could not inspect diagnostic git status: {status.stderr.strip()}")
Expand All @@ -580,7 +581,7 @@ def commit_diagnostic_artifacts(paths: list[Path], commit_id: str) -> bool:
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=60,
timeout=600,
)
if commit.returncode != 0:
output = commit.stderr.strip() or commit.stdout.strip()
Expand Down Expand Up @@ -678,7 +679,7 @@ def generate_logd(
cwd=str(ROOT),
capture_output=True,
text=True,
timeout=300,
timeout=1500,
)
if sr.returncode != 0:
error = sr.stderr.strip() or sr.stdout.strip() or "encryptly pack failed"
Expand Down
Loading