Skip to content

Commit 290d23f

Browse files
authored
Merge pull request #37 from PyDevices/branch/mip-sync-lockfile-descriptions-c94d
MIP sync: lockfile owns the source repo; share pydevices descriptions
2 parents e75671b + fdc843b commit 290d23f

6 files changed

Lines changed: 415 additions & 23 deletions

.github/workflows/checks.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# This repository's own CI: lint the workflows it publishes to the rest of
22
# the org, smoke-test that every reusable workflow is valid YAML, prove the
33
# site generator's output matches what data/repos_db.json currently says it
4-
# should be, and lint scripts/.
4+
# should be, lint scripts/, and run publishing-script unit tests.
55
name: Checks
66

77
on:
@@ -110,4 +110,16 @@ jobs:
110110

111111
- run: python3 -m pip install --disable-pip-version-check ruff
112112

113-
- run: ruff check scripts/
113+
- run: ruff check scripts/ tests/
114+
115+
unittest:
116+
runs-on: ubuntu-latest
117+
steps:
118+
- uses: actions/checkout@v7
119+
120+
- uses: actions/setup-python@v7
121+
with:
122+
python-version: "3.12"
123+
124+
- name: Run publishing-script unit tests
125+
run: python3 -m unittest discover -s tests -v

.github/workflows/reusable-synchronize-mip-package.yml

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -90,10 +90,15 @@ jobs:
9090
profile = os.environ["PUBLICATION_PROFILE"]
9191
if profile not in lock:
9292
raise SystemExit(f"{profile!r} is not in {path}; add it before publishing")
93-
lock[profile] = {
94-
"repository": os.environ["SOURCE_REPOSITORY"],
95-
"ref": os.environ["SOURCE_REF"],
96-
}
93+
entry = lock[profile]
94+
expected = entry["repository"]
95+
got = os.environ["SOURCE_REPOSITORY"]
96+
if expected != got:
97+
raise SystemExit(
98+
f"profile {profile!r} is locked to {expected}, not {got}; "
99+
f"edit pydevices-lock.json on the PyDevices branch to move the profile"
100+
)
101+
entry["ref"] = os.environ["SOURCE_REF"]
97102
path.write_text(json.dumps(lock, indent=2) + "\n")
98103
print(f"{profile} -> {lock[profile]['ref']}")
99104
PY

scripts/build_pydevices_python_distributions.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
import sys
1010
from pathlib import Path
1111

