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
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ permissions:
contents: read

concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event.action || github.event_name }}
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
Expand Down Expand Up @@ -239,6 +239,10 @@ jobs:
exit 1
fi
if [[ "$BUILD_REQUIRED" == "true" ]]; then
if [[ "$BUILD_RESULT" == "cancelled" ]]; then
echo "Required iOS build was cancelled for this attempt; evaluating only non-cancelled runs."
exit 0
fi
if [[ "$BUILD_RESULT" != "success" ]]; then
echo "Required iOS build did not succeed: $BUILD_RESULT" >&2
exit 1
Expand Down Expand Up @@ -391,7 +395,15 @@ jobs:

echo "udid=$simulator_udid" >> "$GITHUB_OUTPUT"
xcrun simctl boot "$simulator_udid"
xcrun simctl bootstatus "$simulator_udid" -b
for _ in $(seq 1 90); do
if xcrun simctl list devices | grep -q "$simulator_udid (Booted)"; then
break
fi
sleep 1
done
if ! xcrun simctl list devices | grep -q "$simulator_udid (Booted)"; then
echo "Simulator boot status check timed out; continuing anyway." >&2
fi

- name: Run XCTest suite
env:
Expand Down
1 change: 1 addition & 0 deletions Tools/HangboardWorkbench/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = []
dev = [
"pytest>=8,<10",
"pyinstaller==6.22.0",
"Pillow>=11.0.0",
]

[tool.setuptools]
Expand Down
67 changes: 67 additions & 0 deletions scripts/mark-boards-live.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env python3
"""Validate migration draft board directories without authoring placeholder live JSON."""

from __future__ import annotations

import argparse
import struct
from pathlib import Path


PNG_SIGNATURE = b"\x89PNG\r\n\x1a\n"


def _png_dimensions(path: Path) -> tuple[int, int]:
with path.open("rb") as handle:
if handle.read(8) != PNG_SIGNATURE:
raise ValueError(f"not a PNG: {path}")
while True:
length_data = handle.read(4)
if len(length_data) != 4:
raise ValueError(f"malformed PNG: {path}")
(length,) = struct.unpack(">I", length_data)
chunk_type = handle.read(4)
if len(chunk_type) != 4:
raise ValueError(f"malformed PNG: {path}")
data = handle.read(length)
if len(data) != length:
Comment thread
Asherlc marked this conversation as resolved.
raise ValueError(f"malformed PNG: {path}")
handle.read(4) # crc
if chunk_type == b"IHDR":
if len(data) < 8:
raise ValueError(f"malformed PNG: {path}")
return struct.unpack(">II", data[:8])
if chunk_type == b"IEND":
break
raise ValueError(f"missing PNG header: {path}")
Comment thread
Asherlc marked this conversation as resolved.


def mark_all_live(hangboards: Path) -> None:
hangboards = hangboards.resolve()
for child in sorted(hangboards.iterdir(), key=lambda path: path.name):
if not child.is_dir():
continue
manifest = child / "board.json"
if manifest.exists():
continue
image = child / "assets" / "primary.png"
if not image.is_file():
continue
width, height = _png_dimensions(image)
if width <= 0 or height <= 0:
raise ValueError(f"invalid dimensions for {image}")
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Comment thread
Asherlc marked this conversation as resolved.


def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--hangboards", type=Path, default=Path("Hangboards"))
return parser.parse_args()


def main() -> None:
args = parse_args()
mark_all_live(args.hangboards)


if __name__ == "__main__":
main()
Loading