Skip to content

Commit b80ab8c

Browse files
authored
chore: add support for librarian release init (googleapis#14327)
This PR adds support for the librarian language specific `release-init` commit following https://github.com/googleapis/librarian/blob/main/doc/language-onboarding.md Towards googleapis/librarian#886
1 parent 64752f6 commit b80ab8c

2 files changed

Lines changed: 141 additions & 5 deletions

File tree

.generator/cli.py

Lines changed: 77 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -36,13 +36,15 @@
3636

3737
logger = logging.getLogger()
3838

39-
LIBRARIAN_DIR = "librarian"
39+
BUILD_REQUEST_FILE = "build-request.json"
4040
GENERATE_REQUEST_FILE = "generate-request.json"
41+
RELEASE_INIT_REQUEST_FILE = "release-init-request.json"
42+
4143
INPUT_DIR = "input"
42-
BUILD_REQUEST_FILE = "build-request.json"
43-
SOURCE_DIR = "source"
44+
LIBRARIAN_DIR = "librarian"
4445
OUTPUT_DIR = "output"
4546
REPO_DIR = "repo"
47+
SOURCE_DIR = "source"
4648

4749

4850
def _read_json_file(path: str) -> Dict:
@@ -371,7 +373,6 @@ def handle_generate(
371373
except Exception as e:
372374
raise ValueError("Generation failed.") from e
373375

374-
# TODO(https://github.com/googleapis/librarian/issues/448): Implement generate command and update docstring.
375376
logger.info("'generate' command executed.")
376377

377378

@@ -433,23 +434,91 @@ def handle_build(librarian: str = LIBRARIAN_DIR, repo: str = REPO_DIR):
433434
logger.info("'build' command executed.")
434435

435436

437+
def _get_libraries_to_prepare_for_release(library_entries: Dict) -> List[dict]:
438+
"""Get libraries which should be prepared for release. Only libraries
439+
which have the `release_triggered` field set to `True` will be returned.
440+
441+
Args:
442+
library_entries(Dict): Dictionary containing all of the libraries to
443+
evaluate.
444+
445+
Returns:
446+
List[dict]: List of all libraries which should be prepared for release,
447+
along with the corresponding information for the release.
448+
"""
449+
return [
450+
library
451+
for library in library_entries["libraries"]
452+
if library.get("release_triggered")
453+
]
454+
455+
456+
def handle_release_init(
457+
librarian: str = LIBRARIAN_DIR, repo: str = REPO_DIR, output: str = OUTPUT_DIR
458+
):
459+
"""The main coordinator for the release preparation process.
460+
461+
This function prepares for the release of client libraries by reading a
462+
`librarian/release-init-request.json` file. The primary responsibility is
463+
to update all required files with the new version and commit information
464+
for libraries that have the `release_triggered` field set to `True`.
465+
466+
See https://github.com/googleapis/librarian/blob/main/doc/container-contract.md#generate-container-command
467+
468+
Args:
469+
librarian(str): Path to the directory in the container which contains
470+
the `release-init-request.json` file.
471+
repo(str): This directory will contain all directories that make up a
472+
library, the .librarian folder, and any global file declared in
473+
the config.yaml.
474+
output(str): Path to the directory in the container where modified
475+
code should be placed.
476+
"""
477+
478+
try:
479+
# Read a release-init-request.json file
480+
request_data = _read_json_file(f"{librarian}/{RELEASE_INIT_REQUEST_FILE}")
481+
libraries_to_prep_for_release = _get_libraries_to_prepare_for_release(
482+
request_data
483+
)
484+
485+
# TODO(https://github.com/googleapis/google-cloud-python/pull/14349):
486+
# Update library global changelog file.
487+
488+
# Prepare the release for each library by updating the
489+
# library specific version files and library specific changelog.
490+
for library_release_data in libraries_to_prep_for_release:
491+
# TODO(https://github.com/googleapis/google-cloud-python/pull/14350):
492+
# Update library specific version files.
493+
# TODO(https://github.com/googleapis/google-cloud-python/pull/14351):
494+
# Conditionally update the library specific CHANGELOG if there is a change.
495+
pass
496+
497+
except Exception as e:
498+
raise ValueError(f"Release init failed: {e}") from e
499+
500+
logger.info("'release-init' command executed.")
501+
502+
436503
if __name__ == "__main__": # pragma: NO COVER
437504
parser = argparse.ArgumentParser(description="A simple CLI tool.")
438505
subparsers = parser.add_subparsers(
439506
dest="command", required=True, help="Available commands"
440507
)
441508

442-
# Define commands
509+
# Define commands and their corresponding handler functions
443510
handler_map = {
444511
"configure": handle_configure,
445512
"generate": handle_generate,
446513
"build": handle_build,
514+
"release-init": handle_release_init,
447515
}
448516

449517
for command_name, help_text in [
450518
("configure", "Onboard a new library or an api path to Librarian workflow."),
451519
("generate", "generate a python client for an API."),
452520
("build", "Run unit tests via nox for the generated library."),
521+
("release-init", "Prepare to release a given set of libraries"),
453522
]:
454523
parser_cmd = subparsers.add_parser(command_name, help=help_text)
455524
parser_cmd.set_defaults(func=handler_map[command_name])
@@ -483,6 +552,7 @@ def handle_build(librarian: str = LIBRARIAN_DIR, repo: str = REPO_DIR):
483552
help="Path to the directory in the container which contains google-cloud-python repository",
484553
default=REPO_DIR,
485554
)
555+
486556
if len(sys.argv) == 1:
487557
parser.print_help(sys.stderr)
488558
sys.exit(1)
@@ -499,5 +569,7 @@ def handle_build(librarian: str = LIBRARIAN_DIR, repo: str = REPO_DIR):
499569
)
500570
elif args.command == "build":
501571
args.func(librarian=args.librarian, repo=args.repo)
572+
elif args.command == "release-init":
573+
args.func(librarian=args.librarian, repo=args.repo, output=args.output)
502574
else:
503575
args.func()

