From f76e7c6d6ee87fd6582835f7374a4ba66355f339 Mon Sep 17 00:00:00 2001 From: Richard Levasseur Date: Sat, 31 Oct 2026 22:01:45 -0700 Subject: [PATCH] build(release): upload artifacts_manifest.json to GitHub releases Switching the release manifest generator to emit `artifacts_manifest.bzl` (#4370) dropped the standalone `artifacts_manifest.json` output, and the publish workflow never downloaded `release-manifest` to attach it to the GitHub release. Generate both `artifacts_manifest.bzl` (bundled into the source archive) and `artifacts_manifest.json` via explicit `--bzl-output` and `--json-output` flags, upload `artifacts_manifest.json` in the `release-manifest` workflow artifact, and download and attach it to the GitHub release during publishing. Work towards #4250 --- .../workflows/release_build_artifacts.yaml | 11 +++-- .github/workflows/release_publish.yaml | 6 +++ RELEASING.md | 6 +-- .../generate_artifacts_manifest.py | 48 ++++++++++++++----- tests/workflows/BUILD.bazel | 3 +- tests/workflows/release_artifacts_test.py | 16 +++++-- .../workflows/release_build_artifacts_test.py | 48 ++++++++++++++++++- 7 files changed, 111 insertions(+), 27 deletions(-) diff --git a/.github/workflows/release_build_artifacts.yaml b/.github/workflows/release_build_artifacts.yaml index aa73b079a5..32a96803b3 100644 --- a/.github/workflows/release_build_artifacts.yaml +++ b/.github/workflows/release_build_artifacts.yaml @@ -4,7 +4,8 @@ # see dev/release_artifacts/build.sh. Linux x86_64 runners cross-compile the # files for other Linux CPUs. macOS and Windows files need those OSes' SDKs, # so they're built on those OSes. -# - `SHA256SUMS` and `artifacts_manifest.bzl` for the built platform binaries. +# - `SHA256SUMS`, `artifacts_manifest.json`, and `artifacts_manifest.bzl` for +# the built platform binaries. # - The release source archive (`rules_python-.tar.gz`) and # `release_notes.txt`. # - The `bazel_runfiles` wheel (`//python/runfiles:wheel.dist`). @@ -187,14 +188,16 @@ jobs: hashFiles('release-artifacts/*') != '' run: >- dev/release_artifacts/generate_artifacts_manifest.py - "$REF" release-artifacts artifacts_manifest.bzl + --bzl-output=artifacts_manifest.bzl + --json-output=artifacts_manifest.json + -- "$REF" release-artifacts - name: Upload artifacts manifest - if: hashFiles('artifacts_manifest.bzl') != '' + if: hashFiles('artifacts_manifest.json') != '' uses: actions/upload-artifact@v7 with: name: release-manifest - path: artifacts_manifest.bzl + path: artifacts_manifest.json if-no-files-found: error - name: Create release archive and notes diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml index 49bb174f42..4b64bc5f3e 100644 --- a/.github/workflows/release_publish.yaml +++ b/.github/workflows/release_publish.yaml @@ -69,6 +69,11 @@ jobs: with: name: release-archive + - name: Download artifacts manifest + uses: actions/download-artifact@v8 + with: + name: release-manifest + - name: Download built artifacts uses: actions/download-artifact@v8 with: @@ -88,6 +93,7 @@ jobs: files: | rules_python-*.tar.gz release-artifacts/* + artifacts_manifest.json SHA256SUMS tag_name: ${{ inputs.tag_name || github.ref_name }} diff --git a/RELEASING.md b/RELEASING.md index a52bf47d8a..4b687ba58d 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -70,9 +70,9 @@ gh workflow run release_publish.yaml --ref -f publish_to_pypi=false ### Release artifacts The release workflow also builds `//dev/release_artifacts:artifacts_for_release` -for each platform and uploads the built artifacts and `SHA256SUMS` to the GitHub -release. For now, build failures only emit a warning rather than failing the -release. +for each platform and uploads the built artifacts, `artifacts_manifest.json`, +and `SHA256SUMS` to the GitHub release. For now, build failures only emit a +warning rather than failing the release. Presubmit builds it for every platform too: natively on each CI host, and through `//dev/release_artifacts:cross_compiled_artifacts` for the others. diff --git a/dev/release_artifacts/generate_artifacts_manifest.py b/dev/release_artifacts/generate_artifacts_manifest.py index 30b5b4bb80..ae8ad4a377 100755 --- a/dev/release_artifacts/generate_artifacts_manifest.py +++ b/dev/release_artifacts/generate_artifacts_manifest.py @@ -1,5 +1,16 @@ #!/usr/bin/env python3 -"""Generates an artifacts_manifest.bzl file for built release artifacts.""" +"""Generates artifacts_manifest.json and artifacts_manifest.bzl files. + +Reads built release binaries named ``-[.exe]`` from +``artifacts_dir`` and writes their SHA-256 digests, download URLs, and platform +constraints to both JSON and Starlark manifest files. + +Usage: + dev/release_artifacts/generate_artifacts_manifest.py \\ + --bzl-output=artifacts_manifest.bzl \\ + --json-output=artifacts_manifest.json \\ + -- 2.4.0 release-artifacts [bazel-contrib/rules_python] +""" from __future__ import annotations @@ -86,8 +97,12 @@ def generate_manifest( return {"artifacts": dict(sorted(artifacts.items()))} +def render_json(manifest: dict[str, dict[str, list[ArtifactEntry]]]) -> str: + return json.dumps(manifest, indent=2, sort_keys=True) + "\n" + + def render_bzl(manifest: dict[str, dict[str, list[ArtifactEntry]]]) -> str: - manifest_json = json.dumps(manifest, indent=1, sort_keys=True) + manifest_json = render_json(manifest) # Embedded in a Starlark string literal, so it must not end the literal # early or rely on escape sequences. if '"""' in manifest_json or "\\" in manifest_json: @@ -95,22 +110,29 @@ def render_bzl(manifest: dict[str, dict[str, list[ArtifactEntry]]]) -> str: return ( '"""Manifest of prebuilt tool binaries for this release."""\n' "\n" - f'ARTIFACTS_MANIFEST_JSON = """\n{manifest_json}\n"""\n' + f'ARTIFACTS_MANIFEST_JSON = """\n{manifest_json}"""\n' ) def _main(argv: list[str] | None = None) -> int: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("tag", help="Release tag name (e.g. 2.4.0).") parser.add_argument( - "artifacts_dir", + "--bzl-output", type=Path, - help="Directory containing built -[.exe] files.", + required=True, + help="Path to write the Starlark (.bzl) manifest file.", ) parser.add_argument( - "output_file", + "--json-output", type=Path, - help="Path to the artifacts_manifest.bzl file to write.", + required=True, + help="Path to write the JSON (.json) manifest file.", + ) + parser.add_argument("tag", help="Release tag name (e.g. 2.4.0).") + parser.add_argument( + "artifacts_dir", + type=Path, + help="Directory containing built -[.exe] files.", ) parser.add_argument( "repo", @@ -121,12 +143,16 @@ def _main(argv: list[str] | None = None) -> int: args = parser.parse_args(argv) try: manifest = generate_manifest(args.tag, args.artifacts_dir, args.repo) - manifest_bzl = render_bzl(manifest) + outputs = ( + (args.json_output, render_json(manifest)), + (args.bzl_output, render_bzl(manifest)), + ) except ValueError as err: print(f"::error::{err}") return 1 - args.output_file.parent.mkdir(parents=True, exist_ok=True) - args.output_file.write_text(manifest_bzl, encoding="utf-8") + for out_path, content in outputs: + out_path.parent.mkdir(parents=True, exist_ok=True) + out_path.write_text(content, encoding="utf-8") return 0 diff --git a/tests/workflows/BUILD.bazel b/tests/workflows/BUILD.bazel index de839eb67f..3fdcb98713 100644 --- a/tests/workflows/BUILD.bazel +++ b/tests/workflows/BUILD.bazel @@ -42,8 +42,7 @@ pytest_test( name = "release_build_artifacts_test", srcs = ["release_build_artifacts_test.py"], data = [ - "//.github/workflows:create_archive_and_notes.sh", - "//.github/workflows:release_build_artifacts.yaml", + "//.github/workflows:all_files", "//dev/platforms:BUILD.bazel", ], target_compatible_with = ["@platforms//os:linux"], diff --git a/tests/workflows/release_artifacts_test.py b/tests/workflows/release_artifacts_test.py index c503d7aa3b..b66b38d7e6 100644 --- a/tests/workflows/release_artifacts_test.py +++ b/tests/workflows/release_artifacts_test.py @@ -20,7 +20,8 @@ def test_generate_artifacts_manifest(tmp_path: Path): artifacts_dir = tmp_path / "release-artifacts" artifacts_dir.mkdir(parents=True) - manifest_file = tmp_path / "artifacts_manifest.bzl" + manifest_json_file = tmp_path / "artifacts_manifest.json" + manifest_bzl_file = tmp_path / "artifacts_manifest.bzl" linux_bin = artifacts_dir / "exe_zip_maker-x86_64-unknown-linux-gnu" linux_bin.write_bytes(b"linux-binary-contents") @@ -37,20 +38,23 @@ def test_generate_artifacts_manifest(tmp_path: Path): subprocess.run( [ str(generate_script), + f"--bzl-output={manifest_bzl_file}", + f"--json-output={manifest_json_file}", + "--", "2.4.0", str(artifacts_dir), - str(manifest_file), "bazel-contrib/rules_python", ], check=True, ) - manifest_bzl = manifest_file.read_text(encoding="utf-8") + json_manifest = json.loads(manifest_json_file.read_text(encoding="utf-8")) + manifest_bzl = manifest_bzl_file.read_text(encoding="utf-8") _, marker, manifest_json = manifest_bzl.partition('ARTIFACTS_MANIFEST_JSON = """') assert marker - manifest = json.loads(manifest_json.removesuffix('"""\n')) + bzl_manifest = json.loads(manifest_json.removesuffix('"""\n')) base_url = "https://github.com/bazel-contrib/rules_python/releases/download/2.4.0" - assert manifest == { + expected_manifest = { "artifacts": { "exe_zip_maker": [ { @@ -83,6 +87,8 @@ def test_generate_artifacts_manifest(tmp_path: Path): ] } } + assert json_manifest == expected_manifest + assert bzl_manifest == expected_manifest def test_create_archive_and_notes_with_manifest(tmp_path: Path): diff --git a/tests/workflows/release_build_artifacts_test.py b/tests/workflows/release_build_artifacts_test.py index 9262d499c2..ad933a1b60 100644 --- a/tests/workflows/release_build_artifacts_test.py +++ b/tests/workflows/release_build_artifacts_test.py @@ -13,9 +13,9 @@ from python.runfiles import runfiles -def _load_workflow() -> dict: +def _load_workflow(filename: str = "release_build_artifacts.yaml") -> dict: root = runfiles.CreateOrRaise().root() / "rules_python" - workflow_path = root / ".github/workflows/release_build_artifacts.yaml" + workflow_path = root / ".github/workflows" / filename return yaml.safe_load(workflow_path.read_text(encoding="utf-8")) @@ -91,6 +91,50 @@ def test_build_jobs_use_buildbuddy_rbe(): assert "secrets.BUILDBUDDY_READ_WRITE_KEY" in api_key +def test_artifacts_manifest_json_built_and_published(): + """Verifies artifacts_manifest.json is generated, uploaded, and published.""" + build_workflow = _load_workflow() + package_steps = build_workflow["jobs"]["package"]["steps"] + + generate_steps = [ + s for s in package_steps if s.get("name") == "Generate artifacts manifest" + ] + assert len(generate_steps) == 1 + generate_cmd = generate_steps[0]["run"] + assert "--bzl-output=artifacts_manifest.bzl" in generate_cmd + assert "--json-output=artifacts_manifest.json" in generate_cmd + + upload_steps = [ + s for s in package_steps if s.get("name") == "Upload artifacts manifest" + ] + assert len(upload_steps) == 1 + assert upload_steps[0]["with"]["name"] == "release-manifest" + assert "artifacts_manifest.json" in upload_steps[0]["with"]["path"] + + publish_workflow = _load_workflow("release_publish.yaml") + release_steps = publish_workflow["jobs"]["release"]["steps"] + download_manifest_steps = [ + s + for s in release_steps + if s.get("uses", "").startswith("actions/download-artifact") + and s.get("with", {}).get("name") == "release-manifest" + ] + assert len(download_manifest_steps) == 1 + + gh_release_steps = [ + s + for s in release_steps + if s.get("uses", "").startswith("softprops/action-gh-release") + ] + assert len(gh_release_steps) == 1 + release_files = [ + line.strip() + for line in gh_release_steps[0]["with"]["files"].splitlines() + if line.strip() + ] + assert "artifacts_manifest.json" in release_files + + def test_create_archive_and_notes_fetches_and_expands_tag(tmp_path: Path): """Verifies create_archive_and_notes.sh fetches missing tags and expands export-subst.""" script = (