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
2 changes: 1 addition & 1 deletion .github/workflows/auto-release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ jobs:
if: needs.detect-version.outputs.should_release == 'true'
uses: ./.github/workflows/_build.yml
with:
runs-on: macos-14
runs-on: macos-15-intel
rust-target: x86_64-apple-darwin

build-macos-arm:
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/macos-x86-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,9 @@ jobs:
uses: ./.github/workflows/_build.yml
with:
source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
runs-on: macos-14
runs-on: macos-15-intel
# Explicit target so CI wheels/binaries are identical to the release
# builds in auto-release.yml (same artifact names, plat-name tags, and
# soldr cache key), letting auto-release reuse them (#170). Also fixes
# a latent bug: macos-14 runners are Apple Silicon, so the previous
# native build produced an arm64 binary under the "macOS x86" name.
# soldr cache key), letting auto-release reuse them (#170).
# The Intel host also executes x86 tests on the architecture we ship.
rust-target: x86_64-apple-darwin
2 changes: 1 addition & 1 deletion .github/workflows/macos-x86-integration-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
uses: ./.github/workflows/_integration-test.yml
with:
source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
runs-on: macos-14
runs-on: macos-15-intel
2 changes: 1 addition & 1 deletion .github/workflows/macos-x86-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
uses: ./.github/workflows/_lint.yml
with:
source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
runs-on: macos-14
runs-on: macos-15-intel
2 changes: 1 addition & 1 deletion .github/workflows/macos-x86-unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ jobs:
uses: ./.github/workflows/_unit-test.yml
with:
source-sha: ${{ github.event_name == 'workflow_dispatch' && inputs.candidate_sha || github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
runs-on: macos-14
runs-on: macos-15-intel
44 changes: 36 additions & 8 deletions ci/verify_full_coverage.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,14 @@
from pathlib import Path

MANIFEST = Path(__file__).with_name("full_coverage.json")
PLATFORMS = {"linux-x86", "linux-arm", "windows-x86", "windows-arm", "macos-x86", "macos-arm"}
PLATFORMS = {
"linux-x86",
"linux-arm",
"windows-x86",
"windows-arm",
"macos-x86",
"macos-arm",
}


def get_json(path):
Expand Down Expand Up @@ -48,12 +55,19 @@ def validate_report(report, manifest, sha):
return False
if any(not checks for checks in tests.values()):
return False
if any(not check["workflow"].startswith(platform + "-") for platform, checks in tests.items() for check in checks):
if any(
not check["workflow"].startswith(platform + "-")
for platform, checks in tests.items()
for check in checks
):
return False
cells = report.get("cells", {})
if set(cells) != set(names):
return False
if not all(cell.get("result") == "passed" and isinstance(cell.get("run_id"), int) for cell in cells.values()):
if not all(
cell.get("result") == "passed" and isinstance(cell.get("run_id"), int)
for cell in cells.values()
):
return False
if report.get("test_execution_gaps") != []:
return False
Expand All @@ -62,7 +76,13 @@ def validate_report(report, manifest, sha):
if check["workflow"] not in cells:
return False
jobs = cells[check["workflow"]].get("jobs", [])
if not any(step.get("name") == check["step"] and step.get("conclusion") == "success" for job in jobs if job.get("conclusion") == "success" for step in job.get("steps", [])):
if not any(
step.get("name") == check["step"]
and step.get("conclusion") == "success"
for job in jobs
if job.get("conclusion") == "success"
for step in job.get("steps", [])
):
return False
return True

Expand All @@ -77,18 +97,26 @@ def main():
if manifest.get("schema_version") != 2:
parser.error("unsupported coverage manifest")
repo = os.environ["GITHUB_REPOSITORY"]
runs = get_json(f"repos/{repo}/actions/workflows/full-coverage.yml/runs?head_sha={args.sha}&event=workflow_dispatch&per_page=100")["workflow_runs"]
runs = get_json(
f"repos/{repo}/actions/workflows/full-coverage.yml/runs?head_sha={args.sha}&event=workflow_dispatch&per_page=100"
)["workflow_runs"]
for run in runs:
if run["head_sha"] != args.sha or run["conclusion"] != "success":
continue
artifacts = get_json(f"repos/{repo}/actions/runs/{run['id']}/artifacts?per_page=100")["artifacts"]
artifacts = get_json(
f"repos/{repo}/actions/runs/{run['id']}/artifacts?per_page=100"
)["artifacts"]
for artifact in artifacts:
if artifact["name"] != f"full-coverage-{run['id']}" or artifact["expired"]:
continue
with zipfile.ZipFile(io.BytesIO(download(artifact["archive_download_url"]))) as archive:
with zipfile.ZipFile(
io.BytesIO(download(artifact["archive_download_url"]))
) as archive:
report = json.loads(archive.read("full-coverage.json"))
if validate_report(report, manifest, args.sha):
print(f"Verified all {len(manifest['workflows'])} platform workflows: {run['html_url']}")
print(
f"Verified all {len(manifest['workflows'])} platform workflows: {run['html_url']}"
)
return 0
print("No passing exact-SHA release full-coverage report found", file=sys.stderr)
return 1
Expand Down
39 changes: 36 additions & 3 deletions ci/vscode_release_artifact_lint.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Validate the complete VS Code release payload before creating its tag."""

import argparse
import hashlib
import json
import zipfile
from pathlib import Path
Expand All @@ -16,28 +17,60 @@
"universal",
}
)
NATIVE_TARGETS = TARGETS - {"universal"}


def check(artifacts: Path, version: str) -> list[Path]:
expected = {f"fastled-wasm-{version}-{target}.vsix" for target in TARGETS}
expected_packages = {f"fastled-wasm-{version}-{target}.vsix" for target in TARGETS}
expected_sidecars = {
f"{target}.{suffix}"
for target in NATIVE_TARGETS
for suffix in ("manifest.json", "sha256", "size")
}
expected = expected_packages | expected_sidecars
found = {path.name for path in artifacts.iterdir() if path.is_file()}
if found != expected:
raise ValueError(
f"expected exact seven versioned VSIX files: {sorted(expected)}; got {sorted(found)}"
f"expected seven VSIX packages and their native sidecars: {sorted(expected)}; got {sorted(found)}"
)
packages = sorted(artifacts / name for name in expected)
packages = sorted(artifacts / name for name in expected_packages)
for package in packages:
if package.stat().st_size == 0:
raise ValueError(f"empty VSIX: {package.name}")
embedded_manifest: bytes | None = None
try:
with zipfile.ZipFile(package) as archive:
if archive.testzip() is not None:
raise ValueError(f"corrupt VSIX: {package.name}")
manifest = json.loads(archive.read("extension/package.json"))
target = next(
target
for target in TARGETS
if package.name.endswith(f"-{target}.vsix")
)
if target in NATIVE_TARGETS:
embedded_manifest = archive.read(
"extension/resources/clangd/manifest.json"
)
except (zipfile.BadZipFile, KeyError, json.JSONDecodeError) as error:
raise ValueError(f"invalid VSIX: {package.name}: {error}") from error
if manifest.get("name") != "fastled-wasm" or manifest.get("version") != version:
raise ValueError(f"VSIX manifest mismatch: {package.name}")
if target in NATIVE_TARGETS:
if embedded_manifest is None:
raise ValueError(f"missing native manifest: {package.name}")
expected_hash = (
f"{hashlib.sha256(package.read_bytes()).hexdigest()} {package.name}\n"
)
expected_size = f"{package.stat().st_size}\n"
if (
artifacts / f"{target}.manifest.json"
).read_bytes() != embedded_manifest:
raise ValueError(f"native manifest sidecar mismatch: {package.name}")
if (artifacts / f"{target}.sha256").read_text() != expected_hash:
raise ValueError(f"SHA-256 sidecar mismatch: {package.name}")
if (artifacts / f"{target}.size").read_text() != expected_size:
raise ValueError(f"size sidecar mismatch: {package.name}")
return packages


Expand Down
63 changes: 57 additions & 6 deletions tests/unit/test_ci_modes.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Guard the event-to-CI-tier wiring across every platform workflow."""

import hashlib
import importlib.util
import json
import zipfile
Expand Down Expand Up @@ -95,16 +96,31 @@ def test_vscode_release_artifact_lint_rejects_missing_corrupt_and_wrong_version(
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
targets = {
"win32-x64", "win32-arm64", "linux-x64", "linux-arm64",
"darwin-x64", "darwin-arm64", "universal",
"win32-x64",
"win32-arm64",
"linux-x64",
"linux-arm64",
"darwin-x64",
"darwin-arm64",
"universal",
}
for target in targets:
with zipfile.ZipFile(
tmp_path / f"fastled-wasm-1.0.1-{target}.vsix", "w"
) as archive:
package = tmp_path / f"fastled-wasm-1.0.1-{target}.vsix"
with zipfile.ZipFile(package, "w") as archive:
archive.writestr(
"extension/package.json", json.dumps({"name": "fastled-wasm", "version": "1.0.1"})
"extension/package.json",
json.dumps({"name": "fastled-wasm", "version": "1.0.1"}),
)
if target != "universal":
archive.writestr(
"extension/resources/clangd/manifest.json", b"native manifest"
)
if target != "universal":
(tmp_path / f"{target}.manifest.json").write_bytes(b"native manifest")
(tmp_path / f"{target}.sha256").write_text(
f"{hashlib.sha256(package.read_bytes()).hexdigest()} {package.name}\n"
)
(tmp_path / f"{target}.size").write_text(f"{package.stat().st_size}\n")
assert len(module.check(tmp_path, "1.0.1")) == 7
with pytest.raises(ValueError):
module.check(tmp_path, "1.0.2")
Expand All @@ -113,6 +129,41 @@ def test_vscode_release_artifact_lint_rejects_missing_corrupt_and_wrong_version(
module.check(tmp_path, "1.0.1")


def test_vscode_release_artifact_lint_accepts_native_sidecars_only(tmp_path):
spec = importlib.util.spec_from_file_location(
"vscode_release_artifact_lint", ROOT / "ci" / "vscode_release_artifact_lint.py"
)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
version = "1.0.1"
for target in module.TARGETS:
package = tmp_path / f"fastled-wasm-{version}-{target}.vsix"
with zipfile.ZipFile(package, "w") as archive:
archive.writestr(
"extension/package.json",
json.dumps({"name": "fastled-wasm", "version": version}),
)
if target != "universal":
archive.writestr(
"extension/resources/clangd/manifest.json", b"native manifest"
)
if target != "universal":
(tmp_path / f"{target}.manifest.json").write_bytes(b"native manifest")
(tmp_path / f"{target}.sha256").write_text(
f"{hashlib.sha256(package.read_bytes()).hexdigest()} {package.name}\n"
)
(tmp_path / f"{target}.size").write_text(f"{package.stat().st_size}\n")
assert len(module.check(tmp_path, version)) == 7
(tmp_path / "unexpected.txt").write_text("extra")
with pytest.raises(ValueError, match="unexpected"):
module.check(tmp_path, version)
(tmp_path / "unexpected.txt").unlink()
(tmp_path / "linux-x64.sha256").write_text("incorrect digest\n")
with pytest.raises(ValueError, match="SHA-256 sidecar mismatch"):
module.check(tmp_path, version)


def test_release_does_not_wait_for_removed_routine_build_artifacts():
workflow = (WORKFLOWS / "auto-release.yml").read_text()
assert "collect-artifacts:" not in workflow
Expand Down
5 changes: 4 additions & 1 deletion tests/unit/test_kernal_boundary.py
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,10 @@ def test_kernal_api_is_the_only_rust_dependency():
# A `kernal-api/<feature>` entry in [features] would reach the
# build-dependency too, so runtime features are named on the dependency.
for name, enables in package["features"].items():
assert not any(entry.startswith("kernal-api/") for entry in enables), (name, enables)
assert not any(entry.startswith("kernal-api/") for entry in enables), (
name,
enables,
)
build_rs = (root / "crates/fastled-cli/build.rs").read_text()
# Leading path segments only: `kernal_api::build_resources::...` names
# the crate `kernal_api`, not a crate called `build_resources`.
Expand Down
Loading