diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 86515b8..c0c24f6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -112,9 +112,9 @@ jobs: fetched=$(pixi run --manifest-path modflow6/pixi.toml python scripts/fetch_releases.py --manifest releases.json --list) pixi run --manifest-path modflow6/pixi.toml make-program : --appdir $ostag --exclude "$fetched" --keep --zip $ostag.zip --verbose pixi run --manifest-path modflow6/pixi.toml make-program mf2005,mfusg,mfnwt,mflgr --appdir $ostag --double --keep --zip $ostag.zip --verbose - pixi run --manifest-path modflow6/pixi.toml make-code-json --appdir $ostag --zip $ostag.zip --verbose + pixi run --manifest-path modflow6/pixi.toml make-code-json --appdir $ostag --verbose - - name: Patch code.json with fetched program versions + - name: Patch code.json and code.md with fetched program versions, add code.json to zip run: | ostag="${{ steps.ostag.outputs.ostag }}" pixi run --manifest-path modflow6/pixi.toml python scripts/patch_code_json.py \ diff --git a/DEVELOPER.md b/DEVELOPER.md index ddec837..e7be825 100644 --- a/DEVELOPER.md +++ b/DEVELOPER.md @@ -91,8 +91,13 @@ The file `releases.json` in the repository root is a manifest listing programs t During a release build, the workflow: 1. Runs `scripts/fetch_releases.py` to download pre-built binaries for the current platform from each repository listed in `releases.json`. -2. Runs pymake to compile the remaining programs, excluding those already fetched. -3. Combines everything into the platform zip alongside pymake-generated metadata. +2. Runs pymake to compile the remaining programs, excluding those already fetched, and adds them to the platform zip. +3. Runs pymake's `make-code-json` to generate `code.json` and `code.md` metadata. These come from pymake's internal program catalog, which may list different versions than were actually fetched. +4. Runs `scripts/patch_code_json.py` to correct the version, URL, and date in `code.json` for each program in `releases.json`, regenerate `code.md` from the corrected `code.json`, and add `code.json` to the platform zip. + +`make-code-json` is deliberately run without `--zip`, so the patch script is the only thing that writes `code.json` into the zip. If the zip already contains a `code.json`, the patch script fails rather than adding a second, conflicting entry. The `code.json` that `make-code-json` also writes into the program directory is not corrected, but that directory is not distributed, only the zip is. + +The top-level `code.json` and `code.md` are uploaded with the build artifacts. The release job uses `code.md` for the release notes and for the program table in `README.md`. The fetch script handles platform-specific file extensions (`.exe`, `.dll`, `.dylib`, `.so`) and supports renaming programs when the archive filename differs from the distribution name (e.g., `mfusgt` in the archive becomes `mfusg_gsi` in the distribution). diff --git a/scripts/patch_code_json.py b/scripts/patch_code_json.py index b5a21c3..581ce31 100644 --- a/scripts/patch_code_json.py +++ b/scripts/patch_code_json.py @@ -3,7 +3,13 @@ make-code-json reads versions from pymake's internal program catalog, which may be out of date relative to the versions actually fetched. This script overwrites the version (and url) for each program listed in releases.json with the values -from the releases.json manifest. +from the releases.json manifest, then regenerates code.md (also written by +make-code-json from the same stale data) from the patched code.json. + +If --zip is given, the patched code.json is added to the archive. This script +is meant to be the only thing that puts code.json in the zip, so make-code-json +should be run *without* --zip; if the archive already contains a code.json the +script fails rather than writing a duplicate entry. Usage: python patch_code_json.py --manifest releases.json --ostag linux --zip linux.zip @@ -36,7 +42,29 @@ def _get_url_date(url): return None -def patch(manifest_path, code_json_path, ostag, zip_path=None): +def write_code_md(code, code_md_path): + """Write the code.md table in the same format as pymake's make-code-json.""" + lines = [ + "| Program | Version | UTC Date |", + "| ------- | ------- | ---- |", + ] + for target in sorted(code): + target_dict = code[target] + date = target_dict.get("url_download_asset_date") + line = f"| {target} | {target_dict['version']} |" + line += f" {date} |" if date else " |" + lines.append(line) + Path(code_md_path).write_text("\n".join(lines) + "\n") + print(f"wrote {code_md_path}") + + +def patch( + manifest_path, + code_json_path, + ostag, + zip_path=None, + code_md_path=None, +): with open(manifest_path) as f: manifest = json.load(f) @@ -70,20 +98,28 @@ def patch(manifest_path, code_json_path, ostag, zip_path=None): code[prog_name]["url_download_asset_date"] = date changed.append(f" {prog_name}: {prev_version!r} -> {version!r}") - if not changed: + if changed: + for line in changed: + print(line) + code_json_path.write_text(json.dumps(code, indent=4) + "\n") + print(f"wrote {code_json_path}") + else: print("code.json already up to date") - return - - for line in changed: - print(line) - code_json_path.write_text(json.dumps(code, indent=4) + "\n") - print(f"wrote {code_json_path}") + if code_md_path: + write_code_md(code, code_md_path) - if zip_path and Path(zip_path).exists(): + if zip_path: with zipfile.ZipFile(zip_path, "a", zipfile.ZIP_DEFLATED) as zf: + if "code.json" in zf.namelist(): + print( + f"{zip_path} already contains code.json; run make-code-json " + "without --zip so this script is the only writer", + file=sys.stderr, + ) + sys.exit(1) zf.write(code_json_path, "code.json") - print(f"updated code.json in {zip_path}") + print(f"added code.json to {zip_path}") def main(): @@ -107,10 +143,23 @@ def main(): parser.add_argument( "--zip", dest="zip_path", - help="zip file to update with the patched code.json", + help="zip file to add the patched code.json to", + ) + parser.add_argument( + "--code-md", + dest="code_md_path", + default="code.md", + help="path to code.md to regenerate from the patched code.json " + "(default: code.md; pass an empty string to skip)", ) args = parser.parse_args() - patch(args.manifest, args.code_json, args.ostag, args.zip_path) + patch( + args.manifest, + args.code_json, + args.ostag, + args.zip_path, + args.code_md_path or None, + ) if __name__ == "__main__":