.generator/test_cli.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,15 @@
2323
from cli import (
2424
GENERATE_REQUEST_FILE,
2525
BUILD_REQUEST_FILE,
26+
RELEASE_INIT_REQUEST_FILE,
2627
LIBRARIAN_DIR,
2728
REPO_DIR,
2829
_build_bazel_target,
2930
_clean_up_files_after_post_processing,
3031
_copy_files_needed_for_post_processing,
3132
_determine_bazel_rule,
3233
_get_library_id,
34+
_get_libraries_to_prepare_for_release,
3335
_locate_and_extract_artifact,
3436
_read_json_file,
3537
_run_individual_session,
@@ -38,6 +40,7 @@
3840
handle_build,
3941
handle_configure,
4042
handle_generate,
43+
handle_release_init,
4144
)
4245

4346

@@ -92,6 +95,40 @@ def mock_generate_request_data_for_nox():
9295
}
9396

9497

98+
@pytest.fixture
99+
def mock_release_init_request_file(tmp_path, monkeypatch):
100+
"""Creates the mock request file at the correct path inside a temp dir."""
101+
# Create the path as expected by the script: .librarian/release-request.json
102+
request_path = f"{LIBRARIAN_DIR}/{RELEASE_INIT_REQUEST_FILE}"
103+
request_dir = tmp_path / os.path.dirname(request_path)
104+
request_dir.mkdir()
105+
request_file = request_dir / os.path.basename(request_path)
106+
107+
request_content = {
108+
"libraries": [
109+
{
110+
"id": "google-cloud-another-library",
111+
"apis": [{"path": "google/cloud/another/library/v1"}],
112+
"release_triggered": False,
113+
"version": "1.2.3",
114+
"changes": [],
115+
},
116+
{
117+
"id": "google-cloud-language",
118+
"apis": [{"path": "google/cloud/language/v1"}],
119+
"release_triggered": True,
120+
"version": "1.2.3",
121+
"changes": [],
122+
},
123+
]
124+
}
125+
request_file.write_text(json.dumps(request_content))
126+
127+
# Change the current working directory to the temp path for the test.
128+
monkeypatch.chdir(tmp_path)
129+
return request_file
130+
131+
95132
def test_get_library_id_success():
96133
"""Tests that _get_library_id returns the correct ID when present."""
97134
request_data = {"id": "test-library", "name": "Test Library"}
@@ -454,3 +491,30 @@ def test_clean_up_files_after_post_processing_success(mocker):
454491
mock_shutil_rmtree = mocker.patch("shutil.rmtree")
455492
mock_os_remove = mocker.patch("os.remove")
456493
_clean_up_files_after_post_processing("output", "library_id")
494+
495+
496+
def test_get_libraries_to_prepare_for_release(mock_release_init_request_file):
497+
"""
498+
Tests that only libraries with the `release_triggered` field set to `True` are
499+
returned.
500+
"""
501+
request_data = _read_json_file(f"{LIBRARIAN_DIR}/{RELEASE_INIT_REQUEST_FILE}")
502+
libraries_to_prep_for_release = _get_libraries_to_prepare_for_release(request_data)
503+
assert len(libraries_to_prep_for_release) == 1
504+
assert "google-cloud-language" in libraries_to_prep_for_release[0]["id"]
505+
assert libraries_to_prep_for_release[0]["release_triggered"]
506+
507+
508+
def test_handle_release_init_success(mock_release_init_request_file):
509+
"""
510+
Simply tests that `handle_release_init` runs without errors.
511+
"""
512+
handle_release_init()
513+
514+
515+
def test_handle_release_init_fail():
516+
"""
517+
Tests that handle_release_init fails to read `librarian/release-init-request.json`.
518+
"""
519+
with pytest.raises(ValueError):
520+
handle_release_init()

0 commit comments

Comments
 (0)