|
1 | 1 | """ |
2 | | -Inject git tag into manifest.json. |
| 2 | +Inject a version string into the Home Assistant manifest.json. |
3 | 3 |
|
4 | | -A utility module for handling version injection into a manifest.json file and managing build hooks |
5 | | -for Home Assistant custom integrations. |
6 | | -
|
7 | | -This module provides a Hatch build hook (CustomHook) for version injection into |
8 | | -`manifest.json`, resolving versions from Git tags when operating outside of Hatch, and |
9 | | -a CLI for creating zip releases with the computed version values. |
10 | | -
|
11 | | -The exported functionality includes: |
12 | | -- Resolving versions based on Git tags. |
13 | | -- Injecting version data into `manifest.json`. |
14 | | -- A CLI entry point for performing version injection. |
15 | | -
|
16 | | -Classes: |
17 | | -- CustomHook: A custom build hook for Hatch to inject computed version into `manifest.json`. |
18 | | -
|
19 | | -Functions: |
20 | | -- resolve_version_from_git_tag: Resolves the integration version, falling back to `0.0.0` |
21 | | - when no Git tag is available. |
22 | | -- _cli: Handles the command-line interface for direct invocation of the versioning process. |
23 | | -
|
24 | | -Constants: |
25 | | -- MANIFEST_REL_PATH: Relative path to the `manifest.json` file within a Home Assistant custom component. |
| 4 | +Used by the release workflow to stamp the git tag version into |
| 5 | +``custom_components/run_chicken/manifest.json`` before packaging the HACS zip. |
| 6 | +The repository copy stays at the ``0.0.0`` placeholder; the version is only |
| 7 | +injected into the ephemeral CI checkout that gets zipped. |
26 | 8 | """ |
27 | 9 |
|
28 | 10 | import argparse |
29 | 11 | import json |
30 | 12 | import os |
31 | | -import shutil |
32 | 13 | import subprocess |
33 | 14 | import sys |
34 | 15 | from pathlib import Path |
35 | 16 |
|
36 | | -try: |
37 | | - # Available during Hatch builds |
38 | | - from hatchling.builders.hooks.plugin.interface import BuildHookInterface |
39 | | -except ImportError: |
40 | | - # Not available outside Hatch so we have CustomHook use object as its base class |
41 | | - BuildHookInterface = object # type: ignore[misc,assignment] |
42 | | - |
43 | | - |
44 | 17 | MANIFEST_REL_PATH = Path("custom_components/run_chicken/manifest.json") |
45 | 18 |
|
46 | 19 |
|
47 | | -def _read_json(path: Path) -> dict: |
48 | | - with path.open("r", encoding="utf-8") as f: |
49 | | - return json.load(f) |
50 | | - |
51 | | - |
52 | | -def _write_json(path: Path, data: dict) -> None: |
53 | | - path.parent.mkdir(parents=True, exist_ok=True) |
54 | | - with path.open("w", encoding="utf-8") as f: |
55 | | - json.dump(data, f, indent=2, ensure_ascii=False) |
56 | | - f.write("\n") |
57 | | - |
58 | | - |
59 | | -def _inject_version(manifest_path: Path, version: str) -> None: |
60 | | - data = _read_json(manifest_path) |
61 | | - data["version"] = version |
62 | | - _write_json(manifest_path, data) |
63 | | - |
64 | | - |
65 | 20 | def resolve_version_from_git_tag() -> str: |
66 | 21 | """ |
67 | | - Best-effort version resolution from Git tags when outside Hatch. |
| 22 | + Resolve the version from the git tag, dropping a leading ``v``. |
68 | 23 |
|
69 | | - Returns the tag without a leading 'v' if present. Falls back to '0.0.0'. |
| 24 | + Prefers the ref name provided by CI (``GITHUB_REF_NAME``) and falls back to |
| 25 | + ``git describe``. Returns ``0.0.0`` when no tag is available. |
70 | 26 | """ |
71 | 27 | try: |
72 | | - # Prefer the ref name provided by CI |
73 | 28 | ref_name = os.environ.get("GITHUB_REF_NAME") |
74 | | - if ref_name: |
75 | | - tag = ref_name |
76 | | - else: |
77 | | - tag = subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], text=True).strip() |
78 | | - |
| 29 | + tag = ref_name or subprocess.check_output(["git", "describe", "--tags", "--abbrev=0"], text=True).strip() |
79 | 30 | except (OSError, subprocess.CalledProcessError): |
80 | 31 | return "0.0.0" |
81 | | - |
82 | 32 | return tag.removeprefix("v") |
83 | 33 |
|
84 | 34 |
|
85 | | -class CustomHook(BuildHookInterface): # type: ignore[misc] |
86 | | - """ |
87 | | - Hatch custom build hook to inject the computed version into manifest.json. |
88 | | -
|
89 | | - This operates on a staged temp copy and force-includes it for both wheel and sdist, |
90 | | - leaving the repository file untouched. |
91 | | - """ |
92 | | - |
93 | | - def initialize(self, version: str, build_data: dict) -> None: # type: ignore[override] |
94 | | - """ |
95 | | - Occurs immediately before each build. |
96 | | -
|
97 | | - Any modifications to the build data will be seen by the build target. |
98 | | - """ |
99 | | - # "version" passed to this function only tells you the _kind_ of build: "standard" |
100 | | - version = self.metadata.version |
101 | | - |
102 | | - _inject_version(MANIFEST_REL_PATH, version) |
103 | | - |
104 | | - # Force-include the modified manifest at the same relative path |
105 | | - force_include = build_data.setdefault("force_include", {}) |
106 | | - force_include[str(MANIFEST_REL_PATH)] = str(MANIFEST_REL_PATH) |
107 | | - |
108 | | - |
109 | | -def _cli(argv: list[str]) -> int: |
110 | | - """CLI entrypoint used by GitHub Actions to create a zip release.""" |
111 | | - parser = argparse.ArgumentParser(description="Inject version into Home Assistant manifest.json") |
112 | | - parser.add_argument("--version", help="Version to inject; if omitted, derived from git tag.") |
113 | | - group = parser.add_mutually_exclusive_group(required=True) |
114 | | - group.add_argument("--file", type=Path, help="Path to a manifest.json to rewrite in-place.") |
115 | | - group.add_argument( |
116 | | - "--copy-dir", |
117 | | - nargs=2, |
118 | | - metavar=("SRC_DIR", "DEST_DIR"), |
119 | | - help=( |
120 | | - "Copy the integration directory from SRC_DIR to DEST_DIR and rewrite the copied " |
121 | | - "manifest.json with the resolved version." |
122 | | - ), |
| 35 | +def inject_version(manifest_path: Path, version: str) -> None: |
| 36 | + """Write ``version`` into the manifest at ``manifest_path``.""" |
| 37 | + data = json.loads(manifest_path.read_text(encoding="utf-8")) |
| 38 | + data["version"] = version |
| 39 | + manifest_path.write_text(json.dumps(data, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") |
| 40 | + |
| 41 | + |
| 42 | +def main(argv: list[str]) -> int: |
| 43 | + """Inject a version into a manifest.json (CLI entry point).""" |
| 44 | + parser = argparse.ArgumentParser(description="Inject a version into manifest.json") |
| 45 | + parser.add_argument("--version", help="Version to inject; derived from the git tag if omitted.") |
| 46 | + parser.add_argument( |
| 47 | + "--file", |
| 48 | + type=Path, |
| 49 | + default=MANIFEST_REL_PATH, |
| 50 | + help="Path to the manifest.json to rewrite in place.", |
123 | 51 | ) |
124 | | - |
125 | 52 | args = parser.parse_args(argv) |
126 | | - version = args.version or resolve_version_from_git_tag() |
127 | | - |
128 | | - if args.file: |
129 | | - _inject_version(args.file, version) |
130 | | - return 0 |
131 | | - |
132 | | - src_dir, dest_dir = map(Path, args.copy_dir) |
133 | | - if not (src_dir / MANIFEST_REL_PATH).is_file(): |
134 | | - parser.error(f"Could not find {MANIFEST_REL_PATH} under {src_dir}") |
135 | 53 |
|
136 | | - # Copy tree, then rewrite manifest in the copied location |
137 | | - if dest_dir.exists(): |
138 | | - shutil.rmtree(dest_dir) |
139 | | - shutil.copytree(src_dir, dest_dir) |
140 | | - copied_manifest = dest_dir / MANIFEST_REL_PATH |
141 | | - _inject_version(copied_manifest, version) |
| 54 | + inject_version(args.file, args.version or resolve_version_from_git_tag()) |
142 | 55 | return 0 |
143 | 56 |
|
144 | 57 |
|
145 | 58 | if __name__ == "__main__": |
146 | | - raise SystemExit(_cli(sys.argv[1:])) |
| 59 | + raise SystemExit(main(sys.argv[1:])) |
0 commit comments