From 7017590d27ac81c62ebe90f0a9f4a361b07fbf0e Mon Sep 17 00:00:00 2001 From: vaishnavi yendole Date: Thu, 22 Oct 2026 14:25:10 +0530 Subject: [PATCH 1/2] docs: publish mw::log dependable-element report alongside main site Add a lightweight docs/dependable_elements/index.rst page, wired into docs/index.rst's toctree, that links to the standalone Sphinx report already generated by the dependable_element() macro for the mw::log backend (//score/mw/log/dependability:dependable_element_mw_log_backend_doc). A new scripts/publish_dependable_element_reports.py (run via the //:publish_dependable_element_reports py_binary) copies that report's pre-built HTML output into _build/dependable_elements/ after the main docs() build, so it is included in the published GitHub Pages site without duplicating or replacing any existing content. CI (.github/workflows/docs.yml) chains the publish step after the primary //:docs build so both outputs ship together. --- .github/workflows/docs.yml | 2 +- BUILD | 9 ++++ docs/dependable_elements/index.rst | 22 +++++++++ docs/index.rst | 1 + scripts/publish_dependable_element_reports.py | 49 +++++++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 docs/dependable_elements/index.rst create mode 100644 scripts/publish_dependable_element_reports.py diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 83f245fdc..bf0e4eab7 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -54,6 +54,6 @@ jobs: id-token: write with: # the bazel-target depends on your repo specific docs_targets configuration (e.g. "suffix") - bazel-target: "--lockfile_mode=error //:docs -- --github_user=${{ github.repository_owner }} --github_repo=${{ github.event.repository.name }}" + bazel-target: "--lockfile_mode=error //:docs -- --github_user=${{ github.repository_owner }} --github_repo=${{ github.event.repository.name }} && bazel run --lockfile_mode=error //:publish_dependable_element_reports" retention-days: 3 tests-report-artifact: logging_cpp_coverage diff --git a/BUILD b/BUILD index e722e11dd..79d0d55d7 100644 --- a/BUILD +++ b/BUILD @@ -10,6 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* +load("@rules_python//python:defs.bzl", "py_binary") load("@score_docs_as_code//:docs.bzl", "docs") load("@score_tooling//:defs.bzl", "copyright_checker", "setup_starpls") load("@score_tooling//third_party/format:macros.bzl", "use_format_targets") @@ -46,6 +47,14 @@ exports_files([ ".clang-tidy", ]) +py_binary( + name = "publish_dependable_element_reports", + testonly = True, + srcs = ["scripts/publish_dependable_element_reports.py"], + data = ["//score/mw/log/dependability:dependable_element_mw_log_backend_doc"], + main = "scripts/publish_dependable_element_reports.py", +) + # Creates all documentation targets: # - `:docs` for building documentation at build-time docs( diff --git a/docs/dependable_elements/index.rst b/docs/dependable_elements/index.rst new file mode 100644 index 000000000..f2412ada5 --- /dev/null +++ b/docs/dependable_elements/index.rst @@ -0,0 +1,22 @@ +.. + # ******************************************************************************* + # Copyright (c) 2026 Contributors to the Eclipse Foundation + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* + +mw::log Backend Dependable Element Report +=========================================== + +The dependable-element report for the ``mw::log`` backend, including requirements, +architecture, dependability analysis, and `LOBSTER `_ +traceability. + +`Open the mw::log Backend Dependable Element Report `_ diff --git a/docs/index.rst b/docs/index.rst index b789cabe3..58751c87d 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -80,6 +80,7 @@ The Components documentation provides detailed documentation for each individual components/datarouter/index.rst components/mw_log/index.rst + dependable_elements/index Quick Start ----------- diff --git a/scripts/publish_dependable_element_reports.py b/scripts/publish_dependable_element_reports.py new file mode 100644 index 000000000..ea459f036 --- /dev/null +++ b/scripts/publish_dependable_element_reports.py @@ -0,0 +1,49 @@ +import os +import shutil +import sys +from pathlib import Path + + +def _make_writable(path: Path) -> None: + for root, dirs, files in os.walk(path): + for name in dirs + files: + os.chmod(os.path.join(root, name), 0o755) + os.chmod(path, 0o755) + + +def main() -> int: + workspace_dir = os.environ.get("BUILD_WORKSPACE_DIRECTORY") + if not workspace_dir: + print("error: this script must be run via 'bazel run'", file=sys.stderr) + return 1 + + src = ( + Path(__file__).parent.parent + / "score/mw/log/dependability/dependable_element_mw_log_backend_doc/html" + ) + dest = ( + Path(workspace_dir) + / "_build" + / "dependable_elements" + / "dependable_element_mw_log_backend_doc" + ) + + if not src.is_dir(): + print( + f"error: expected dependable_element_mw_log_backend_doc output at {src}", + file=sys.stderr, + ) + return 1 + + if dest.exists(): + _make_writable(dest) + shutil.rmtree(dest) + shutil.copytree(src, dest) + _make_writable(dest) + + print(f"Published dependable_element_mw_log_backend_doc to {dest}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 635d0545f7e960ba4365e4499f926699460d36da Mon Sep 17 00:00:00 2001 From: rmaddikery Date: Fri, 23 Oct 2026 11:50:07 +0200 Subject: [PATCH 2/2] split docs build and publish, keep Bazel caches shared - Bump the docs workflow to a version that sets up bazel caching for every bazel run in the job, so the chained target runs share caches. - build on pull_request with read-only permissions - add docs-publish.yml (workflow_run) for deployment - drop docs-verify, now covered by the docs build - locate the dependable-element report via runfiles --- .github/workflows/docs-publish.yml | 30 +++++ .github/workflows/docs.yml | 30 ++--- BUILD | 27 ++++- docs/dependable_elements/index.rst | 8 +- scripts/publish_dependable_element_reports.py | 94 ++++++++++++---- ...publish_dependable_element_reports_test.py | 106 ++++++++++++++++++ 6 files changed, 244 insertions(+), 51 deletions(-) create mode 100644 .github/workflows/docs-publish.yml create mode 100644 scripts/publish_dependable_element_reports_test.py diff --git a/.github/workflows/docs-publish.yml b/.github/workflows/docs-publish.yml new file mode 100644 index 000000000..e9a7191b5 --- /dev/null +++ b/.github/workflows/docs-publish.yml @@ -0,0 +1,30 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +name: Publish Docs + +on: + workflow_run: + workflows: ["Test and Build Docs"] + types: [completed] + +jobs: + docs-publish: + if: github.event.workflow_run.conclusion == 'success' + uses: eclipse-score/cicd-workflows/.github/workflows/docs-publish.yml@f77ed751fa587e3a1142b2d85320917e00c888ae # main (2026-10) + permissions: + actions: read + contents: write + id-token: write + pages: write + pull-requests: write diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index bf0e4eab7..7d30285a1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -14,14 +14,10 @@ name: Test and Build Docs permissions: - contents: write - pages: write - pull-requests: write - id-token: write + contents: read on: - pull_request_target: - types: [opened, reopened, synchronize] # Allows forks to trigger the docs build + pull_request: push: branches: - main @@ -29,14 +25,6 @@ on: types: [checks_requested] jobs: - docs-verify: - uses: eclipse-score/cicd-workflows/.github/workflows/docs-verify.yml@c1c90b1a82a1fab0fc202979dde6686b2162d5a8 # v0.0.0 - permissions: - pull-requests: write - contents: read - with: - bazel-docs-verify-target: "--lockfile_mode=error //:docs_check" - run-tests: uses: ./.github/workflows/coverage_report.yml permissions: @@ -44,16 +32,14 @@ jobs: pull-requests: read build-docs: - needs: [run-tests, docs-verify] + needs: [run-tests] if: ${{ needs.run-tests.result == 'success' }} - uses: eclipse-score/cicd-workflows/.github/workflows/docs.yml@c1c90b1a82a1fab0fc202979dde6686b2162d5a8 # v0.0.0 + uses: eclipse-score/cicd-workflows/.github/workflows/docs.yml@f77ed751fa587e3a1142b2d85320917e00c888ae # main (2026-10) permissions: - contents: write - pages: write - pull-requests: write - id-token: write + contents: read with: - # the bazel-target depends on your repo specific docs_targets configuration (e.g. "suffix") - bazel-target: "--lockfile_mode=error //:docs -- --github_user=${{ github.repository_owner }} --github_repo=${{ github.event.repository.name }} && bazel run --lockfile_mode=error //:publish_dependable_element_reports" + # Appended to `bazel run`; setup-bazel-cache applies the caches to every bazel call in the job. + bazel-target: >- + --lockfile_mode=error //:docs -- --github_user=${{ github.repository_owner }} --github_repo=${{ github.event.repository.name }} && bazel run --lockfile_mode=error //:publish_dependable_element_reports retention-days: 3 tests-report-artifact: logging_cpp_coverage diff --git a/BUILD b/BUILD index 79d0d55d7..f2c80fea6 100644 --- a/BUILD +++ b/BUILD @@ -10,7 +10,7 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -load("@rules_python//python:defs.bzl", "py_binary") +load("@rules_python//python:defs.bzl", "py_binary", "py_test") load("@score_docs_as_code//:docs.bzl", "docs") load("@score_tooling//:defs.bzl", "copyright_checker", "setup_starpls") load("@score_tooling//third_party/format:macros.bzl", "use_format_targets") @@ -47,12 +47,35 @@ exports_files([ ".clang-tidy", ]) +_DEPENDABLE_ELEMENT = "dependable_element_mw_log_backend_doc" + +# Public URL folder; docs/dependable_elements/index.rst links to it. +_PUBLISH_NAME = "mw_log_backend" + +_DEPENDABLE_ELEMENT_DOC = "//score/mw/log/dependability:" + _DEPENDABLE_ELEMENT + +# Must run after `:docs`, which owns and may wipe `_build`. py_binary( name = "publish_dependable_element_reports", testonly = True, srcs = ["scripts/publish_dependable_element_reports.py"], - data = ["//score/mw/log/dependability:dependable_element_mw_log_backend_doc"], + data = [_DEPENDABLE_ELEMENT_DOC], + env = { + "DEPENDABLE_ELEMENT_HTML": "$(rlocationpath " + _DEPENDABLE_ELEMENT_DOC + ")", + "DEPENDABLE_ELEMENT_NAME": _PUBLISH_NAME, + }, main = "scripts/publish_dependable_element_reports.py", + deps = ["@rules_python//python/runfiles"], +) + +py_test( + name = "publish_dependable_element_reports_test", + srcs = [ + "scripts/publish_dependable_element_reports.py", + "scripts/publish_dependable_element_reports_test.py", + ], + main = "scripts/publish_dependable_element_reports_test.py", + deps = ["@rules_python//python/runfiles"], ) # Creates all documentation targets: diff --git a/docs/dependable_elements/index.rst b/docs/dependable_elements/index.rst index f2412ada5..323857a04 100644 --- a/docs/dependable_elements/index.rst +++ b/docs/dependable_elements/index.rst @@ -12,11 +12,9 @@ # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* -mw::log Backend Dependable Element Report +Safety Report =========================================== -The dependable-element report for the ``mw::log`` backend, including requirements, -architecture, dependability analysis, and `LOBSTER `_ -traceability. +The work products for the ``mw::log`` backend dependable-element can be found in the report: -`Open the mw::log Backend Dependable Element Report `_ +`mw::log Backend Dependable Element Report `_ diff --git a/scripts/publish_dependable_element_reports.py b/scripts/publish_dependable_element_reports.py index ea459f036..9ec7a23af 100644 --- a/scripts/publish_dependable_element_reports.py +++ b/scripts/publish_dependable_element_reports.py @@ -1,14 +1,74 @@ import os import shutil +import stat import sys from pathlib import Path +from python.runfiles import Runfiles + + +def _add_owner_write(path: str) -> None: + # Skip symlinks: chmod would follow them and modify the target. + if os.path.islink(path): + return + os.chmod(path, stat.S_IMODE(os.stat(path).st_mode) | stat.S_IWUSR) + def _make_writable(path: Path) -> None: for root, dirs, files in os.walk(path): for name in dirs + files: - os.chmod(os.path.join(root, name), 0o755) - os.chmod(path, 0o755) + _add_owner_write(os.path.join(root, name)) + _add_owner_write(str(path)) + + +def publish(src: Path, build_dir: Path, name: str) -> int: + """Copies the pre-built HTML at `src` to `build_dir/dependable_elements/`.""" + # The docs build may wipe _build, so it has to run before this script. + if not (build_dir / "index.html").is_file(): + print( + f"error: {build_dir}/index.html not found; run //:docs before publishing", + file=sys.stderr, + ) + return 1 + + if not src.is_dir(): + print(f"error: expected {name} output at {src}", file=sys.stderr) + return 1 + + # Catches a mismatch between the published folder and the link in docs/dependable_elements. + landing = build_dir / "dependable_elements" / "index.html" + link = f'href="{name}/index.html"' + if not landing.is_file() or link not in landing.read_text(encoding="utf-8"): + print(f"error: {landing} does not contain {link}", file=sys.stderr) + return 1 + + dest = build_dir / "dependable_elements" / name + tmp = dest.parent / f".{name}.tmp" + dest.parent.mkdir(parents=True, exist_ok=True) + + try: + if tmp.exists(): + _make_writable(tmp) + shutil.rmtree(tmp) + # Bazel outputs are read-only; make the copy writable so reruns can replace it. + shutil.copytree(src, tmp) + _make_writable(tmp) + + if not (tmp / "index.html").is_file(): + print(f"error: {name}/index.html missing in {src}", file=sys.stderr) + return 1 + + if dest.exists(): + _make_writable(dest) + shutil.rmtree(dest) + os.replace(tmp, dest) + finally: + if tmp.exists(): + _make_writable(tmp) + shutil.rmtree(tmp) + + print(f"Published {name} to {dest}") + return 0 def main() -> int: @@ -17,32 +77,22 @@ def main() -> int: print("error: this script must be run via 'bazel run'", file=sys.stderr) return 1 - src = ( - Path(__file__).parent.parent - / "score/mw/log/dependability/dependable_element_mw_log_backend_doc/html" - ) - dest = ( - Path(workspace_dir) - / "_build" - / "dependable_elements" - / "dependable_element_mw_log_backend_doc" - ) - - if not src.is_dir(): + name = os.environ.get("DEPENDABLE_ELEMENT_NAME") + html_rlocation = os.environ.get("DEPENDABLE_ELEMENT_HTML") + if not name or not html_rlocation: print( - f"error: expected dependable_element_mw_log_backend_doc output at {src}", + "error: DEPENDABLE_ELEMENT_NAME and DEPENDABLE_ELEMENT_HTML must be set", file=sys.stderr, ) return 1 - if dest.exists(): - _make_writable(dest) - shutil.rmtree(dest) - shutil.copytree(src, dest) - _make_writable(dest) + runfiles = Runfiles.Create() + src = runfiles.Rlocation(html_rlocation) if runfiles else None + if not src: + print(f"error: cannot resolve runfile {html_rlocation}", file=sys.stderr) + return 1 - print(f"Published dependable_element_mw_log_backend_doc to {dest}") - return 0 + return publish(Path(src), Path(workspace_dir) / "_build", name) if __name__ == "__main__": diff --git a/scripts/publish_dependable_element_reports_test.py b/scripts/publish_dependable_element_reports_test.py new file mode 100644 index 000000000..a09e6c607 --- /dev/null +++ b/scripts/publish_dependable_element_reports_test.py @@ -0,0 +1,106 @@ +import os +import tempfile +import unittest +from pathlib import Path + +from scripts.publish_dependable_element_reports import publish + +_NAME = "element_doc" + + +class PublishTest(unittest.TestCase): + def setUp(self) -> None: + tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._cleanup, tmp) + self.root = Path(tmp.name) + self.build = self.root / "_build" + self.landing_dir = self.build / "dependable_elements" + self.dest = self.landing_dir / _NAME + + self.landing_dir.mkdir(parents=True) + (self.build / "index.html").write_text("main") + (self.landing_dir / "index.html").write_text( + f'report' + ) + + self.src = self._make_source( + "src", {"index.html": "v1", "sub/page.html": "page"} + ) + + def _make_source(self, name: str, files: dict[str, str]) -> Path: + """Creates a read-only tree, like Bazel outputs.""" + root = self.root / name + for rel, text in files.items(): + path = root / rel + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(text) + path.chmod(0o444) + for dirpath, _, _ in os.walk(root, topdown=False): + os.chmod(dirpath, 0o555) + return root + + @staticmethod + def _cleanup(tmp: tempfile.TemporaryDirectory) -> None: + for dirpath, dirs, _ in os.walk(tmp.name): + for d in dirs: + os.chmod(os.path.join(dirpath, d), 0o755) + tmp.cleanup() + + def _published_names(self) -> list[str]: + return sorted(p.name for p in self.landing_dir.iterdir()) + + def test_publishes_tree_without_stray_files(self) -> None: + self.assertEqual(publish(self.src, self.build, _NAME), 0) + + self.assertEqual((self.dest / "index.html").read_text(), "v1") + self.assertEqual((self.dest / "sub" / "page.html").read_text(), "page") + self.assertEqual(self._published_names(), sorted(["index.html", _NAME])) + + def test_republish_replaces_previous_output(self) -> None: + publish(self.src, self.build, _NAME) + new_src = self._make_source("src_v2", {"index.html": "v2"}) + + self.assertEqual(publish(new_src, self.build, _NAME), 0) + + self.assertEqual((self.dest / "index.html").read_text(), "v2") + self.assertFalse((self.dest / "sub").exists()) + + def test_published_files_are_not_executable(self) -> None: + publish(self.src, self.build, _NAME) + + files = [p for p in self.dest.rglob("*") if p.is_file()] + self.assertTrue(files) + for path in files: + self.assertEqual(path.stat().st_mode & 0o111, 0, path) + + def _assert_rejected(self, src: Path) -> None: + self.assertEqual(publish(src, self.build, _NAME), 1) + self.assertFalse(self.dest.exists()) + + def test_rejects_when_docs_not_built(self) -> None: + (self.build / "index.html").unlink() + self._assert_rejected(self.src) + + def test_rejects_missing_source(self) -> None: + self._assert_rejected(self.root / "missing") + + def test_rejects_landing_page_without_link_to_report(self) -> None: + (self.landing_dir / "index.html").write_text('x') + self._assert_rejected(self.src) + + def test_rejects_missing_landing_page(self) -> None: + (self.landing_dir / "index.html").unlink() + self._assert_rejected(self.src) + + def test_failed_publish_keeps_previous_output(self) -> None: + publish(self.src, self.build, _NAME) + without_index = self._make_source("broken", {"page.html": "x"}) + + self.assertEqual(publish(without_index, self.build, _NAME), 1) + + self.assertEqual((self.dest / "index.html").read_text(), "v1") + self.assertEqual(self._published_names(), sorted(["index.html", _NAME])) + + +if __name__ == "__main__": + unittest.main()