From 3f6eca4c03214e021cc333fecd466abea67bd1a3 Mon Sep 17 00:00:00 2001 From: mvgijssel <6029816+mvgijssel@users.noreply.github.com> Date: Tue, 5 Dec 2023 16:03:05 +0100 Subject: [PATCH] test(rules_release): Bazel integration test for setting up using a WORKSPACE file (#612) --- .bazelignore | 6 + .bazelrc | 4 +- .../rules_release-quick-cheetahs-thank-2.md | 5 + .../rules_release-quick-cheetahs-thank-3.md | 5 + .../rules_release-quick-cheetahs-thank-4.md | 5 + .../rules_release-quick-cheetahs-thank.md | 5 + .gitignore | 5 +- .vscode/settings.json | 5 +- BUILD.bazel | 1 - buildbuddy.yaml | 2 +- rules/rules_release/.bazelignore | 3 +- rules/rules_release/.bazelrc | 5 +- rules/rules_release/.bazelversion | 1 + rules/rules_release/BUILD.bazel | 60 +- rules/rules_release/MODULE.bazel | 24 +- rules/rules_release/examples/BUILD.bazel | 36 + .../examples/workspace/.bazelignore | 4 + .../rules_release/examples/workspace/.bazelrc | 5 + .../examples/workspace/.changeset/config.json | 15 + .../examples/workspace/BUILD.bazel | 80 ++ .../examples/workspace/CHANGELOG_bar.md | 0 .../examples/workspace/CHANGELOG_foo.md | 0 .../examples/workspace/VERSION_bar.txt | 1 + .../examples/workspace/VERSION_foo.txt | 1 + .../examples/workspace/WORKSPACE | 25 + .../rules_release/examples/workspace/bar.txt | 1 + .../examples/workspace/bar_not_changed.sh | 4 + .../examples/workspace/final_publish.sh | 5 + .../rules_release/examples/workspace/foo.txt | 1 + .../examples/workspace/foo_changed.sh | 4 + .../examples/workspace/foo_publish.sh | 4 + .../examples/workspace/package.json | 9 + .../examples/workspace/tests/BUILD.bazel | 37 + .../examples/workspace/tests/test.sh | 52 ++ .../examples/workspace/tests/unittest.bash | 846 ++++++++++++++++++ .../workspace/tests/unittest_utils.sh | 182 ++++ .../lib/actions/GenerateAction.mjs | 40 - .../lib/repositories/TargetRepository.mjs | 87 -- rules/rules_release/lib/utils.mjs | 4 - rules/rules_release/release/BUILD.bazel | 41 + .../{ => release}/changesets_cli.mjs | 0 rules/rules_release/{ => release}/cli.mjs | 12 +- rules/rules_release/release/defs.bzl | 2 - .../{ => release}/extensions.bzl | 4 +- .../release/lib/actions/GenerateAction.mjs | 26 + .../lib/actions/PublishAction.mjs | 0 .../lib/actions/VersionAction.mjs | 0 .../lib/repositories/ChangelogRepository.mjs | 0 .../lib/repositories/ChangesetRepository.mjs | 18 +- .../lib/repositories/ConfigRepository.mjs | 6 +- .../lib/repositories/PackageRepository.mjs | 0 .../lib/repositories/PublishRepository.mjs | 0 .../lib/repositories/ReleaseRepository.mjs | 24 + .../lib/repositories/VersionRepository.mjs | 0 rules/rules_release/release/lib/utils.mjs | 9 + .../rules_release/release/private/release.bzl | 18 +- .../release/private/release_manager.bzl | 19 +- rules/rules_release/release/repositories.bzl | 137 +++ .../release/repository_primary_deps.bzl | 37 + .../release/repository_secondary_deps.bzl | 16 + rules/rules_release/repositories.bzl | 53 -- rules/rules_release/tools/BUILD.bazel | 27 + .../tools/bazel_diff_change_cli.mjs | 35 + rules/rules_release/tools/defs.bzl | 5 + .../lib/actions/BazelDiffChangeAction.mjs | 42 + .../lib/repositories/BazelDiffRepository.mjs | 99 ++ rules/rules_release/tools/private/BUILD.bazel | 6 + .../tools/private/bazel_diff_release.bzl | 95 ++ .../private/publish_github_release.bzl | 0 rules/rules_task/BUILD.bazel | 3 +- tools/bunq2ynab/BUILD.bazel | 2 +- 71 files changed, 2021 insertions(+), 294 deletions(-) create mode 100644 .changeset/rules_release-quick-cheetahs-thank-2.md create mode 100644 .changeset/rules_release-quick-cheetahs-thank-3.md create mode 100644 .changeset/rules_release-quick-cheetahs-thank-4.md create mode 100644 .changeset/rules_release-quick-cheetahs-thank.md create mode 120000 rules/rules_release/.bazelversion create mode 100644 rules/rules_release/examples/BUILD.bazel create mode 100644 rules/rules_release/examples/workspace/.bazelignore create mode 100644 rules/rules_release/examples/workspace/.bazelrc create mode 100644 rules/rules_release/examples/workspace/.changeset/config.json create mode 100644 rules/rules_release/examples/workspace/BUILD.bazel create mode 100644 rules/rules_release/examples/workspace/CHANGELOG_bar.md create mode 100644 rules/rules_release/examples/workspace/CHANGELOG_foo.md create mode 100644 rules/rules_release/examples/workspace/VERSION_bar.txt create mode 100644 rules/rules_release/examples/workspace/VERSION_foo.txt create mode 100644 rules/rules_release/examples/workspace/WORKSPACE create mode 100644 rules/rules_release/examples/workspace/bar.txt create mode 100755 rules/rules_release/examples/workspace/bar_not_changed.sh create mode 100755 rules/rules_release/examples/workspace/final_publish.sh create mode 100644 rules/rules_release/examples/workspace/foo.txt create mode 100755 rules/rules_release/examples/workspace/foo_changed.sh create mode 100755 rules/rules_release/examples/workspace/foo_publish.sh create mode 100644 rules/rules_release/examples/workspace/package.json create mode 100644 rules/rules_release/examples/workspace/tests/BUILD.bazel create mode 100755 rules/rules_release/examples/workspace/tests/test.sh create mode 100644 rules/rules_release/examples/workspace/tests/unittest.bash create mode 100644 rules/rules_release/examples/workspace/tests/unittest_utils.sh delete mode 100644 rules/rules_release/lib/actions/GenerateAction.mjs delete mode 100644 rules/rules_release/lib/repositories/TargetRepository.mjs delete mode 100644 rules/rules_release/lib/utils.mjs rename rules/rules_release/{ => release}/changesets_cli.mjs (100%) rename rules/rules_release/{ => release}/cli.mjs (81%) rename rules/rules_release/{ => release}/extensions.bzl (61%) create mode 100644 rules/rules_release/release/lib/actions/GenerateAction.mjs rename rules/rules_release/{ => release}/lib/actions/PublishAction.mjs (100%) rename rules/rules_release/{ => release}/lib/actions/VersionAction.mjs (100%) rename rules/rules_release/{ => release}/lib/repositories/ChangelogRepository.mjs (100%) rename rules/rules_release/{ => release}/lib/repositories/ChangesetRepository.mjs (77%) rename rules/rules_release/{ => release}/lib/repositories/ConfigRepository.mjs (75%) rename rules/rules_release/{ => release}/lib/repositories/PackageRepository.mjs (100%) rename rules/rules_release/{ => release}/lib/repositories/PublishRepository.mjs (100%) rename rules/rules_release/{ => release}/lib/repositories/ReleaseRepository.mjs (53%) rename rules/rules_release/{ => release}/lib/repositories/VersionRepository.mjs (100%) create mode 100644 rules/rules_release/release/lib/utils.mjs create mode 100644 rules/rules_release/release/repositories.bzl create mode 100644 rules/rules_release/release/repository_primary_deps.bzl create mode 100644 rules/rules_release/release/repository_secondary_deps.bzl delete mode 100644 rules/rules_release/repositories.bzl create mode 100644 rules/rules_release/tools/BUILD.bazel create mode 100644 rules/rules_release/tools/bazel_diff_change_cli.mjs create mode 100644 rules/rules_release/tools/defs.bzl create mode 100644 rules/rules_release/tools/lib/actions/BazelDiffChangeAction.mjs create mode 100644 rules/rules_release/tools/lib/repositories/BazelDiffRepository.mjs create mode 100644 rules/rules_release/tools/private/BUILD.bazel create mode 100644 rules/rules_release/tools/private/bazel_diff_release.bzl rename rules/rules_release/{release => tools}/private/publish_github_release.bzl (100%) diff --git a/.bazelignore b/.bazelignore index 631fe2879..b697b914e 100644 --- a/.bazelignore +++ b/.bazelignore @@ -1,4 +1,5 @@ # Ignore the convenience symlink due to VSCode issue + # fix described here https://github.com/bazelbuild/bazel/issues/10653 bazel-setup @@ -12,3 +13,8 @@ node_modules rules/rules_task rules/rules_release + +bazel-bin +bazel-out +bazel-testlogs +bazel-setup diff --git a/.bazelrc b/.bazelrc index ccf553edf..7d3c60255 100644 --- a/.bazelrc +++ b/.bazelrc @@ -8,8 +8,8 @@ build --java_runtime_version=remotejdk_11 build:quiet --ui_event_filters=-info --noshow_progress query:quiet --ui_event_filters=-info --noshow_progress -# Move the bazel symlinks outside of the project root to avoid the symlinks appearing in the volume mount -build --symlink_prefix=/tmp/bazel-setup- +# Disable the bazel symlinks +common --experimental_convenience_symlinks=ignore # Print warning messages about test suite being configured as too small/big test --test_verbose_timeout_warnings diff --git a/.changeset/rules_release-quick-cheetahs-thank-2.md b/.changeset/rules_release-quick-cheetahs-thank-2.md new file mode 100644 index 000000000..f9b656806 --- /dev/null +++ b/.changeset/rules_release-quick-cheetahs-thank-2.md @@ -0,0 +1,5 @@ +--- +"rules_release": major +--- + +BREAKING CHANGE: Extracted bazel-diff from release manager and introduced `bazel_diff_release` rule. diff --git a/.changeset/rules_release-quick-cheetahs-thank-3.md b/.changeset/rules_release-quick-cheetahs-thank-3.md new file mode 100644 index 000000000..907645d0c --- /dev/null +++ b/.changeset/rules_release-quick-cheetahs-thank-3.md @@ -0,0 +1,5 @@ +--- +"rules_release": patch +--- + +build: Updated release template to include WORKSPACE setup diff --git a/.changeset/rules_release-quick-cheetahs-thank-4.md b/.changeset/rules_release-quick-cheetahs-thank-4.md new file mode 100644 index 000000000..46246885b --- /dev/null +++ b/.changeset/rules_release-quick-cheetahs-thank-4.md @@ -0,0 +1,5 @@ +--- +"rules_release": major +--- + +BREAKING CHANGE: Move `bazel_diff_release` and `publish_github_release` from release/defs.bzl to tools/defs.bzl. diff --git a/.changeset/rules_release-quick-cheetahs-thank.md b/.changeset/rules_release-quick-cheetahs-thank.md new file mode 100644 index 000000000..f86e3870f --- /dev/null +++ b/.changeset/rules_release-quick-cheetahs-thank.md @@ -0,0 +1,5 @@ +--- +"rules_release": patch +--- + +test: Add integration test for a WORKSPACE setup diff --git a/.gitignore b/.gitignore index fa845a650..a758be502 100755 --- a/.gitignore +++ b/.gitignore @@ -10,10 +10,7 @@ infrastructure/kubernetes_config.yaml infrastructure/stacks/provisioner/Pulumi.dev.yaml # For Bazel -bazel-bin -bazel-out -bazel-setup -bazel-testlogs +bazel-* cache # Entries below this point are managed by Please (DO NOT EDIT) diff --git a/.vscode/settings.json b/.vscode/settings.json index c96d54aee..b22afdf73 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -17,5 +17,8 @@ }, "python.formatting.provider": "none", "devbox.autoShellOnTerminal": true, - "nix.enableLanguageServer": false + "nix.enableLanguageServer": false, + "files.exclude": { + "**/bazel-*": true + } } diff --git a/BUILD.bazel b/BUILD.bazel index 836a7a361..3cf674942 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -229,7 +229,6 @@ task( release_manager( name = "release_manager", - bazel_diff_args = "--fineGrainedHashExternalRepos=rules_release,rules_task", publish_cmds = [ ":push_git_changes", ], diff --git a/buildbuddy.yaml b/buildbuddy.yaml index bfc3116ad..616713d17 100644 --- a/buildbuddy.yaml +++ b/buildbuddy.yaml @@ -11,7 +11,7 @@ actions: - "*" bazel_commands: - bazel run //:setup_ci - - bazel test --keep_going //... @rules_task//... --config buildbuddy --config buildbuddy_rbe + - bazel test --keep_going //... @rules_task//... @rules_release//... --config buildbuddy --config buildbuddy_rbe - name: "Release all targets" user: buildbuddy diff --git a/rules/rules_release/.bazelignore b/rules/rules_release/.bazelignore index b512c09d4..183f7ec5b 100644 --- a/rules/rules_release/.bazelignore +++ b/rules/rules_release/.bazelignore @@ -1 +1,2 @@ -node_modules \ No newline at end of file +node_modules +examples/workspace \ No newline at end of file diff --git a/rules/rules_release/.bazelrc b/rules/rules_release/.bazelrc index c24798f72..ee5612556 100644 --- a/rules/rules_release/.bazelrc +++ b/rules/rules_release/.bazelrc @@ -1 +1,4 @@ -common --enable_bzlmod=true \ No newline at end of file +common --enable_bzlmod=true + +# Disable the bazel symlinks +common --experimental_convenience_symlinks=ignore \ No newline at end of file diff --git a/rules/rules_release/.bazelversion b/rules/rules_release/.bazelversion new file mode 120000 index 000000000..96cf94962 --- /dev/null +++ b/rules/rules_release/.bazelversion @@ -0,0 +1 @@ +../../.bazelversion \ No newline at end of file diff --git a/rules/rules_release/BUILD.bazel b/rules/rules_release/BUILD.bazel index ce8839844..a7c442cc6 100644 --- a/rules/rules_release/BUILD.bazel +++ b/rules/rules_release/BUILD.bazel @@ -1,9 +1,9 @@ -load("@npm//:defs.bzl", "npm_link_all_packages") -load("@aspect_rules_js//js:defs.bzl", "js_binary") -load("@rules_java//java:defs.bzl", "java_binary") load("@rules_task//task:defs.bzl", "cmd") -load("//release:defs.bzl", "publish_github_release", "release") +load("//tools:defs.bzl", "publish_github_release", release = "bazel_diff_release") load("@aspect_bazel_lib//lib:tar.bzl", "mtree_spec", "tar") +load("@npm//:defs.bzl", "npm_link_all_packages") + +npm_link_all_packages() package(default_visibility = ["//visibility:public"]) @@ -37,54 +37,13 @@ config_setting( constraint_values = _darwin_arm64, ) -npm_link_all_packages() - -java_binary( - name = "bazel-diff", - main_class = "com.bazel_diff.Main", - runtime_deps = ["@bazel_diff//jar"], -) - -js_binary( - name = "cli", - data = [ - "lib/actions/GenerateAction.mjs", - "lib/actions/PublishAction.mjs", - "lib/actions/VersionAction.mjs", - "lib/repositories/ChangelogRepository.mjs", - "lib/repositories/ChangesetRepository.mjs", - "lib/repositories/ConfigRepository.mjs", - "lib/repositories/PackageRepository.mjs", - "lib/repositories/PublishRepository.mjs", - "lib/repositories/ReleaseRepository.mjs", - "lib/repositories/TargetRepository.mjs", - "lib/repositories/VersionRepository.mjs", - "lib/utils.mjs", - "//:node_modules/@changesets/write", - "//:node_modules/commander", - "//:node_modules/zx", - ], - entry_point = "cli.mjs", - env = { - "WORKSPACE_NAME": repository_name().removeprefix("@"), - }, -) - -js_binary( - name = "changesets_cli", - data = [ - "//:node_modules/@changesets/changelog-github", - "//:node_modules/@changesets/cli", - "//:node_modules/zx", - ], - entry_point = "changesets_cli.mjs", -) - filegroup( name = "all_files", srcs = glob(["**/*"]) + [ "//release:all_files", "//release/private:all_files", + "//tools:all_files", + "//tools/private:all_files", ], ) @@ -126,6 +85,7 @@ genrule( ":release.version_changelog", ":release.version", ":release_archive", + "@examples_workspace//:WORKSPACE", ], outs = [ "github_release_template.txt", @@ -135,6 +95,9 @@ export VERSION="v$$(cat $(location :release.version))" export SHA=$$(shasum -a 256 $(location :release_archive) | awk '{print $$1}') export PREFIX="rules_release-$${VERSION}" export ARCHIVE="rules_release.tar.gz" +export WORKSPACE_CONTENT=$$(cat $(location @examples_workspace//:WORKSPACE)) +export SEARCH_TARGET_LINE="#### Generic for each workspace file" +export FILTERED_WORKSPACE_CONTENT=$$(echo "$$WORKSPACE_CONTENT" | sed "1,/$$SEARCH_TARGET_LINE/d") cat < $(OUTS) ## Using WORKSPACE: @@ -147,6 +110,8 @@ http_archive( sha256 = "$${SHA}", url = "https://github.com/vgijssel/setup/releases/download/$${PREFIX}/$${ARCHIVE}", ) + +$$FILTERED_WORKSPACE_CONTENT \\`\\`\\` EOF @@ -173,6 +138,7 @@ publish_github_release( release( name = "release", changelog_file = "CHANGELOG.md", + generate_hashes_extra_args = ["--fineGrainedHashExternalRepos=rules_release"], publish_cmds = [ ":publish_github_release", ], diff --git a/rules/rules_release/MODULE.bazel b/rules/rules_release/MODULE.bazel index eeb1d2303..f9b9a8ea7 100644 --- a/rules/rules_release/MODULE.bazel +++ b/rules/rules_release/MODULE.bazel @@ -8,7 +8,7 @@ module( version = "0.0.0", ) -non_module_dependencies = use_extension(":extensions.bzl", "non_module_dependencies") +non_module_dependencies = use_extension("//release:extensions.bzl", "non_module_dependencies") # ------------------------------------ platforms ------------------------------------ # bazel_dep(name = "platforms", version = "0.0.8") @@ -42,6 +42,9 @@ use_repo(non_module_dependencies, "onepassword_linux_amd64") use_repo(non_module_dependencies, "onepassword_darwin_arm64") +# ------------------------------------ examples ------------------------------------ # +use_repo(non_module_dependencies, "examples_workspace") + # ------------------------------------ rules_js ------------------------------------ # bazel_dep( name = "aspect_rules_js", @@ -81,3 +84,22 @@ local_path_override( module_name = "rules_task", path = "../rules_task", ) + +# ------------------------------------ rules_bazel_integration_test ------------------------------------ # +bazel_dep( + name = "rules_bazel_integration_test", + version = "0.20.0", +) + +bazel_binaries = use_extension( + "@rules_bazel_integration_test//:extensions.bzl", + "bazel_binaries", +) + +bazel_binaries.download(version_file = "//:.bazelversion") + +use_repo(bazel_binaries, "bazel_binaries") + +use_repo(bazel_binaries, "bazel_binaries_bazelisk") + +use_repo(bazel_binaries, "build_bazel_bazel_.bazelversion") diff --git a/rules/rules_release/examples/BUILD.bazel b/rules/rules_release/examples/BUILD.bazel new file mode 100644 index 000000000..9e095216e --- /dev/null +++ b/rules/rules_release/examples/BUILD.bazel @@ -0,0 +1,36 @@ +load("@bazel_binaries//:defs.bzl", "bazel_binaries") +load( + "@rules_bazel_integration_test//bazel_integration_test:defs.bzl", + "bazel_integration_test", + "default_test_runner", + "integration_test_utils", +) + +# if the tests are executed from setup repository then the path is ../../../rules_release~override +# if it's executed from the rules_release repository then the path is ../../ +rules_release_repository_path = "../../" if repository_name() == "@" else "../rules_release~override" + +rules_task_repository_path = "../rules_task~override" + +default_test_runner( + name = "workspace_test_runner", + bazel_cmds = [ + "info --override_repository=rules_release={} --override_repository=rules_task={}".format(rules_release_repository_path, rules_task_repository_path), + "test --override_repository=rules_release={} --override_repository=rules_task={} //...".format(rules_release_repository_path, rules_task_repository_path), + ], +) + +integration_test_tags = [tag for tag in integration_test_utils.DEFAULT_INTEGRATION_TEST_TAGS if tag != "manual"] + +bazel_integration_test( + name = "workspace_test", + bazel_version = bazel_binaries.versions.current, + tags = integration_test_tags, + test_runner = ":workspace_test_runner", + workspace_files = [ + "@examples_workspace//:all_files", + "//:all_files", + "@rules_task//:all_files", + ], + workspace_path = "workspace", +) diff --git a/rules/rules_release/examples/workspace/.bazelignore b/rules/rules_release/examples/workspace/.bazelignore new file mode 100644 index 000000000..a7bf8847e --- /dev/null +++ b/rules/rules_release/examples/workspace/.bazelignore @@ -0,0 +1,4 @@ +bazel-bin/ +bazel-out/ +bazel-testlogs/ +bazel-workspace/ \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/.bazelrc b/rules/rules_release/examples/workspace/.bazelrc new file mode 100644 index 000000000..6177ff91f --- /dev/null +++ b/rules/rules_release/examples/workspace/.bazelrc @@ -0,0 +1,5 @@ +# To ensure a hermetic environment for Java +build --java_runtime_version=remotejdk_11 + +# Disable the bazel symlinks +common --experimental_convenience_symlinks=ignore \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/.changeset/config.json b/rules/rules_release/examples/workspace/.changeset/config.json new file mode 100644 index 000000000..b1bbe8b6b --- /dev/null +++ b/rules/rules_release/examples/workspace/.changeset/config.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://unpkg.com/@changesets/config@2.3.1/schema.json", + "changelog": ["@changesets/changelog-github", { "repo": "vgijssel/setup" }], + "commit": false, + "fixed": [], + "linked": [], + "access": "restricted", + "baseBranch": "master", + "updateInternalDependencies": "patch", + "ignore": [], + "privatePackages": { + "version": true, + "tag": true + } +} diff --git a/rules/rules_release/examples/workspace/BUILD.bazel b/rules/rules_release/examples/workspace/BUILD.bazel new file mode 100644 index 000000000..9e03b041a --- /dev/null +++ b/rules/rules_release/examples/workspace/BUILD.bazel @@ -0,0 +1,80 @@ +load("@rules_release//release:defs.bzl", "release", "release_manager") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "all_files", + srcs = ["//tests:all_files"] + glob(["**/*"]), +) + +sh_binary( + name = "foo_changed", + srcs = [ + "foo_changed.sh", + ], +) + +sh_binary( + name = "foo_publish", + srcs = [ + "foo_publish.sh", + ], +) + +filegroup( + name = "foo_files", + srcs = [ + "foo.txt", + ], +) + +release( + name = "foo_release", + change_cmd = ":foo_changed", + changelog_file = "CHANGELOG_foo.md", + publish_cmds = [ + ":foo_publish", + ], + release_name = "foo", + version_file = "VERSION_foo.txt", +) + +sh_binary( + name = "bar_not_changed", + srcs = [ + "bar_not_changed.sh", + ], +) + +filegroup( + name = "bar_files", + srcs = [ + "bar.txt", + ], +) + +release( + name = "bar_release", + change_cmd = ":bar_not_changed", + changelog_file = "CHANGELOG_bar.md", + release_name = "bar", + version_file = "VERSION_bar.txt", +) + +sh_binary( + name = "final_publish", + srcs = [ + "final_publish.sh", + ], +) + +release_manager( + name = "release_manager", + publish_cmds = [ + ":final_publish", + ], + deps = [ + ":bar_release", + ":foo_release", + ], +) diff --git a/rules/rules_release/examples/workspace/CHANGELOG_bar.md b/rules/rules_release/examples/workspace/CHANGELOG_bar.md new file mode 100644 index 000000000..e69de29bb diff --git a/rules/rules_release/examples/workspace/CHANGELOG_foo.md b/rules/rules_release/examples/workspace/CHANGELOG_foo.md new file mode 100644 index 000000000..e69de29bb diff --git a/rules/rules_release/examples/workspace/VERSION_bar.txt b/rules/rules_release/examples/workspace/VERSION_bar.txt new file mode 100644 index 000000000..bd52db81d --- /dev/null +++ b/rules/rules_release/examples/workspace/VERSION_bar.txt @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/VERSION_foo.txt b/rules/rules_release/examples/workspace/VERSION_foo.txt new file mode 100644 index 000000000..bd52db81d --- /dev/null +++ b/rules/rules_release/examples/workspace/VERSION_foo.txt @@ -0,0 +1 @@ +0.0.0 \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/WORKSPACE b/rules/rules_release/examples/workspace/WORKSPACE new file mode 100644 index 000000000..d8f9f0f2b --- /dev/null +++ b/rules/rules_release/examples/workspace/WORKSPACE @@ -0,0 +1,25 @@ +#### Specificy for this particular repository +local_repository( + name = "rules_task", + path = "../../../rules_task", +) + +local_repository( + name = "rules_release", + path = "../../../rules_release", +) + +#### Generic for each workspace file +load("@rules_release//release:repositories.bzl", "rules_release_bazel_dependencies", "rules_release_dependencies") + +rules_release_bazel_dependencies() + +rules_release_dependencies() + +load("@rules_release//release:repository_primary_deps.bzl", "install_primary_deps") + +install_primary_deps() + +load("@rules_release//release:repository_secondary_deps.bzl", "install_secondary_deps") + +install_secondary_deps() diff --git a/rules/rules_release/examples/workspace/bar.txt b/rules/rules_release/examples/workspace/bar.txt new file mode 100644 index 000000000..ba0e162e1 --- /dev/null +++ b/rules/rules_release/examples/workspace/bar.txt @@ -0,0 +1 @@ +bar \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/bar_not_changed.sh b/rules/rules_release/examples/workspace/bar_not_changed.sh new file mode 100755 index 000000000..13c6752dc --- /dev/null +++ b/rules/rules_release/examples/workspace/bar_not_changed.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo false \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/final_publish.sh b/rules/rules_release/examples/workspace/final_publish.sh new file mode 100755 index 000000000..7b0eb6251 --- /dev/null +++ b/rules/rules_release/examples/workspace/final_publish.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "This runs after all other releases have been published." +echo "This can be used for example to push changes to git." \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/foo.txt b/rules/rules_release/examples/workspace/foo.txt new file mode 100644 index 000000000..191028156 --- /dev/null +++ b/rules/rules_release/examples/workspace/foo.txt @@ -0,0 +1 @@ +foo \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/foo_changed.sh b/rules/rules_release/examples/workspace/foo_changed.sh new file mode 100755 index 000000000..2cae34479 --- /dev/null +++ b/rules/rules_release/examples/workspace/foo_changed.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo true \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/foo_publish.sh b/rules/rules_release/examples/workspace/foo_publish.sh new file mode 100755 index 000000000..ed8e20614 --- /dev/null +++ b/rules/rules_release/examples/workspace/foo_publish.sh @@ -0,0 +1,4 @@ +#!/usr/bin/env bash +set -euo pipefail + +echo "Publishing foo!" \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/package.json b/rules/rules_release/examples/workspace/package.json new file mode 100644 index 000000000..3f5b62f64 --- /dev/null +++ b/rules/rules_release/examples/workspace/package.json @@ -0,0 +1,9 @@ +{ + "name": "examples_workspace", + "version": "0.0.0", + "private": true, + "dependencies": {}, + "workspaces": [ + "tmp/rules_release/packages/*" + ] +} diff --git a/rules/rules_release/examples/workspace/tests/BUILD.bazel b/rules/rules_release/examples/workspace/tests/BUILD.bazel new file mode 100644 index 000000000..b2664eb54 --- /dev/null +++ b/rules/rules_release/examples/workspace/tests/BUILD.bazel @@ -0,0 +1,37 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "all_files", + srcs = glob(["**/*"]), +) + +sh_library( + name = "unittest", + srcs = [ + "unittest.bash", + "unittest_utils.sh", + ], +) + +sh_test( + name = "test", + srcs = [ + "test.sh", + ], + args = [ + "$(location //:release_manager.generate)", + "$(location //:release_manager.version)", + "$(location //:release_manager.publish)", + ], + data = [ + # We need to include all files because the release manager currently assumes the + # package.json and .changeset directory are present. + "//:all_files", + "//:release_manager.generate", + "//:release_manager.publish", + "//:release_manager.version", + ], + deps = [ + ":unittest", + ], +) diff --git a/rules/rules_release/examples/workspace/tests/test.sh b/rules/rules_release/examples/workspace/tests/test.sh new file mode 100755 index 000000000..93128652a --- /dev/null +++ b/rules/rules_release/examples/workspace/tests/test.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +set -euo pipefail + +source ./tests/unittest.bash || exit 1 + +export GENERATE=$1 +export VERSION=$2 +export PUBLISH=$3 + +function convert_to_actual_file() { + local file=$1 + cat $file > $file.tmp + rm $file + mv $file.tmp $file +} + +# Convert the changelog and version into actual files, +# because the files referenced by the Bazel symlinks are readonly +convert_to_actual_file CHANGELOG_foo.md +convert_to_actual_file VERSION_foo.txt +convert_to_actual_file CHANGELOG_bar.md +convert_to_actual_file VERSION_bar.txt + +$GENERATE +$VERSION +$PUBLISH 2>&1 | tee publish_output.txt + +function test_foo_updated_version() { + assert_contains "0.0.1" "VERSION_foo.txt" +} + +function test_foo_changelog_contains_new_version() { + assert_contains "0.0.1" "CHANGELOG_foo.md" +} + +function test_foo_publish_is_called() { + assert_contains "Publishing foo!" "publish_output.txt" +} + +function test_final_publish_is_called() { + assert_contains "This runs after all other releases have been published." "publish_output.txt" +} + +function test_bar_version_did_not_update() { + assert_contains "0.0.0" "VERSION_bar.txt" +} + +function test_bar_changelog_contains_new_version() { + assert_not_contains "0.0.0" "CHANGELOG_bar.md" +} + +run_suite "workspace test suite" \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/tests/unittest.bash b/rules/rules_release/examples/workspace/tests/unittest.bash new file mode 100644 index 000000000..27935fab1 --- /dev/null +++ b/rules/rules_release/examples/workspace/tests/unittest.bash @@ -0,0 +1,846 @@ +#!/bin/bash +# +# Copied from https://github.com/bazelbuild/bazel/blob/master/src/test/shell/unittest.bash +# Copyright 2015 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# Common utility file for Bazel shell tests +# +# unittest.bash: a unit test framework in Bash. +# +# A typical test suite looks like so: +# +# ------------------------------------------------------------------------ +# #!/bin/bash +# +# source path/to/unittest.bash || exit 1 +# +# # Test that foo works. +# function test_foo() { +# foo >$TEST_log || fail "foo failed"; +# expect_log "blah" "Expected to see 'blah' in output of 'foo'." +# } +# +# # Test that bar works. +# function test_bar() { +# bar 2>$TEST_log || fail "bar failed"; +# expect_not_log "ERROR" "Unexpected error from 'bar'." +# ... +# assert_equals $x $y +# } +# +# run_suite "Test suite for blah" +# ------------------------------------------------------------------------ +# +# Each test function is considered to pass iff fail() is not called +# while it is active. fail() may be called directly, or indirectly +# via other assertions such as expect_log(). run_suite must be called +# at the very end. +# +# A test suite may redefine functions "set_up" and/or "tear_down"; +# these functions are executed before and after each test function, +# respectively. Similarly, "cleanup" and "timeout" may be redefined, +# and these function are called upon exit (of any kind) or a timeout. +# +# The user can pass --test_filter to blaze test to select specific tests +# to run with Bash globs. A union of tests matching any of the provided globs +# will be run. Additionally the user may define TESTS=(test_foo test_bar ...) to +# specify a subset of test functions to execute, for example, a working set +# during debugging. By default, all functions called test_* will be executed. +# +# This file provides utilities for assertions over the output of a +# command. The output of the command under test is directed to the +# file $TEST_log, and then the expect_log* assertions can be used to +# test for the presence of certain regular expressions in that file. +# +# The test framework is responsible for restoring the original working +# directory before each test. +# +# The order in which test functions are run is not defined, so it is +# important that tests clean up after themselves. +# +# Each test will be run in a new subshell. +# +# Functions named __* are not intended for use by clients. +# +# This framework implements the "test sharding protocol". +# + +[[ -n "$BASH_VERSION" ]] || + { echo "unittest.bash only works with bash!" >&2; exit 1; } + +export BAZEL_SHELL_TEST=1 + +DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) + +# Load the environment support utilities. +source "${DIR}/unittest_utils.sh" || { echo "unittest_utils.sh not found" >&2; exit 1; } + +#### Global variables: + +TEST_name="" # The name of the current test. + +TEST_log=$TEST_TMPDIR/log # The log file over which the + # expect_log* assertions work. Must + # be absolute to be robust against + # tests invoking 'cd'! + +TEST_passed="true" # The result of the current test; + # failed assertions cause this to + # become false. + +# These variables may be overridden by the test suite: + +TESTS=() # A subset or "working set" of test + # functions that should be run. By + # default, all tests called test_* are + # run. + +_TEST_FILTERS=() # List of globs to use to filter the tests. + # If non-empty, all tests matching at least one + # of the globs are run and test list provided in + # the arguments is ignored if present. + +__in_tear_down=0 # Indicates whether we are in `tear_down` phase + # of test. Used to avoid re-entering `tear_down` + # on failures within it. + +if (( $# > 0 )); then + ( + IFS=':' + echo "WARNING: Passing test names in arguments (--test_arg) is deprecated, please use --test_filter='$*' instead." >&2 + ) + + # Legacy behavior is to ignore missing regexp, but with errexit + # the following line fails without || true. + # TODO(dmarting): maybe we should revisit the way of selecting + # test with that framework (use Bazel's environment variable instead). + TESTS=($(for i in "$@"; do echo $i; done | grep ^test_ || true)) + if (( ${#TESTS[@]} == 0 )); then + echo "WARNING: Arguments do not specify tests!" >&2 + fi +fi +# TESTBRIDGE_TEST_ONLY contains the value of --test_filter, if any. We want to +# preferentially use that instead of $@ to determine which tests to run. +if [[ ${TESTBRIDGE_TEST_ONLY:-} != "" ]]; then + if (( ${#TESTS[@]} != 0 )); then + echo "WARNING: Both --test_arg and --test_filter specified, ignoring --test_arg" >&2 + TESTS=() + fi + # Split TESTBRIDGE_TEST_ONLY on colon and store it in `_TEST_FILTERS` array. + IFS=':' read -r -a _TEST_FILTERS <<< "$TESTBRIDGE_TEST_ONLY" +fi + +TEST_verbose="true" # Whether or not to be verbose. A + # command; "true" or "false" are + # acceptable. The default is: true. + +TEST_script="$0" # Full path to test script +# Check if the script path is absolute, if not prefix the PWD. +if [[ ! "$TEST_script" = /* ]]; then + TEST_script="${PWD}/$0" +fi + + +#### Internal functions + +function __show_log() { + echo "-- Test log: -----------------------------------------------------------" + [[ -e $TEST_log ]] && cat "$TEST_log" || echo "(Log file did not exist.)" + echo "------------------------------------------------------------------------" +} + +# Usage: __pad <pad-char> +# Print $title padded to 80 columns with $pad_char. +function __pad() { + local title=$1 + local pad=$2 + # Ignore the subshell error -- `head` closes the fd before reading to the + # end, therefore the subshell will get SIGPIPE while stuck in `write`. + { + echo -n "${pad}${pad} ${title} " + printf "%80s" " " | tr ' ' "$pad" + } | head -c 80 || true + echo +} + +#### Exported functions + +# Usage: init_test ... +# Deprecated. Has no effect. +function init_test() { + : +} + + +# Usage: set_up +# Called before every test function. May be redefined by the test suite. +function set_up() { + : +} + +# Usage: tear_down +# Called after every test function. May be redefined by the test suite. +function tear_down() { + : +} + +# Usage: cleanup +# Called upon eventual exit of the test suite. May be redefined by +# the test suite. +function cleanup() { + : +} + +# Usage: timeout +# Called upon early exit from a test due to timeout. +function timeout() { + : +} + +# Usage: testenv_set_up +# Called prior to set_up. For use by testenv.sh. +function testenv_set_up() { + : +} + +# Usage: testenv_tear_down +# Called after tear_down. For use by testenv.sh. +function testenv_tear_down() { + : +} + +# Usage: fail <message> [<message> ...] +# Print failure message with context information, and mark the test as +# a failure. The context includes a stacktrace including the longest sequence +# of calls outside this module. (We exclude the top and bottom portions of +# the stack because they just add noise.) Also prints the contents of +# $TEST_log. +function fail() { + __show_log >&2 + echo "${TEST_name} FAILED: $*." >&2 + # Keep the original error message if we fail in `tear_down` after a failure. + [[ "${TEST_passed}" == "true" ]] && echo "$@" >"$TEST_TMPDIR"/__fail + TEST_passed="false" + __show_stack + # Cleanup as we are leaving the subshell now + __run_tear_down_after_failure + exit 1 +} + +function __run_tear_down_after_failure() { + # Skip `tear_down` after a failure in `tear_down` to prevent infinite + # recursion. + (( __in_tear_down )) && return + __in_tear_down=1 + echo -e "\nTear down:\n" >&2 + tear_down + testenv_tear_down +} + +# Usage: warn <message> +# Print a test warning with context information. +# The context includes a stacktrace including the longest sequence +# of calls outside this module. (We exclude the top and bottom portions of +# the stack because they just add noise.) +function warn() { + __show_log >&2 + echo "${TEST_name} WARNING: $1." >&2 + __show_stack + + if [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then + echo "${TEST_name} WARNING: $1." >> "$TEST_WARNINGS_OUTPUT_FILE" + fi +} + +# Usage: show_stack +# Prints the portion of the stack that does not belong to this module, +# i.e. the user's code that called a failing assertion. Stack may not +# be available if Bash is reading commands from stdin; an error is +# printed in that case. +__show_stack() { + local i=0 + local trace_found=0 + + # Skip over active calls within this module: + while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} == "${BASH_SOURCE[0]}" ]]; do + (( ++i )) + done + + # Show all calls until the next one within this module (typically run_suite): + while (( i < ${#FUNCNAME[@]} )) && [[ ${BASH_SOURCE[i]:-} != "${BASH_SOURCE[0]}" ]]; do + # Read online docs for BASH_LINENO to understand the strange offset. + # Undefined can occur in the BASH_SOURCE stack apparently when one exits from a subshell + echo "${BASH_SOURCE[i]:-"Unknown"}:${BASH_LINENO[i - 1]:-"Unknown"}: in call to ${FUNCNAME[i]:-"Unknown"}" >&2 + (( ++i )) + trace_found=1 + done + + (( trace_found )) || echo "[Stack trace not available]" >&2 +} + +# Usage: expect_log <regexp> [error-message] +# Asserts that $TEST_log matches regexp. Prints the contents of +# $TEST_log and the specified (optional) error message otherwise, and +# returns non-zero. +function expect_log() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found} + grep -sq -- "$pattern" $TEST_log && return 0 + + fail "$message" + return 1 +} + +# Usage: expect_log_warn <regexp> [error-message] +# Warns if $TEST_log does not match regexp. Prints the contents of +# $TEST_log and the specified (optional) error message on mismatch. +function expect_log_warn() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found} + grep -sq -- "$pattern" $TEST_log && return 0 + + warn "$message" + return 1 +} + +# Usage: expect_log_once <regexp> [error-message] +# Asserts that $TEST_log contains one line matching <regexp>. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_once() { + local pattern=$1 + local message=${2:-Expected regexp "$pattern" not found exactly once} + expect_log_n "$pattern" 1 "$message" +} + +# Usage: expect_log_n <regexp> <count> [error-message] +# Asserts that $TEST_log contains <count> lines matching <regexp>. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_n() { + local pattern=$1 + local expectednum=${2:-1} + local message=${3:-Expected regexp "$pattern" not found exactly $expectednum times} + local count=$(grep -sc -- "$pattern" $TEST_log) + (( count == expectednum )) && return 0 + fail "$message" + return 1 +} + +# Usage: expect_not_log <regexp> [error-message] +# Asserts that $TEST_log does not match regexp. Prints the contents +# of $TEST_log and the specified (optional) error message otherwise, and +# returns non-zero. +function expect_not_log() { + local pattern=$1 + local message=${2:-Unexpected regexp "$pattern" found} + grep -sq -- "$pattern" $TEST_log || return 0 + + fail "$message" + return 1 +} + +# Usage: expect_query_targets <arguments> +# Checks that log file contains exactly the targets in the argument list. +function expect_query_targets() { + for arg in "$@"; do + expect_log_once "^$arg$" + done + +# Checks that the number of lines started with '//' equals to the number of +# arguments provided. + expect_log_n "^//[^ ]*$" $# +} + +# Usage: expect_log_with_timeout <regexp> <timeout> [error-message] +# Waits for the given regexp in the $TEST_log for up to timeout seconds. +# Prints the contents of $TEST_log and the specified (optional) +# error message otherwise, and returns non-zero. +function expect_log_with_timeout() { + local pattern=$1 + local timeout=$2 + local message=${3:-Regexp "$pattern" not found in "$timeout" seconds} + local count=0 + while (( count < timeout )); do + grep -sq -- "$pattern" "$TEST_log" && return 0 + let count=count+1 + sleep 1 + done + + grep -sq -- "$pattern" "$TEST_log" && return 0 + fail "$message" + return 1 +} + +# Usage: expect_cmd_with_timeout <expected> <cmd> [timeout] +# Repeats the command once a second for up to timeout seconds (10s by default), +# until the output matches the expected value. Fails and returns 1 if +# the command does not return the expected value in the end. +function expect_cmd_with_timeout() { + local expected="$1" + local cmd="$2" + local timeout=${3:-10} + local count=0 + while (( count < timeout )); do + local actual="$($cmd)" + [[ "$expected" == "$actual" ]] && return 0 + (( ++count )) + sleep 1 + done + + [[ "$expected" == "$actual" ]] && return 0 + fail "Expected '${expected}' within ${timeout}s, was '${actual}'" + return 1 +} + +# Usage: assert_one_of <expected_list>... <actual> +# Asserts that actual is one of the items in expected_list +# +# Example: +# local expected=( "foo", "bar", "baz" ) +# assert_one_of $expected $actual +function assert_one_of() { + local args=("$@") + local last_arg_index=$((${#args[@]} - 1)) + local actual=${args[last_arg_index]} + unset args[last_arg_index] + for expected_item in "${args[@]}"; do + [[ "$expected_item" == "$actual" ]] && return 0 + done; + + fail "Expected one of '${args[*]}', was '$actual'" + return 1 +} + +# Usage: assert_not_one_of <expected_list>... <actual> +# Asserts that actual is not one of the items in expected_list +# +# Example: +# local unexpected=( "foo", "bar", "baz" ) +# assert_not_one_of $unexpected $actual +function assert_not_one_of() { + local args=("$@") + local last_arg_index=$((${#args[@]} - 1)) + local actual=${args[last_arg_index]} + unset args[last_arg_index] + for expected_item in "${args[@]}"; do + if [[ "$expected_item" == "$actual" ]]; then + fail "'${args[*]}' contains '$actual'" + return 1 + fi + done; + + return 0 +} + +# Usage: assert_equals <expected> <actual> +# Asserts [[ expected == actual ]]. +function assert_equals() { + local expected=$1 actual=$2 + [[ "$expected" == "$actual" ]] && return 0 + + fail "Expected '$expected', was '$actual'" + return 1 +} + +# Usage: assert_not_equals <unexpected> <actual> +# Asserts [[ unexpected != actual ]]. +function assert_not_equals() { + local unexpected=$1 actual=$2 + [[ "$unexpected" != "$actual" ]] && return 0; + + fail "Expected not '${unexpected}', was '${actual}'" + return 1 +} + +# Usage: assert_contains <regexp> <file> [error-message] +# Asserts that file matches regexp. Prints the contents of +# file and the specified (optional) error message otherwise, and +# returns non-zero. +function assert_contains() { + local pattern=$1 + local file=$2 + local message=${3:-Expected regexp "$pattern" not found in "$file"} + grep -sq -- "$pattern" "$file" && return 0 + + cat "$file" >&2 + fail "$message" + return 1 +} + +# Usage: assert_not_contains <regexp> <file> [error-message] +# Asserts that file does not match regexp. Prints the contents of +# file and the specified (optional) error message otherwise, and +# returns non-zero. +function assert_not_contains() { + local pattern=$1 + local file=$2 + local message=${3:-Expected regexp "$pattern" found in "$file"} + + if [[ -f "$file" ]]; then + grep -sq -- "$pattern" "$file" || return 0 + else + fail "$file is not a file: $message" + return 1 + fi + + cat "$file" >&2 + fail "$message" + return 1 +} + +function assert_contains_n() { + local pattern=$1 + local expectednum=${2:-1} + local file=$3 + local message=${4:-Expected regexp "$pattern" not found exactly $expectednum times} + local count + if [[ -f "$file" ]]; then + count=$(grep -sc -- "$pattern" "$file") + else + fail "$file is not a file: $message" + return 1 + fi + (( count == expectednum )) && return 0 + + cat "$file" >&2 + fail "$message" + return 1 +} + +# Updates the global variables TESTS if +# sharding is enabled, i.e. ($TEST_TOTAL_SHARDS > 0). +function __update_shards() { + [[ -z "${TEST_TOTAL_SHARDS-}" ]] && return 0 + + (( TEST_TOTAL_SHARDS > 0 )) || + { echo "Invalid total shards ${TEST_TOTAL_SHARDS}" >&2; exit 1; } + + (( TEST_SHARD_INDEX < 0 || TEST_SHARD_INDEX >= TEST_TOTAL_SHARDS )) && + { echo "Invalid shard ${TEST_SHARD_INDEX}" >&2; exit 1; } + + IFS=$'\n' read -rd $'\0' -a TESTS < <( + for test in "${TESTS[@]}"; do echo "$test"; done | + awk "NR % ${TEST_TOTAL_SHARDS} == ${TEST_SHARD_INDEX}" && + echo -en '\0') + + [[ -z "${TEST_SHARD_STATUS_FILE-}" ]] || touch "$TEST_SHARD_STATUS_FILE" +} + +# Usage: __test_terminated <signal-number> +# Handler that is called when the test terminated unexpectedly +function __test_terminated() { + __show_log >&2 + echo "$TEST_name FAILED: terminated by signal $1." >&2 + TEST_passed="false" + __show_stack + timeout + exit 1 +} + +# Usage: __test_terminated_err +# Handler that is called when the test terminated unexpectedly due to "errexit". +function __test_terminated_err() { + # When a subshell exits due to signal ERR, its parent shell also exits, + # thus the signal handler is called recursively and we print out the + # error message and stack trace multiple times. We're only interested + # in the first one though, as it contains the most information, so ignore + # all following. + if [[ -f $TEST_TMPDIR/__err_handled ]]; then + exit 1 + fi + __show_log >&2 + if [[ ! -z "$TEST_name" ]]; then + echo -n "$TEST_name " >&2 + fi + echo "FAILED: terminated because this command returned a non-zero status:" >&2 + touch $TEST_TMPDIR/__err_handled + TEST_passed="false" + __show_stack + # If $TEST_name is still empty, the test suite failed before we even started + # to run tests, so we shouldn't call tear_down. + if [[ -n "$TEST_name" ]]; then + __run_tear_down_after_failure + fi + exit 1 +} + +# Usage: __trap_with_arg <handler> <signals ...> +# Helper to install a trap handler for several signals preserving the signal +# number, so that the signal number is available to the trap handler. +function __trap_with_arg() { + func="$1" ; shift + for sig ; do + trap "$func $sig" "$sig" + done +} + +# Usage: <node> <block> +# Adds the block to the given node in the report file. Quotes in the in +# arguments need to be escaped. +function __log_to_test_report() { + local node="$1" + local block="$2" + if [[ ! -e "$XML_OUTPUT_FILE" ]]; then + local xml_header='<?xml version="1.0" encoding="UTF-8"?>' + echo "${xml_header}<testsuites></testsuites>" > "$XML_OUTPUT_FILE" + fi + + # replace match on node with block and match + # replacement expression only needs escaping for quotes + perl -e "\ +\$input = @ARGV[0]; \ +\$/=undef; \ +open FILE, '+<$XML_OUTPUT_FILE'; \ +\$content = <FILE>; \ +if (\$content =~ /($node.*)\$/) { \ + seek FILE, 0, 0; \ + print FILE \$\` . \$input . \$1; \ +}; \ +close FILE" "$block" +} + +# Usage: <total> <passed> +# Adds the test summaries to the xml nodes. +function __finish_test_report() { + local suite_name="$1" + local total="$2" + local passed="$3" + local failed=$((total - passed)) + + # Update the xml output with the suite name and total number of + # passed/failed tests. + cat "$XML_OUTPUT_FILE" | \ + sed \ + "s/<testsuites>/<testsuites tests=\"$total\" failures=\"0\" errors=\"$failed\">/" | \ + sed \ + "s/<testsuite>/<testsuite name=\"${suite_name}\" tests=\"$total\" failures=\"0\" errors=\"$failed\">/" \ + > "${XML_OUTPUT_FILE}.bak" + + rm -f "$XML_OUTPUT_FILE" + mv "${XML_OUTPUT_FILE}.bak" "$XML_OUTPUT_FILE" +} + +# Multi-platform timestamp function +UNAME=$(uname -s | tr 'A-Z' 'a-z') +if [[ "$UNAME" == "linux" ]] || [[ "$UNAME" =~ msys_nt* ]]; then + function timestamp() { + echo $(($(date +%s%N)/1000000)) + } +else + function timestamp() { + # macOS and BSDs do not have %N, so Python is the best we can do. + # LC_ALL=C works around python 3.8 and 3.9 crash on macOS when the + # filesystem encoding is unspecified (e.g. when LANG=en_US). + local PYTHON=python + command -v python3 &> /dev/null && PYTHON=python3 + LC_ALL=C "${PYTHON}" -c 'import time; print(int(round(time.time() * 1000)))' + } +fi + +function get_run_time() { + local ts_start=$1 + local ts_end=$2 + run_time_ms=$((ts_end - ts_start)) + echo $((run_time_ms / 1000)).${run_time_ms: -3} +} + +# Usage: run_tests <suite-comment> +# Must be called from the end of the user's test suite. +# Calls exit with zero on success, non-zero otherwise. +function run_suite() { + local message="$1" + # The name of the suite should be the script being run, which + # will be the filename with the ".sh" extension removed. + local suite_name="$(basename "$0")" + + echo >&2 + echo "$message" >&2 + echo >&2 + + __log_to_test_report "<\/testsuites>" "<testsuite></testsuite>" + + local total=0 + local passed=0 + + atexit "cleanup" + + # If the user didn't specify an explicit list of tests (e.g. a + # working set), use them all. + if (( ${#TESTS[@]} == 0 )); then + # Even if there aren't any tests, this needs to succeed. + local all_tests=() + IFS=$'\n' read -d $'\0' -ra all_tests < <( + declare -F | awk '{print $3}' | grep ^test_ || true; echo -en '\0') + + if (( "${#_TEST_FILTERS[@]}" == 0 )); then + # Use ${array[@]+"${array[@]}"} idiom to avoid errors when running with + # Bash version <= 4.4 with `nounset` when `all_tests` is empty ( + # https://github.com/bminor/bash/blob/a0c0a00fc419b7bc08202a79134fcd5bc0427071/CHANGES#L62-L63). + TESTS=("${all_tests[@]+${all_tests[@]}}") + else + for t in "${all_tests[@]+${all_tests[@]}}"; do + local matches=0 + for f in "${_TEST_FILTERS[@]}"; do + # We purposely want to glob match. + # shellcheck disable=SC2053 + [[ "$t" = $f ]] && matches=1 && break + done + if (( matches )); then + TESTS+=("$t") + fi + done + fi + + elif [[ -n "${TEST_WARNINGS_OUTPUT_FILE:-}" ]]; then + if grep -q "TESTS=" "$TEST_script" ; then + echo "TESTS variable overridden in sh_test. Please remove before submitting" \ + >> "$TEST_WARNINGS_OUTPUT_FILE" + fi + fi + + # Reset TESTS in the common case where it contains a single empty string. + if [[ -z "${TESTS[*]-}" ]]; then + TESTS=() + fi + local original_tests_size=${#TESTS[@]} + + __update_shards + + if [[ "${#TESTS[@]}" -ne 0 ]]; then + for TEST_name in "${TESTS[@]}"; do + >"$TEST_log" # Reset the log. + TEST_passed="true" + + (( ++total )) + if [[ "$TEST_verbose" == "true" ]]; then + date >&2 + __pad "$TEST_name" '*' >&2 + fi + + local run_time="0.0" + rm -f "${TEST_TMPDIR}"/{__ts_start,__ts_end} + + if [[ "$(type -t "$TEST_name")" == function ]]; then + # Save exit handlers eventually set. + local SAVED_ATEXIT="$ATEXIT"; + ATEXIT= + + # Run test in a subshell. + rm -f "${TEST_TMPDIR}"/__err_handled + __trap_with_arg __test_terminated INT KILL PIPE TERM ABRT FPE ILL QUIT SEGV + + # Remember -o pipefail value and disable it for the subshell result + # collection. + if [[ "${SHELLOPTS}" =~ (^|:)pipefail(:|$) ]]; then + local __opt_switch=-o + else + local __opt_switch=+o + fi + set +o pipefail + ( + set "${__opt_switch}" pipefail + # if errexit is enabled, make sure we run cleanup and collect the log. + if [[ "$-" = *e* ]]; then + set -E + trap __test_terminated_err ERR + fi + timestamp >"${TEST_TMPDIR}"/__ts_start + testenv_set_up + set_up + eval "$TEST_name" + __in_tear_down=1 + tear_down + testenv_tear_down + timestamp >"${TEST_TMPDIR}"/__ts_end + test "$TEST_passed" == "true" + ) 2>&1 | tee "${TEST_TMPDIR}"/__log + # Note that tee will prevent the control flow continuing if the test + # spawned any processes which are still running and have not closed + # their stdout. + + test_subshell_status=${PIPESTATUS[0]} + set "${__opt_switch}" pipefail + if (( test_subshell_status != 0 )); then + TEST_passed="false" + # Ensure that an end time is recorded in case the test subshell + # terminated prematurely. + [[ -f "$TEST_TMPDIR"/__ts_end ]] || timestamp >"$TEST_TMPDIR"/__ts_end + fi + + # Calculate run time for the testcase. + local ts_start + ts_start=$(<"${TEST_TMPDIR}"/__ts_start) + local ts_end + ts_end=$(<"${TEST_TMPDIR}"/__ts_end) + run_time=$(get_run_time $ts_start $ts_end) + + # Eventually restore exit handlers. + if [[ -n "$SAVED_ATEXIT" ]]; then + ATEXIT="$SAVED_ATEXIT" + trap "$ATEXIT" EXIT + fi + else # Bad test explicitly specified in $TESTS. + fail "Not a function: '$TEST_name'" + fi + + local testcase_tag="" + + local red='\033[0;31m' + local green='\033[0;32m' + local no_color='\033[0m' + + if [[ "$TEST_verbose" == "true" ]]; then + echo >&2 + fi + + if [[ "$TEST_passed" == "true" ]]; then + if [[ "$TEST_verbose" == "true" ]]; then + echo -e "${green}PASSED${no_color}: ${TEST_name}" >&2 + fi + (( ++passed )) + testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"></testcase>" + else + echo -e "${red}FAILED${no_color}: ${TEST_name}" >&2 + # end marker in CDATA cannot be escaped, we need to split the CDATA sections + log=$(sed 's/]]>/]]>]]><![CDATA[/g' "${TEST_TMPDIR}"/__log) + fail_msg=$(cat "${TEST_TMPDIR}"/__fail 2> /dev/null || echo "No failure message") + # Replacing '&' with '&', '<' with '<', '>' with '>', and '"' with '"' + escaped_fail_msg=$(echo "$fail_msg" | sed 's/&/\&/g' | sed 's/</\</g' | sed 's/>/\>/g' | sed 's/"/\"/g') + testcase_tag="<testcase name=\"${TEST_name}\" status=\"run\" time=\"${run_time}\" classname=\"\"><error message=\"${escaped_fail_msg}\"><![CDATA[${log}]]></error></testcase>" + fi + + if [[ "$TEST_verbose" == "true" ]]; then + echo >&2 + fi + __log_to_test_report "<\/testsuite>" "$testcase_tag" + done + fi + + __finish_test_report "$suite_name" $total $passed + __pad "${passed} / ${total} tests passed." '*' >&2 + if (( original_tests_size == 0 )); then + __pad "No tests found." '*' + exit 1 + elif (( total != passed )); then + __pad "There were errors." '*' >&2 + exit 1 + elif (( total == 0 )); then + __pad "No tests executed due to sharding. Check your test's shard_count." '*' + __pad "Succeeding anyway." '*' + fi + + exit 0 +} \ No newline at end of file diff --git a/rules/rules_release/examples/workspace/tests/unittest_utils.sh b/rules/rules_release/examples/workspace/tests/unittest_utils.sh new file mode 100644 index 000000000..bec75ee79 --- /dev/null +++ b/rules/rules_release/examples/workspace/tests/unittest_utils.sh @@ -0,0 +1,182 @@ +# Copied from https://github.com/bazelbuild/bazel/blob/master/src/test/shell/unittest_utils.sh +# Copyright 2020 The Bazel Authors. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Support for unittest.bash + +#### Set up the test environment. + +set -euo pipefail + +cat_jvm_log () { + if [[ "$log_content" =~ \ + "(error code:".*", error message: '".*"', log file: '"(.*)"')" ]]; then + echo >&2 + echo "Content of ${BASH_REMATCH[1]}:" >&2 + cat "${BASH_REMATCH[1]}" >&2 + fi +} + +# Print message in "$1" then exit with status "$2" +die () { + # second argument is optional, defaulting to 1 + local status_code=${2:-1} + # Stop capturing stdout/stderr, and dump captured output + if [[ "$CAPTURED_STD_ERR" -ne 0 || "$CAPTURED_STD_OUT" -ne 0 ]]; then + restore_outputs + if [[ "$CAPTURED_STD_OUT" -ne 0 ]]; then + cat "${TEST_TMPDIR}/captured.out" + CAPTURED_STD_OUT=0 + fi + if [[ "$CAPTURED_STD_ERR" -ne 0 ]]; then + cat "${TEST_TMPDIR}/captured.err" 1>&2 + cat_jvm_log "$(cat "${TEST_TMPDIR}/captured.err")" + CAPTURED_STD_ERR=0 + fi + fi + + if [[ -n "${1-}" ]] ; then + echo "$1" 1>&2 + fi + if [[ -n "${BASH-}" ]]; then + local caller_n=0 + while [[ $caller_n -lt 4 ]] && \ + caller_out=$(caller $caller_n 2>/dev/null); do + test $caller_n -eq 0 && echo "CALLER stack (max 4):" + echo " $caller_out" + let caller_n=caller_n+1 + done 1>&2 + fi + if [[ -n "${status_code}" && "${status_code}" -ne 0 ]]; then + exit "$status_code" + else + exit 1 + fi +} + +# Print message in "$1" then record that a non-fatal error occurred in +# ERROR_COUNT +ERROR_COUNT="${ERROR_COUNT:-0}" +error () { + if [[ -n "$1" ]] ; then + echo "$1" 1>&2 + fi + ERROR_COUNT=$(($ERROR_COUNT + 1)) +} + +# Die if "$1" != "$2", print $3 as death reason +check_eq () { + [[ "$1" = "$2" ]] || die "Check failed: '$1' == '$2' ${3:+ ($3)}" +} + +# Die if "$1" == "$2", print $3 as death reason +check_ne () { + [[ "$1" != "$2" ]] || die "Check failed: '$1' != '$2' ${3:+ ($3)}" +} + +# The structure of the following if statements is such that if '[[' fails +# (e.g., a non-number was passed in) then the check will fail. + +# Die if "$1" > "$2", print $3 as death reason +check_le () { + [[ "$1" -gt "$2" ]] || die "Check failed: '$1' <= '$2' ${3:+ ($3)}" +} + +# Die if "$1" >= "$2", print $3 as death reason +check_lt () { + [[ "$1" -lt "$2" ]] || die "Check failed: '$1' < '$2' ${3:+ ($3)}" +} + +# Die if "$1" < "$2", print $3 as death reason +check_ge () { + [[ "$1" -ge "$2" ]] || die "Check failed: '$1' >= '$2' ${3:+ ($3)}" +} + +# Die if "$1" <= "$2", print $3 as death reason +check_gt () { + [[ "$1" -gt "$2" ]] || die "Check failed: '$1' > '$2' ${3:+ ($3)}" +} + +# Die if $2 !~ $1; print $3 as death reason +check_match () +{ + expr match "$2" "$1" >/dev/null || \ + die "Check failed: '$2' does not match regex '$1' ${3:+ ($3)}" +} + +# Run command "$1" at exit. Like "trap" but multiple atexits don't +# overwrite each other. Will break if someone does call trap +# directly. So, don't do that. +ATEXIT="${ATEXIT-}" +atexit () { + if [[ -z "$ATEXIT" ]]; then + ATEXIT="$1" + else + ATEXIT="$1 ; $ATEXIT" + fi + trap "$ATEXIT" EXIT +} + +## TEST_TMPDIR +if [[ -z "${TEST_TMPDIR:-}" ]]; then + export TEST_TMPDIR="$(mktemp -d ${TMPDIR:-/tmp}/bazel-test.XXXXXXXX)" +fi +if [[ ! -e "${TEST_TMPDIR}" ]]; then + mkdir -p -m 0700 "${TEST_TMPDIR}" + # Clean TEST_TMPDIR on exit + atexit "rm -fr ${TEST_TMPDIR}" +fi + +# Functions to compare the actual output of a test to the expected +# (golden) output. +# +# Usage: +# capture_test_stdout +# ... do something ... +# diff_test_stdout "$TEST_SRCDIR/path/to/golden.out" + +# Redirect a file descriptor to a file. +CAPTURED_STD_OUT="${CAPTURED_STD_OUT:-0}" +CAPTURED_STD_ERR="${CAPTURED_STD_ERR:-0}" + +capture_test_stdout () { + exec 3>&1 # Save stdout as fd 3 + exec 4>"${TEST_TMPDIR}/captured.out" + exec 1>&4 + CAPTURED_STD_OUT=1 +} + +capture_test_stderr () { + exec 6>&2 # Save stderr as fd 6 + exec 7>"${TEST_TMPDIR}/captured.err" + exec 2>&7 + CAPTURED_STD_ERR=1 +} + +# Force XML_OUTPUT_FILE to an existing path +if [[ -z "${XML_OUTPUT_FILE:-}" ]]; then + XML_OUTPUT_FILE=${TEST_TMPDIR}/output.xml +fi + +# Functions to provide easy access to external repository outputs in the sibling +# repository layout. +# +# Usage: +# bin_dir <repository name> +# genfiles_dir <repository name> +# testlogs_dir <repository name> + +testlogs_dir() { + echo $(bazel info bazel-testlogs | sed "s|bazel-out|bazel-out/$1|") +} \ No newline at end of file diff --git a/rules/rules_release/lib/actions/GenerateAction.mjs b/rules/rules_release/lib/actions/GenerateAction.mjs deleted file mode 100644 index 959854f67..000000000 --- a/rules/rules_release/lib/actions/GenerateAction.mjs +++ /dev/null @@ -1,40 +0,0 @@ -import ReleaseRepository from "../repositories/ReleaseRepository.mjs"; -import ConfigRepository from "../repositories/ConfigRepository.mjs"; -import TargetRepository from "../repositories/TargetRepository.mjs"; -import ChangesetRepository from "../repositories/ChangesetRepository.mjs"; - -export default class GenerateAction { - constructor({ configPaths, bazelDiffPath, bazelDiffArgs }) { - this.configPaths = configPaths; - this.bazelDiffPath = bazelDiffPath; - this.bazelDiffArgs = bazelDiffArgs; - } - - async execute() { - const configRepository = new ConfigRepository(); - const releaseRepository = new ReleaseRepository(this.configPaths); - const targetRepository = new TargetRepository({ - bazelDiffPath: this.bazelDiffPath, - bazelDiffArgs: this.bazelDiffArgs, - workspaceDir: configRepository.workspaceDir(), - hashesDir: configRepository.hashesDir(), - }); - const changesetRepository = new ChangesetRepository({ - workspaceDir: configRepository.workspaceDir(), - changesetDir: configRepository.changesetDir(), - }); - const releaseLabels = await releaseRepository.getAllLabels(); - const impactedTargets = await targetRepository.getImpactedTargets(); - const changedReleaseLabels = impactedTargets.filter((target) => { - return releaseLabels.includes(target); - }); - - for (const changedReleaseLabel of changedReleaseLabels) { - const release = await releaseRepository.getByLabel(changedReleaseLabel); - const changeset = await changesetRepository.writeChangeset({ - name: release.name, - }); - console.log(changeset); - } - } -} diff --git a/rules/rules_release/lib/repositories/TargetRepository.mjs b/rules/rules_release/lib/repositories/TargetRepository.mjs deleted file mode 100644 index 045675141..000000000 --- a/rules/rules_release/lib/repositories/TargetRepository.mjs +++ /dev/null @@ -1,87 +0,0 @@ -import { mkdir, readFile } from "fs/promises"; -import { $, which } from "zx"; -import { fileExists } from "../utils.mjs"; - -export default class TargetRepository { - constructor({ bazelDiffPath, bazelDiffArgs, workspaceDir, hashesDir }) { - this.bazelDiffPath = bazelDiffPath; - this.bazelDiffArgs = bazelDiffArgs; - this.workspaceDir = workspaceDir; - this.hashesDir = hashesDir; - } - - async getImpactedTargets() { - if (!(await fileExists(this.hashesDir))) { - mkdir(this.hashesDir, { recursive: true }); - } - - const previousCommit = ( - await $`git --work-tree=${this.workspaceDir} rev-parse master` - ).stdout.trim(); - - const currentCommit = ( - await $`git --work-tree=${this.workspaceDir} rev-parse HEAD` - ).stdout.trim(); - - const previousHashes = await this._generateHashesForSha( - previousCommit, - true - ); - - const currentHashes = await this._generateHashesForSha( - currentCommit, - false - ); - - const impactedTargets = await this._generateImpactedTargets( - currentCommit, - previousHashes, - currentHashes, - false - ); - - const data = await readFile(impactedTargets, "utf8"); - const result = data.split("\n"); - return result; - } - - async _generateHashesForSha(sha, cache) { - const hashesFile = `${this.hashesDir}/${sha}.json`; - const bazelPath = await which("bazel"); - - if (cache && (await fileExists(hashesFile))) { - return hashesFile; - } - - const currentBranch = - await $`git --work-tree=${this.workspaceDir} rev-parse --abbrev-ref HEAD`; - - try { - await this._checkoutSha(sha); - await $`${this.bazelDiffPath} generate-hashes ${this.bazelDiffArgs} -w ${this.workspaceDir} -b ${bazelPath} ${hashesFile}`; - await this._checkoutSha(currentBranch); - } catch (error) { - // make sure we checkout back to the current branch - await this._checkoutSha(currentBranch); - throw error; - } - - return hashesFile; - } - - async _generateImpactedTargets(sha, previousHashes, currentHashes, cache) { - const impactedTargetsPath = `${this.hashesDir}/${sha}.impacted_targets.json`; - - if (cache && (await fileExists(impactedTargetsPath))) { - return impactedTargetsPath; - } - - await $`${this.bazelDiffPath} get-impacted-targets -sh ${previousHashes} -fh ${currentHashes} -o ${impactedTargetsPath}`; - - return impactedTargetsPath; - } - - async _checkoutSha(sha) { - return await $`git --work-tree=${this.workspaceDir} checkout ${sha}`; - } -} diff --git a/rules/rules_release/lib/utils.mjs b/rules/rules_release/lib/utils.mjs deleted file mode 100644 index 67db1124e..000000000 --- a/rules/rules_release/lib/utils.mjs +++ /dev/null @@ -1,4 +0,0 @@ -import { stat } from "fs/promises"; - -export const fileExists = async (path) => - !!(await stat(path).catch((e) => false)); diff --git a/rules/rules_release/release/BUILD.bazel b/rules/rules_release/release/BUILD.bazel index 6a915928c..bc2120e78 100644 --- a/rules/rules_release/release/BUILD.bazel +++ b/rules/rules_release/release/BUILD.bazel @@ -1,6 +1,47 @@ +load("@aspect_rules_js//js:defs.bzl", "js_binary", "js_library") + package(default_visibility = ["//visibility:public"]) filegroup( name = "all_files", srcs = glob(["**/*"]), ) + +js_library( + name = "utils", + srcs = ["lib/utils.mjs"], +) + +js_binary( + name = "cli", + data = [ + "lib/actions/GenerateAction.mjs", + "lib/actions/PublishAction.mjs", + "lib/actions/VersionAction.mjs", + "lib/repositories/ChangelogRepository.mjs", + "lib/repositories/ChangesetRepository.mjs", + "lib/repositories/ConfigRepository.mjs", + "lib/repositories/PackageRepository.mjs", + "lib/repositories/PublishRepository.mjs", + "lib/repositories/ReleaseRepository.mjs", + "lib/repositories/VersionRepository.mjs", + ":utils", + "//:node_modules/@changesets/write", + "//:node_modules/commander", + "//:node_modules/zx", + ], + entry_point = "cli.mjs", + env = { + "WORKSPACE_NAME": repository_name().removeprefix("@"), + }, +) + +js_binary( + name = "changesets_cli", + data = [ + "//:node_modules/@changesets/changelog-github", + "//:node_modules/@changesets/cli", + "//:node_modules/zx", + ], + entry_point = "changesets_cli.mjs", +) diff --git a/rules/rules_release/changesets_cli.mjs b/rules/rules_release/release/changesets_cli.mjs similarity index 100% rename from rules/rules_release/changesets_cli.mjs rename to rules/rules_release/release/changesets_cli.mjs diff --git a/rules/rules_release/cli.mjs b/rules/rules_release/release/cli.mjs similarity index 81% rename from rules/rules_release/cli.mjs rename to rules/rules_release/release/cli.mjs index 28fd53264..ce691c57d 100644 --- a/rules/rules_release/cli.mjs +++ b/rules/rules_release/release/cli.mjs @@ -18,20 +18,10 @@ program program .command("generate") - .description( - "Generate changesets based on changed targets with latest master" - ) - .requiredOption("--bazel-diff-path <string>") - .option( - "--bazel-diff-args <string>", - "Additional args generate hashes command for bazel-diff", - "" - ) + .description("Generate changesets based on changed releases") .action(async (options) => { const action = new GenerateAction({ configPaths: program.opts().config, - bazelDiffPath: options.bazelDiffPath, - bazelDiffArgs: options.bazelDiffArgs, }); await action.execute(); }); diff --git a/rules/rules_release/release/defs.bzl b/rules/rules_release/release/defs.bzl index db880af85..9ce8f9cd0 100644 --- a/rules/rules_release/release/defs.bzl +++ b/rules/rules_release/release/defs.bzl @@ -1,7 +1,5 @@ load("//release/private:release.bzl", _release = "release") load("//release/private:release_manager.bzl", _release_manager = "release_manager") -load("//release/private:publish_github_release.bzl", _publish_github_release = "publish_github_release") release = _release release_manager = _release_manager -publish_github_release = _publish_github_release diff --git a/rules/rules_release/extensions.bzl b/rules/rules_release/release/extensions.bzl similarity index 61% rename from rules/rules_release/extensions.bzl rename to rules/rules_release/release/extensions.bzl index a91d178c1..7bdd21d64 100644 --- a/rules/rules_release/extensions.bzl +++ b/rules/rules_release/release/extensions.bzl @@ -1,7 +1,7 @@ -load("//:repositories.bzl", "dependencies") +load(":repositories.bzl", "rules_release_dependencies") def _non_module_dependencies_impl(_ctx): - dependencies() + rules_release_dependencies() non_module_dependencies = module_extension( implementation = _non_module_dependencies_impl, diff --git a/rules/rules_release/release/lib/actions/GenerateAction.mjs b/rules/rules_release/release/lib/actions/GenerateAction.mjs new file mode 100644 index 000000000..3e630aa05 --- /dev/null +++ b/rules/rules_release/release/lib/actions/GenerateAction.mjs @@ -0,0 +1,26 @@ +import ReleaseRepository from "../repositories/ReleaseRepository.mjs"; +import ConfigRepository from "../repositories/ConfigRepository.mjs"; +import ChangesetRepository from "../repositories/ChangesetRepository.mjs"; + +export default class GenerateAction { + constructor({ configPaths }) { + this.configPaths = configPaths; + } + + async execute() { + const configRepository = new ConfigRepository(); + const releaseRepository = new ReleaseRepository(this.configPaths); + const changesetRepository = new ChangesetRepository({ + workspaceDir: configRepository.workspaceDir(), + changesetDir: configRepository.changesetDir(), + }); + const changedReleases = await releaseRepository.getChangedReleases(); + + for (const changedRelease of changedReleases) { + const changeset = await changesetRepository.writeChangeset({ + name: changedRelease.name, + }); + console.log(changeset); + } + } +} diff --git a/rules/rules_release/lib/actions/PublishAction.mjs b/rules/rules_release/release/lib/actions/PublishAction.mjs similarity index 100% rename from rules/rules_release/lib/actions/PublishAction.mjs rename to rules/rules_release/release/lib/actions/PublishAction.mjs diff --git a/rules/rules_release/lib/actions/VersionAction.mjs b/rules/rules_release/release/lib/actions/VersionAction.mjs similarity index 100% rename from rules/rules_release/lib/actions/VersionAction.mjs rename to rules/rules_release/release/lib/actions/VersionAction.mjs diff --git a/rules/rules_release/lib/repositories/ChangelogRepository.mjs b/rules/rules_release/release/lib/repositories/ChangelogRepository.mjs similarity index 100% rename from rules/rules_release/lib/repositories/ChangelogRepository.mjs rename to rules/rules_release/release/lib/repositories/ChangelogRepository.mjs diff --git a/rules/rules_release/lib/repositories/ChangesetRepository.mjs b/rules/rules_release/release/lib/repositories/ChangesetRepository.mjs similarity index 77% rename from rules/rules_release/lib/repositories/ChangesetRepository.mjs rename to rules/rules_release/release/lib/repositories/ChangesetRepository.mjs index d10bbdc82..6b9632ec4 100644 --- a/rules/rules_release/lib/repositories/ChangesetRepository.mjs +++ b/rules/rules_release/release/lib/repositories/ChangesetRepository.mjs @@ -1,7 +1,8 @@ import pkg from "@changesets/write"; const { default: write } = pkg; -import { rename, readdir } from "fs/promises"; +import { rename, readdir, mkdir } from "fs/promises"; import { path, $ } from "zx"; +import { fileExists } from "../utils.mjs"; export default class ChangesetRepository { constructor({ workspaceDir, changesetDir, changesetBinaryPath }) { @@ -12,6 +13,8 @@ export default class ChangesetRepository { } async getByName(name) { + await this._ensureChangesetDir(); + const changesetFiles = (await readdir(this.changesetDir)) .filter((file) => { return ( @@ -29,6 +32,8 @@ export default class ChangesetRepository { } async writeChangeset({ name }) { + await this._ensureChangesetDir(); + let newFilePath = await this.getByName(name); if (newFilePath) { @@ -51,15 +56,24 @@ export default class ChangesetRepository { } async updateVersions() { + await this._ensureChangesetDir(); + + // NOTE: this is necessary so changesets find the changelog module like "@changesets/changelog-github" + // inside of .changeset/config.json. const nodeModulesPath = path.join( process.env.JS_BINARY__RUNFILES, process.env.WORKSPACE_NAME, "node_modules" ); process.env.NODE_PATH = nodeModulesPath; - process.env.WORKSPACE_DIR = this.workspaceDir; await $`${this.changesetBinaryPath} version`; } + + async _ensureChangesetDir() { + if (!(await fileExists(this.changesetDir))) { + await mkdir(this.changesetDir, { recursive: true }); + } + } } diff --git a/rules/rules_release/lib/repositories/ConfigRepository.mjs b/rules/rules_release/release/lib/repositories/ConfigRepository.mjs similarity index 75% rename from rules/rules_release/lib/repositories/ConfigRepository.mjs rename to rules/rules_release/release/lib/repositories/ConfigRepository.mjs index e4b0fc137..6b927d8f4 100644 --- a/rules/rules_release/lib/repositories/ConfigRepository.mjs +++ b/rules/rules_release/release/lib/repositories/ConfigRepository.mjs @@ -2,7 +2,7 @@ import { path } from "zx"; export default class ConfigRepository { workspaceDir() { - return process.env.BUILD_WORKSPACE_DIRECTORY; + return process.env.BUILD_WORKSPACE_DIRECTORY || process.cwd(); } tmpDir() { @@ -20,8 +20,4 @@ export default class ConfigRepository { packagesDir() { return path.join(this.rulesReleaseDir(), "packages"); } - - hashesDir() { - return path.join(this.rulesReleaseDir(), "bazel_diff_hashes"); - } } diff --git a/rules/rules_release/lib/repositories/PackageRepository.mjs b/rules/rules_release/release/lib/repositories/PackageRepository.mjs similarity index 100% rename from rules/rules_release/lib/repositories/PackageRepository.mjs rename to rules/rules_release/release/lib/repositories/PackageRepository.mjs diff --git a/rules/rules_release/lib/repositories/PublishRepository.mjs b/rules/rules_release/release/lib/repositories/PublishRepository.mjs similarity index 100% rename from rules/rules_release/lib/repositories/PublishRepository.mjs rename to rules/rules_release/release/lib/repositories/PublishRepository.mjs diff --git a/rules/rules_release/lib/repositories/ReleaseRepository.mjs b/rules/rules_release/release/lib/repositories/ReleaseRepository.mjs similarity index 53% rename from rules/rules_release/lib/repositories/ReleaseRepository.mjs rename to rules/rules_release/release/lib/repositories/ReleaseRepository.mjs index 718651b77..9d3c05a66 100644 --- a/rules/rules_release/lib/repositories/ReleaseRepository.mjs +++ b/rules/rules_release/release/lib/repositories/ReleaseRepository.mjs @@ -1,3 +1,4 @@ +import { $ } from "zx"; import { readFile } from "fs/promises"; export default class ReleaseRepository { @@ -10,6 +11,29 @@ export default class ReleaseRepository { return await this._getData(); } + async getChangedReleases() { + const releases = await this._getData(); + const changedReleases = []; + + for (const release of releases) { + const hasChanged = await $`${release.change_cmd}`; + + const output = hasChanged.stdout.trim().toLowerCase(); + + if (output !== "true" && output !== "false") { + throw new Error( + `Change command ${release.change_cmd} for release ${release.name} must return true or false, but returned ${output}` + ); + } + + if (hasChanged.stdout.trim().toLowerCase() === "true") { + changedReleases.push(release); + } + } + + return changedReleases; + } + async getAllLabels() { return (await this._getData()).map((release) => release.label); } diff --git a/rules/rules_release/lib/repositories/VersionRepository.mjs b/rules/rules_release/release/lib/repositories/VersionRepository.mjs similarity index 100% rename from rules/rules_release/lib/repositories/VersionRepository.mjs rename to rules/rules_release/release/lib/repositories/VersionRepository.mjs diff --git a/rules/rules_release/release/lib/utils.mjs b/rules/rules_release/release/lib/utils.mjs new file mode 100644 index 000000000..07280fad4 --- /dev/null +++ b/rules/rules_release/release/lib/utils.mjs @@ -0,0 +1,9 @@ +import { stat } from "fs/promises"; +import { createHash } from "node:crypto"; + +export const fileExists = async (path) => + !!(await stat(path).catch((e) => false)); + +export const md5 = (content) => { + return createHash("md5").update(content).digest("hex"); +}; diff --git a/rules/rules_release/release/private/release.bzl b/rules/rules_release/release/private/release.bzl index 008e5762a..c7884bfa6 100644 --- a/rules/rules_release/release/private/release.bzl +++ b/rules/rules_release/release/private/release.bzl @@ -2,15 +2,6 @@ load(":release_info.bzl", "ReleaseInfo") load("@aspect_rules_js//js:defs.bzl", "js_run_binary") load(":utils.bzl", "get_executable_from_target") -def _to_label_string(label): - if label.workspace_name == "": - workspace_name = "" - else: - # TODO: Wonder if there is a better way to get the workspace name of a locally overriden external repository - workspace_name = "@" + label.workspace_name.removesuffix("~override") - - return workspace_name + "//" + label.package + ":" + label.name - def _release_impl(ctx): release_config_file = ctx.actions.declare_file(ctx.label.name + ".json") release_name = ctx.attr.release_name @@ -23,11 +14,10 @@ def _release_impl(ctx): release_config_data = { "name": release_name, - "target_name": ctx.label.name, - "label": _to_label_string(ctx.label), "version_file": ctx.file.version_file.short_path, "changelog_file": ctx.file.changelog_file.short_path, "publish_cmds": publish_cmds_paths, + "change_cmd": get_executable_from_target(ctx.attr.change_cmd).short_path, "deps": [dep[ReleaseInfo].name for dep in ctx.attr.deps], } @@ -40,10 +30,10 @@ def _release_impl(ctx): name = release_name, ) - runfiles = ctx.runfiles(files = ctx.files.version_file + ctx.files.target + ctx.files.publish_cmds + ctx.files.deps + [ctx.file.changelog_file]) + runfiles = ctx.runfiles(files = ctx.files.version_file + ctx.files.publish_cmds + ctx.files.deps + [ctx.file.changelog_file] + ctx.files.change_cmd) runfiles = runfiles.merge_all([ d[DefaultInfo].default_runfiles - for d in (ctx.attr.publish_cmds + [ctx.attr.target] + ctx.attr.deps) + for d in (ctx.attr.publish_cmds + ctx.attr.deps + [ctx.attr.change_cmd]) ]) return [ @@ -55,11 +45,11 @@ _release = rule( implementation = _release_impl, attrs = { "deps": attr.label_list(providers = [ReleaseInfo]), - "target": attr.label(mandatory = True), "version_file": attr.label(allow_single_file = True, mandatory = True), "publish_cmds": attr.label_list(cfg = "target"), "release_name": attr.string(mandatory = True), "changelog_file": attr.label(allow_single_file = True, mandatory = True), + "change_cmd": attr.label(cfg = "target", mandatory = True), }, ) diff --git a/rules/rules_release/release/private/release_manager.bzl b/rules/rules_release/release/private/release_manager.bzl index b48aecd28..8536be128 100644 --- a/rules/rules_release/release/private/release_manager.bzl +++ b/rules/rules_release/release/private/release_manager.bzl @@ -31,20 +31,14 @@ def _common(ctx, extra_args, extra_runfiles = []): def _generate_impl(ctx): extra_args = ["generate"] - extra_args = extra_args + ["--bazel-diff-path", ctx.executable._bazel_diff.short_path] - - if ctx.attr.bazel_diff_args: - extra_args = extra_args + ["--bazel-diff-args", ctx.attr.bazel_diff_args] - extra_runfiles = [ctx.attr._bazel_diff] + extra_runfiles = [] return _common(ctx, extra_args, extra_runfiles) _generate = rule( implementation = _generate_impl, attrs = { "deps": attr.label_list(providers = [ReleaseInfo]), - "_cli": attr.label(executable = True, default = Label("@rules_release//:cli"), cfg = "target"), - "_bazel_diff": attr.label(executable = True, default = Label("@rules_release//:bazel-diff"), cfg = "target"), - "bazel_diff_args": attr.string(), + "_cli": attr.label(executable = True, default = Label("@rules_release//release:cli"), cfg = "target"), }, executable = True, ) @@ -59,8 +53,8 @@ _version = rule( implementation = _version_impl, attrs = { "deps": attr.label_list(providers = [ReleaseInfo]), - "_cli": attr.label(executable = True, default = Label("@rules_release//:cli"), cfg = "target"), - "_changesets_cli": attr.label(executable = True, default = Label("@rules_release//:changesets_cli"), cfg = "target"), + "_cli": attr.label(executable = True, default = Label("@rules_release//release:cli"), cfg = "target"), + "_changesets_cli": attr.label(executable = True, default = Label("@rules_release//release:changesets_cli"), cfg = "target"), }, executable = True, ) @@ -80,13 +74,13 @@ _publish = rule( implementation = _publish_impl, attrs = { "deps": attr.label_list(providers = [ReleaseInfo]), - "_cli": attr.label(executable = True, default = Label("@rules_release//:cli"), cfg = "target"), + "_cli": attr.label(executable = True, default = Label("@rules_release//release:cli"), cfg = "target"), "publish_cmds": attr.label_list(cfg = "target"), }, executable = True, ) -def release_manager(name, deps, publish_cmds = [], bazel_diff_args = ""): +def release_manager(name, deps, publish_cmds = [], change_cmd = None): generate_name = "{}.generate".format(name) version_name = "{}.version".format(name) publish_name = "{}.publish".format(name) @@ -94,7 +88,6 @@ def release_manager(name, deps, publish_cmds = [], bazel_diff_args = ""): _generate( name = generate_name, deps = deps, - bazel_diff_args = bazel_diff_args, ) _version( diff --git a/rules/rules_release/release/repositories.bzl b/rules/rules_release/release/repositories.bzl new file mode 100644 index 000000000..cc9d64b0c --- /dev/null +++ b/rules/rules_release/release/repositories.bzl @@ -0,0 +1,137 @@ +load("@bazel_tools//tools/build_defs/repo:http.bzl", _http_archive = "http_archive", _http_jar = "http_jar") +load("@bazel_tools//tools/build_defs/repo:utils.bzl", "maybe") + +# Copied from https://groups.google.com/g/bazel-discuss/c/xpsg3mWQPZg +def _starlarkified_local_repository_impl(repository_ctx): + relative_path = repository_ctx.attr.path + workspace_root = repository_ctx.path(repository_ctx.attr._root_file).dirname + + absolute_path = workspace_root + for segment in relative_path.split("/"): + absolute_path = absolute_path.get_child(segment) + + repository_ctx.symlink(absolute_path, ".") + +starlarkified_local_repository = repository_rule( + implementation = _starlarkified_local_repository_impl, + attrs = { + "_root_file": attr.label(default = Label("//:MODULE.bazel")), + "path": attr.string(mandatory = True), + }, +) + +def http_archive(**kwargs): + maybe(_http_archive, **kwargs) + +def http_jar(**kwargs): + maybe(_http_jar, **kwargs) + +# load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +# http_archive( +# name = "bazel_skylib", +# sha256 = "66ffd9315665bfaafc96b52278f57c7e2dd09f5ede279ea6d39b2be471e7e3aa", +# urls = [ +# "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", +# "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", +# ], +# ) + +# load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") + +# bazel_skylib_workspace() + +def rules_release_bazel_dependencies(): + http_archive( + name = "bazel_features", + sha256 = "62c26e427e5cbc751024446927622e398a9dcdf32c64325238815709d11c11a8", + strip_prefix = "bazel_features-1.1.1", + url = "https://github.com/bazel-contrib/bazel_features/releases/download/v1.1.1/bazel_features-v1.1.1.tar.gz", + ) + + http_archive( + name = "bazel_skylib", + sha256 = "66ffd9315665bfaafc96b52278f57c7e2dd09f5ede279ea6d39b2be471e7e3aa", + urls = [ + "https://mirror.bazel.build/github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", + "https://github.com/bazelbuild/bazel-skylib/releases/download/1.4.2/bazel-skylib-1.4.2.tar.gz", + ], + ) + + http_archive( + name = "aspect_bazel_lib", + sha256 = "4b32cf6feab38b887941db022020eea5a49b848e11e3d6d4d18433594951717a", + strip_prefix = "bazel-lib-2.0.1", + url = "https://github.com/aspect-build/bazel-lib/releases/download/v2.0.1/bazel-lib-v2.0.1.tar.gz", + ) + + http_archive( + name = "aspect_rules_js", + sha256 = "76a04ef2120ee00231d85d1ff012ede23963733339ad8db81f590791a031f643", + strip_prefix = "rules_js-1.34.1", + url = "https://github.com/aspect-build/rules_js/releases/download/v1.34.1/rules_js-v1.34.1.tar.gz", + ) + + http_archive( + name = "rules_python", + sha256 = "94750828b18044533e98a129003b6a68001204038dc4749f40b195b24c38f49f", + strip_prefix = "rules_python-0.21.0", + url = "https://github.com/bazelbuild/rules_python/releases/download/0.21.0/rules_python-0.21.0.tar.gz", + ) + +def rules_release_dependencies(): + http_jar( + name = "bazel_diff", + sha256 = "7943790f690ad5115493da8495372c89f7895b09334cb4fee5174a8f213654dd", + urls = [ + "https://github.com/Tinder/bazel-diff/releases/download/5.0.0/bazel-diff_deploy.jar", + ], + ) + + http_archive( + name = "github_cli_linux_arm64", + build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", + sha256 = "e29e51efae58693cab394b983771bc0c73b400e429dd1d7339fc62c8b257c74a", + url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_linux_arm64.tar.gz", + ) + + http_archive( + name = "github_cli_linux_amd64", + build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", + sha256 = "18a1bc97eb72305ff20e965d3c67aee7e1ac607fabc6251c7374226d8c41422b", + url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_linux_amd64.tar.gz", + ) + + http_archive( + name = "github_cli_darwin_arm64", + build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", + sha256 = "f854225778b7215480c442cd2e3eeec1a56d33876bbbad19daf557c1b00d6913", + url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_macOS_arm64.zip", + ) + + # From https://app-updates.agilebits.com/product_history/CLI2 + http_archive( + name = "onepassword_linux_arm64", + build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", + sha256 = "b93a8e0dc42c0979bb13047ac4412bd73092be57bb84ad223eeca295151159fa", + url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.18.0/op_linux_arm64_v2.18.0.zip", + ) + + http_archive( + name = "onepassword_linux_amd64", + build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", + sha256 = "2baf610b476727f24c62cc843419f55b157e1a05521a698c1c8b4ed676a766aa", + url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.18.0/op_linux_amd64_v2.18.0.zip", + ) + + http_archive( + name = "onepassword_darwin_arm64", + build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", + sha256 = "b9ae52df3003216b454f6ac0a402c71bcfb4804eafb3ee3593a84a2002930d27", + url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.22.0/op_darwin_arm64_v2.22.0.zip", + ) + + starlarkified_local_repository( + name = "examples_workspace", + path = "examples/workspace", + ) diff --git a/rules/rules_release/release/repository_primary_deps.bzl b/rules/rules_release/release/repository_primary_deps.bzl new file mode 100644 index 000000000..5f04901a8 --- /dev/null +++ b/rules/rules_release/release/repository_primary_deps.bzl @@ -0,0 +1,37 @@ +load("@bazel_features//:deps.bzl", "bazel_features_deps") +load("@aspect_bazel_lib//lib:repositories.bzl", "aspect_bazel_lib_dependencies", "aspect_bazel_lib_register_toolchains") +load("@aspect_rules_js//js:repositories.bzl", "rules_js_dependencies") +load("@aspect_rules_js//npm:repositories.bzl", "npm_translate_lock") +load("@rules_python//python:repositories.bzl", "py_repositories") +load("@rules_python//python:pip.bzl", "pip_parse") +load("@bazel_skylib//:workspace.bzl", "bazel_skylib_workspace") + +def install_primary_deps(): + # ------------------------------------ bazel_features ------------------------------------ # + bazel_features_deps() + + # ------------------------------------ bazel_skylib ------------------------------------ # + bazel_skylib_workspace() + + # ------------------------------------ aspect_bazel_lib ------------------------------------ # + aspect_bazel_lib_dependencies() + aspect_bazel_lib_register_toolchains() + + # ------------------------------------ aspect_rules_js ------------------------------------ # + rules_js_dependencies() + + # ------------------------------------ rules_python ------------------------------------ # + py_repositories() + + # ------------------------------------ rules_task ------------------------------------ # + pip_parse( + name = "pip", + requirements_lock = "@rules_task//:requirements.txt", + ) + + # ------------------------------------ rules_release ------------------------------------ # + npm_translate_lock( + name = "npm", + pnpm_lock = "@rules_release//:pnpm-lock.yaml", + verify_node_modules_ignored = "@rules_release//:.bazelignore", + ) diff --git a/rules/rules_release/release/repository_secondary_deps.bzl b/rules/rules_release/release/repository_secondary_deps.bzl new file mode 100644 index 000000000..8f565b2aa --- /dev/null +++ b/rules/rules_release/release/repository_secondary_deps.bzl @@ -0,0 +1,16 @@ +load("@rules_nodejs//nodejs:repositories.bzl", "DEFAULT_NODE_VERSION", "nodejs_register_toolchains") +load("@pip//:requirements.bzl", install_pip_deps = "install_deps") +load("@npm//:repositories.bzl", "npm_repositories") + +def install_secondary_deps(): + # ------------------------------------ aspect_rules_js ------------------------------------ # + nodejs_register_toolchains( + name = "nodejs", + node_version = DEFAULT_NODE_VERSION, + ) + + # ------------------------------------ rules_task ------------------------------------ # + install_pip_deps() + + # ------------------------------------ rules_release ------------------------------------ # + npm_repositories() diff --git a/rules/rules_release/repositories.bzl b/rules/rules_release/repositories.bzl deleted file mode 100644 index cd8bc7504..000000000 --- a/rules/rules_release/repositories.bzl +++ /dev/null @@ -1,53 +0,0 @@ -load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive", "http_jar") - -def dependencies(): - http_jar( - name = "bazel_diff", - sha256 = "7943790f690ad5115493da8495372c89f7895b09334cb4fee5174a8f213654dd", - urls = [ - "https://github.com/Tinder/bazel-diff/releases/download/5.0.0/bazel-diff_deploy.jar", - ], - ) - - http_archive( - name = "github_cli_linux_arm64", - build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", - sha256 = "e29e51efae58693cab394b983771bc0c73b400e429dd1d7339fc62c8b257c74a", - url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_linux_arm64.tar.gz", - ) - - http_archive( - name = "github_cli_linux_amd64", - build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", - sha256 = "18a1bc97eb72305ff20e965d3c67aee7e1ac607fabc6251c7374226d8c41422b", - url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_linux_amd64.tar.gz", - ) - - http_archive( - name = "github_cli_darwin_arm64", - build_file = "//tools/github_cli:BUILD.repositories.bazel.tpl", - sha256 = "f854225778b7215480c442cd2e3eeec1a56d33876bbbad19daf557c1b00d6913", - url = "https://github.com/cli/cli/releases/download/v2.39.1/gh_2.39.1_macOS_arm64.zip", - ) - - # From https://app-updates.agilebits.com/product_history/CLI2 - http_archive( - name = "onepassword_linux_arm64", - build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", - sha256 = "b93a8e0dc42c0979bb13047ac4412bd73092be57bb84ad223eeca295151159fa", - url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.18.0/op_linux_arm64_v2.18.0.zip", - ) - - http_archive( - name = "onepassword_linux_amd64", - build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", - sha256 = "2baf610b476727f24c62cc843419f55b157e1a05521a698c1c8b4ed676a766aa", - url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.18.0/op_linux_amd64_v2.18.0.zip", - ) - - http_archive( - name = "onepassword_darwin_arm64", - build_file = "//tools/onepassword:BUILD.repositories.bazel.tpl", - sha256 = "b9ae52df3003216b454f6ac0a402c71bcfb4804eafb3ee3593a84a2002930d27", - url = "https://cache.agilebits.com/dist/1P/op2/pkg/v2.22.0/op_darwin_arm64_v2.22.0.zip", - ) diff --git a/rules/rules_release/tools/BUILD.bazel b/rules/rules_release/tools/BUILD.bazel new file mode 100644 index 000000000..e08ad3b0b --- /dev/null +++ b/rules/rules_release/tools/BUILD.bazel @@ -0,0 +1,27 @@ +load("@rules_java//java:defs.bzl", "java_binary") +load("@aspect_rules_js//js:defs.bzl", "js_binary") + +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "all_files", + srcs = glob(["**/*"]), +) + +java_binary( + name = "bazel-diff", + main_class = "com.bazel_diff.Main", + runtime_deps = ["@bazel_diff//jar"], +) + +js_binary( + name = "bazel-diff-change-cli", + data = [ + "lib/actions/BazelDiffChangeAction.mjs", + "lib/repositories/BazelDiffRepository.mjs", + "//:node_modules/commander", + "//:node_modules/zx", + "//release:utils", + ], + entry_point = "bazel_diff_change_cli.mjs", +) diff --git a/rules/rules_release/tools/bazel_diff_change_cli.mjs b/rules/rules_release/tools/bazel_diff_change_cli.mjs new file mode 100644 index 000000000..df9868286 --- /dev/null +++ b/rules/rules_release/tools/bazel_diff_change_cli.mjs @@ -0,0 +1,35 @@ +import { Command } from "commander"; +const program = new Command(); + +import BazelDiffChangeAction from "./lib/actions/BazelDiffChangeAction.mjs"; + +program + .name("bazel-diff-change-cli") + .description("CLI to interact with bazel-diff for release-manager") + .requiredOption("--bazel-diff-path <string>", "Path to bazel-diff") + .option( + "--generate-hashes-extra-args <string>", + "Additional args to pass to bazel-diff generate-hashes command", + "" + ) + .option( + "--get-impacted-targets-extra-args <string>", + "Additional args to pass to bazel-diff get-impacted-targets command", + "" + ) + .requiredOption( + "--previous-revision-cmd <string>", + "Executable to get previous git revision" + ) + .requiredOption( + "--final-revision-cmd <string>", + "Executable to get final git revision" + ) + .argument("<label>", "Bazel Target Label to diff") + .version("0.0.0") + .action(async (label, options) => { + const action = new BazelDiffChangeAction(label, options); + await action.execute(); + }); + +program.parse(); diff --git a/rules/rules_release/tools/defs.bzl b/rules/rules_release/tools/defs.bzl new file mode 100644 index 000000000..b3a10bcba --- /dev/null +++ b/rules/rules_release/tools/defs.bzl @@ -0,0 +1,5 @@ +load("//tools/private:publish_github_release.bzl", _publish_github_release = "publish_github_release") +load("//tools/private:bazel_diff_release.bzl", _bazel_diff_release = "bazel_diff_release") + +publish_github_release = _publish_github_release +bazel_diff_release = _bazel_diff_release diff --git a/rules/rules_release/tools/lib/actions/BazelDiffChangeAction.mjs b/rules/rules_release/tools/lib/actions/BazelDiffChangeAction.mjs new file mode 100644 index 000000000..d53ac8d17 --- /dev/null +++ b/rules/rules_release/tools/lib/actions/BazelDiffChangeAction.mjs @@ -0,0 +1,42 @@ +import BazelDiffRepository from "../repositories/BazelDiffRepository.mjs"; +import { path } from "zx"; + +export default class BazelDiffChangeAction { + constructor( + label, + { + generateHashesExtraArgs, + getImpactedTargetsExtraArgs, + bazelDiffPath, + previousRevisionCmd, + finalRevisionCmd, + } + ) { + this.label = label; + this.generateHashesExtraArgs = generateHashesExtraArgs; + this.getImpactedTargetsExtraArgs = getImpactedTargetsExtraArgs; + this.bazelDiffPath = bazelDiffPath; + this.previousRevisionCmd = previousRevisionCmd; + this.finalRevisionCmd = finalRevisionCmd; + } + + async execute() { + const workspaceDir = process.env.BUILD_WORKSPACE_DIRECTORY || process.cwd(); + const hashesDir = path.join(workspaceDir, "tmp", "bazel-diff-hashes"); + + const bazelDiffRepository = new BazelDiffRepository({ + bazelDiffPath: this.bazelDiffPath, + generateHashesExtraArgs: this.generateHashesExtraArgs, + getImpactedTargetsExtraArgs: this.getImpactedTargetsExtraArgs, + workspaceDir: workspaceDir, + hashesDir: hashesDir, + previousRevisionCmd: this.previousRevisionCmd, + finalRevisionCmd: this.finalRevisionCmd, + }); + + const hasLabelChanged = await bazelDiffRepository.hasLabelChanged( + this.label + ); + console.log(hasLabelChanged); + } +} diff --git a/rules/rules_release/tools/lib/repositories/BazelDiffRepository.mjs b/rules/rules_release/tools/lib/repositories/BazelDiffRepository.mjs new file mode 100644 index 000000000..5be0975de --- /dev/null +++ b/rules/rules_release/tools/lib/repositories/BazelDiffRepository.mjs @@ -0,0 +1,99 @@ +import { mkdir, readFile } from "fs/promises"; +import { $, which } from "zx"; +import { fileExists, md5 } from "../../../release/lib/utils.mjs"; + +export default class BazelDiffRepository { + constructor({ + bazelDiffPath, + generateHashesExtraArgs, + getImpactedTargetsExtraArgs, + workspaceDir, + hashesDir, + previousRevisionCmd, + finalRevisionCmd, + }) { + this.bazelDiffPath = bazelDiffPath; + this.generateHashesExtraArgs = generateHashesExtraArgs; + this.getImpactedTargetsExtraArgs = getImpactedTargetsExtraArgs; + this.workspaceDir = workspaceDir; + this.hashesDir = hashesDir; + this.previousRevisionCmd = previousRevisionCmd; + this.finalRevisionCmd = finalRevisionCmd; + } + + async hasLabelChanged(label) { + const impactedTargets = await this._getImpactedTargets(); + return impactedTargets.includes(label); + } + + async _getImpactedTargets() { + if (!(await fileExists(this.hashesDir))) { + mkdir(this.hashesDir, { recursive: true }); + } + + const previousCommit = (await $`${this.previousRevisionCmd}`).stdout.trim(); + const currentCommit = (await $`${this.finalRevisionCmd}`).stdout.trim(); + + const previousHashes = await this._generateHashesForSha( + previousCommit, + true + ); + + const currentHashes = await this._generateHashesForSha(currentCommit, true); + + const impactedTargets = await this._generateImpactedTargets( + currentCommit, + previousHashes, + currentHashes, + true + ); + + const data = await readFile(impactedTargets, "utf8"); + const result = data.split("\n"); + return result; + } + + async _generateHashesForSha(sha, cache) { + const hashesFile = `${this.hashesDir}/${sha}-${md5( + this.generateHashesExtraArgs + )}.json`; + const bazelPath = await which("bazel"); + + if (cache && (await fileExists(hashesFile))) { + return hashesFile; + } + + const currentBranch = + await $`git -C ${this.workspaceDir} rev-parse --abbrev-ref HEAD`; + + try { + await this._checkoutSha(sha); + await $`${this.bazelDiffPath} generate-hashes ${this.generateHashesExtraArgs} -w ${this.workspaceDir} -b ${bazelPath} ${hashesFile}`; + await this._checkoutSha(currentBranch); + } catch (error) { + // make sure we checkout back to the current branch + await this._checkoutSha(currentBranch); + throw error; + } + + return hashesFile; + } + + async _generateImpactedTargets(sha, previousHashes, currentHashes, cache) { + const impactedTargetsPath = `${this.hashesDir}/${sha}-${md5( + this.generateHashesExtraArgs + )}-${md5(this.getImpactedTargetsExtraArgs)}.impacted_targets.json`; + + if (cache && (await fileExists(impactedTargetsPath))) { + return impactedTargetsPath; + } + + await $`${this.bazelDiffPath} get-impacted-targets ${this.getImpactedTargetsExtraArgs} -sh ${previousHashes} -fh ${currentHashes} -o ${impactedTargetsPath}`; + + return impactedTargetsPath; + } + + async _checkoutSha(sha) { + return await $`git -C ${this.workspaceDir} checkout ${sha}`; + } +} diff --git a/rules/rules_release/tools/private/BUILD.bazel b/rules/rules_release/tools/private/BUILD.bazel new file mode 100644 index 000000000..6a915928c --- /dev/null +++ b/rules/rules_release/tools/private/BUILD.bazel @@ -0,0 +1,6 @@ +package(default_visibility = ["//visibility:public"]) + +filegroup( + name = "all_files", + srcs = glob(["**/*"]), +) diff --git a/rules/rules_release/tools/private/bazel_diff_release.bzl b/rules/rules_release/tools/private/bazel_diff_release.bzl new file mode 100644 index 000000000..e0aed7141 --- /dev/null +++ b/rules/rules_release/tools/private/bazel_diff_release.bzl @@ -0,0 +1,95 @@ +load("//release:defs.bzl", "release") +load("@rules_task//task:defs.bzl", "task") + +def _to_label_string(label): + if label.workspace_name == "": + workspace_name = "" + else: + # TODO: Wonder if there is a better way to get the workspace name of a locally overriden external repository + workspace_name = "@" + label.workspace_name.removesuffix("~override") + + return workspace_name + "//" + label.package + ":" + label.name + +def _bazel_diff_release_impl(ctx): + executable = ctx.actions.declare_file(ctx.label.name) + bazel_diff_cli_path = ctx.executable._bazel_diff_cli.short_path + bazel_diff_path = ctx.executable._bazel_diff.short_path + previous_revision_path = ctx.executable.previous_revision_cmd.short_path + final_revision_path = ctx.executable.final_revision_cmd.short_path + + args = [bazel_diff_cli_path, "--bazel-diff-path", bazel_diff_path, "--previous-revision-cmd", previous_revision_path, "--final-revision-cmd", final_revision_path] + + if ctx.attr.generate_hashes_extra_args: + args += ["--generate-hashes-extra-args", " ".join(ctx.attr.generate_hashes_extra_args)] + + if ctx.attr.get_impacted_targets_extra_args: + args += ["--get-impacted-targets-extra-args", " ".join(ctx.attr.get_impacted_targets_extra_args)] + + args.append(_to_label_string(ctx.attr.target.label)) + + command = " ".join(args) + + runfiles = ctx.runfiles(files = ctx.files._bazel_diff + ctx.files._bazel_diff_cli + ctx.files.previous_revision_cmd + ctx.files.final_revision_cmd) + runfiles = runfiles.merge_all([ + d[DefaultInfo].default_runfiles + for d in ([ctx.attr._bazel_diff] + [ctx.attr._bazel_diff_cli] + [ctx.attr.previous_revision_cmd] + [ctx.attr.final_revision_cmd]) + ]) + + ctx.actions.write( + output = executable, + content = command, + ) + + return [ + DefaultInfo(executable = executable, runfiles = runfiles), + ] + +_bazel_diff_release = rule( + implementation = _bazel_diff_release_impl, + attrs = { + "_bazel_diff": attr.label(executable = True, cfg = "target", default = Label("//tools:bazel-diff")), + "_bazel_diff_cli": attr.label(executable = True, cfg = "target", default = Label("//tools:bazel-diff-change-cli")), + "generate_hashes_extra_args": attr.string_list(default = []), + "get_impacted_targets_extra_args": attr.string_list(default = []), + "target": attr.label(mandatory = True), + "previous_revision_cmd": attr.label(executable = True, cfg = "target", mandatory = True), + "final_revision_cmd": attr.label(executable = True, cfg = "target", mandatory = True), + }, + executable = True, +) + +def bazel_diff_release(**kwargs): + name = kwargs.get("name") + change_cmd_name = "{}.change_cmd".format(name) + previous_revision_cmd_name = "{}.previous_revision_cmd".format(name) + final_revision_cmd_name = "{}.final_revision_cmd".format(name) + target = kwargs.pop("target") + generate_hashes_extra_args = kwargs.pop("generate_hashes_extra_args", []) + get_impacted_targets_extra_args = kwargs.pop("get_impacted_targets_extra_args", []) + + task( + name = previous_revision_cmd_name, + cmds = [ + "git rev-parse master", + ], + cwd = "$BUILD_WORKSPACE_DIRECTORY", + ) + + task( + name = final_revision_cmd_name, + cmds = [ + "git rev-parse HEAD", + ], + cwd = "$BUILD_WORKSPACE_DIRECTORY", + ) + + _bazel_diff_release( + name = change_cmd_name, + generate_hashes_extra_args = generate_hashes_extra_args, + get_impacted_targets_extra_args = get_impacted_targets_extra_args, + target = target, + previous_revision_cmd = previous_revision_cmd_name, + final_revision_cmd = final_revision_cmd_name, + ) + + release(change_cmd = change_cmd_name, **kwargs) diff --git a/rules/rules_release/release/private/publish_github_release.bzl b/rules/rules_release/tools/private/publish_github_release.bzl similarity index 100% rename from rules/rules_release/release/private/publish_github_release.bzl rename to rules/rules_release/tools/private/publish_github_release.bzl diff --git a/rules/rules_task/BUILD.bazel b/rules/rules_task/BUILD.bazel index e4a730bde..ada5ceba8 100644 --- a/rules/rules_task/BUILD.bazel +++ b/rules/rules_task/BUILD.bazel @@ -1,4 +1,4 @@ -load("@rules_release//release:defs.bzl", "publish_github_release", "release") +load("@rules_release//tools:defs.bzl", "publish_github_release", release = "bazel_diff_release") load("@aspect_bazel_lib//lib:tar.bzl", "mtree_spec", "tar") load("//task:defs.bzl", "cmd") load("//tools:defs.bzl", "compile_pip_requirements") @@ -107,6 +107,7 @@ publish_github_release( release( name = "release", changelog_file = "CHANGELOG.md", + generate_hashes_extra_args = ["--fineGrainedHashExternalRepos=rules_task"], publish_cmds = [ ":publish_github_release", ], diff --git a/tools/bunq2ynab/BUILD.bazel b/tools/bunq2ynab/BUILD.bazel index bf9789024..5d5fce3e2 100644 --- a/tools/bunq2ynab/BUILD.bazel +++ b/tools/bunq2ynab/BUILD.bazel @@ -2,7 +2,7 @@ load("@rules_python//python:defs.bzl", "py_binary") load("@rules_task//task:defs.bzl", "cmd", "task", "task_test") load("//tools/python:defs.bzl", "py_image") load("@pdm-setup//:requirements.bzl", "requirement") -load("@rules_release//release:defs.bzl", "publish_github_release", "release") +load("@rules_release//tools:defs.bzl", "publish_github_release", release = "bazel_diff_release") load("@rules_oci//oci:defs.bzl", "oci_push") package(default_visibility = ["//visibility:public"])