12+
from pydevices_package_metadata import PYDEVICES_DESCRIPTIONS
13+
1214
DEBRIS = {"__pycache__", "README.md", "build", "dist"}
1315
# No internal dependency table here: with lib/ shipped as one distribution the
1416
# graph between its components is internal imports, not package requirements.
@@ -114,7 +116,7 @@ def build(root: Path, output: Path, version: str) -> None:
114116
meta,
115117
"pydevices",
116118
version,
117-
"Portable display, audio, event, and timing foundations for PyDevices",
119+
PYDEVICES_DESCRIPTIONS["pydevices"],
118120
[],
119121
)
120122
stages.append(meta)
@@ -129,7 +131,7 @@ def build(root: Path, output: Path, version: str) -> None:
129131
desktop,
130132
"pydevices-desktop",
131133
version,
132-
"Complete PyDevices desktop stack and board configuration",
134+
PYDEVICES_DESCRIPTIONS["pydevices-desktop"],
133135
[f"pydevices=={version}"],
134136
)
135137
stages.append(desktop)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Descriptions shared by TestPyPI and MIP publications of pydevices packages."""
2+
3+
PYDEVICES_DESCRIPTIONS = {
4+
"pydevices": "Portable display, audio, event, and timing foundations for PyDevices",
5+
"pydevices-desktop": "Complete PyDevices desktop stack and board configuration",
6+
}

scripts/synchronize_mip_package.py

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@
44
from __future__ import annotations
55

66
import argparse
7+
import json
78
import shutil
89
from dataclasses import dataclass
910
from pathlib import Path
1011

12+
from pydevices_package_metadata import PYDEVICES_DESCRIPTIONS
13+
1114

1215
@dataclass(frozen=True)
1316
class Profile:
@@ -76,16 +79,12 @@ class Profile:
7679
),
7780
}
7881

79-
PROFILE_REPOSITORIES = {
80-
"palettes": "PyDevices/palettes",
81-
"pdwidgets": "PyDevices/pdwidgets",
82-
"pygraphics": "PyDevices/pygraphics",
83-
"pydevices": "PyDevices/pydevices",
84-
# Moved from PyDevices/audioif in the audioif/audiocomponents split
85-
# (audiocomponents#2, 2026-09-03); audioif publishes the core only now.
86-
"audioinstruments": "PyDevices/audiocomponents",
87-
"audioeffects": "PyDevices/audiocomponents",
88-
}
82+
# The source repository for each profile is pydevices-lock.json in the MIP
83+
# checkout, not a second map in this script. reusable-synchronize-mip-package.yml
84+
# already keeps that lockfile on the runner; a hardcoded PROFILE_REPOSITORIES
85+
# table disagreed with it after the audioif/audiocomponents split and blocked
86+
# every publication until a new publishing-tools tag (#35).
87+
LOCKFILE_NAME = "pydevices-lock.json"
8988

9089
# No internal dependency table: lib/ ships as a single MIP package, so the graph
9190
# between its components is imports rather than package requirements. It was
@@ -98,6 +97,26 @@ class Profile:
9897
PYDEVICES_DESKTOP_DIR = "board_configs/desktop"
9998

10099

100+
def lockfile_repository(mip_root: Path, profile: str) -> str:
101+
"""Return the GitHub repository the MIP lockfile names for *profile*.
102+
103+
A new profile is added to the lockfile deliberately, not auto-created.
104+
"""
105+
lockfile = mip_root / LOCKFILE_NAME
106+
if not lockfile.is_file():
107+
raise SystemExit(f"{lockfile} is missing; add {profile!r} to {LOCKFILE_NAME} before publishing")
108+
try:
109+
lock = json.loads(lockfile.read_text(encoding="utf-8"))
110+
except json.JSONDecodeError as exc:
111+
raise SystemExit(f"{lockfile} is not valid JSON: {exc}") from exc
112+
if not isinstance(lock, dict) or profile not in lock:
113+
raise SystemExit(f"{profile!r} is not in {lockfile}; add it before publishing")
114+
entry = lock[profile]
115+
if not isinstance(entry, dict) or not entry.get("repository"):
116+
raise SystemExit(f"{profile!r} in {lockfile} has no repository")
117+
return str(entry["repository"])
118+
119+
101120
def ignore_debris(_directory: str, names: list[str]) -> set[str]:
102121
# publishable() gates the top level; this gates everything nested inside a
103122
# package directory, which copytree would otherwise take wholesale. That is
@@ -155,9 +174,13 @@ def copy_component(source: Path, destination: Path) -> None:
155174

156175

157176
def render_pydevices_manifest(name: str, version: str, requirements: tuple[str, ...], payloads: tuple[str, ...] = ()) -> str:
177+
try:
178+
description = PYDEVICES_DESCRIPTIONS[name]
179+
except KeyError:
180+
raise SystemExit(f"no shared description for {name!r}") from None
158181
lines = [
159182
"metadata(",
160-
f' description="PyDevices {name}",',
183+
f" description={description!r},",
161184
f' version="{version}",',
162185
' author="Brad Barnett",',
163186
' license="MIT",',
@@ -223,15 +246,14 @@ def main() -> None:
223246
parser.add_argument("--version", required=True)
224247
args = parser.parse_args()
225248

226-
expected_repository = PROFILE_REPOSITORIES[args.profile]
249+
source_repository = args.source_repository.resolve()
250+
mip_root = args.mip_repository.resolve()
251+
expected_repository = lockfile_repository(mip_root, args.profile)
227252
if args.source_repository_name != expected_repository:
228253
raise SystemExit(
229254
f"profile {args.profile!r} requires {expected_repository}, "
230255
f"not {args.source_repository_name}"
231256
)
232-
233-
source_repository = args.source_repository.resolve()
234-
mip_root = args.mip_repository.resolve()
235257
if args.profile == "pydevices":
236258
synchronize_pydevices(source_repository, mip_root, args.version)
237259
return

0 commit comments

Comments
 (0)