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
11 changes: 7 additions & 4 deletions .github/workflows/release_build_artifacts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<ref>.tar.gz`) and
# `release_notes.txt`.
# - The `bazel_runfiles` wheel (`//python/runfiles:wheel.dist`).
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release_publish.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -88,6 +93,7 @@ jobs:
files: |
rules_python-*.tar.gz
release-artifacts/*
artifacts_manifest.json
SHA256SUMS
tag_name: ${{ inputs.tag_name || github.ref_name }}

Expand Down
6 changes: 3 additions & 3 deletions RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,9 @@ gh workflow run release_publish.yaml --ref <TAG> -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.
Expand Down
48 changes: 37 additions & 11 deletions dev/release_artifacts/generate_artifacts_manifest.py
Original file line number Diff line number Diff line change
@@ -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 ``<artifact>-<triple>[.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

Expand Down Expand Up @@ -86,31 +97,42 @@ 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:
raise ValueError("Manifest JSON cannot be embedded in a .bzl string")
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 <artifact>-<triple>[.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 <artifact>-<triple>[.exe] files.",
)
parser.add_argument(
"repo",
Expand All @@ -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


Expand Down
3 changes: 1 addition & 2 deletions tests/workflows/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -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"],
Expand Down
16 changes: 11 additions & 5 deletions tests/workflows/release_artifacts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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": [
{
Expand Down Expand Up @@ -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):
Expand Down
48 changes: 46 additions & 2 deletions tests/workflows/release_build_artifacts_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"))


Expand Down Expand Up @@ -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 = (
Expand Down
Loading