Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/docs-preview-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ jobs:
const docsChanged = files.some(
({ filename }) => filename.startsWith('docs/') ||
filename.startsWith('overrides/') ||
/^projects\/[^/]+\/docs\//.test(filename) ||
/^projects\/[^/]+\/(docs|assets)\//.test(filename) ||
exactInputs.has(filename),
);
operation = docsChanged ? 'deploy' : 'remove';
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/docs-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
const docsChanged = files.some(
({ filename }) => filename.startsWith('docs/') ||
filename.startsWith('overrides/') ||
/^projects\/[^/]+\/docs\//.test(filename) ||
/^projects\/[^/]+\/(docs|assets)\//.test(filename) ||
exactInputs.has(filename),
);
core.setOutput('operation', docsChanged ? 'deploy' : 'remove');
Expand Down
5 changes: 5 additions & 0 deletions docs/development/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ Commit any generated changes with the source change.

## Theme and brand assets

Project logos shared by a README and the site belong in `projects/<name>/assets/`.
Register that directory in `PROJECT_ASSETS` in `scripts/stage-project-docs.py`;
the build copies it into the staged documentation's `assets/` directory.
README image links should be relative so they work on feature branches.

Keep shared brand assets in `docs/assets/brand/`. Use the compact SVG mark for
`project.theme.logo`, `favicon.svg` for the browser icon, and the light and dark
PNG banners for full OpenShell Research lockups. Never reference files from a
Expand Down
13 changes: 13 additions & 0 deletions docs/stylesheets/documentation.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,19 @@
/* SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. */
/* SPDX-License-Identifier: Apache-2.0 */

.md-typeset .documentation-logo {
display: block;
width: 540px;
max-width: 100%;
height: auto;
margin: 1.5rem auto 2rem;
color-scheme: light;
}

body[data-md-color-scheme="slate"] .md-typeset .documentation-logo {
color-scheme: dark;
}

/* Documentation figures use one quiet visual treatment across light and dark pages. */
.md-typeset .documentation-figure {
width: 100%;
Expand Down
4 changes: 4 additions & 0 deletions projects/openshell-agent-runner/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# OpenShell Agent Runner

<p align="center">
<img src="assets/oar-logo.svg" alt="OAR — OpenShell Agent Runner" width="360">
</p>

OpenShell Agent Runner (OAR) launches ephemeral
[Pi coding agents](https://github.com/earendil-works/pi/tree/main/packages/coding-agent)
in OpenShell sandboxes. Each agent works on a task using the prompts, skills,
Expand Down
25 changes: 25 additions & 0 deletions projects/openshell-agent-runner/assets/oar-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions projects/openshell-agent-runner/docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ agent_markdown: true

# OpenShell Agent Runner

<img class="documentation-logo" src="assets/oar-logo.svg" alt="OAR — OpenShell Agent Runner" width="540">

OpenShell Agent Runner (OAR) launches ephemeral
[Pi coding agents](https://github.com/earendil-works/pi/tree/main/packages/coding-agent)
in OpenShell sandboxes. Each agent works on a task using the prompts, skills,
Expand Down
1 change: 1 addition & 0 deletions projects/openshell-agent-runner/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ packages = ["src/openshell_agent_runner"]

[tool.hatch.build.targets.sdist]
include = [
"/assets",
"/src/openshell_agent_runner",
"/LICENSE",
"/README.md",
Expand Down
36 changes: 29 additions & 7 deletions scripts/stage-project-docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,15 +14,17 @@
ROOT = Path(__file__).resolve().parents[1]
PROJECT_DOCUMENTATION = {
"egress-gate": ROOT / "projects" / "egress-gate" / "docs",
"openshell-agent-runner": ROOT
/ "projects"
/ "openshell-agent-runner"
/ "docs",
"openshell-agent-runner": ROOT / "projects" / "openshell-agent-runner" / "docs",
}
DOCUMENTATION_ROOT = ROOT / "docs" / "documentation"
PROJECT_ASSETS = {
"openshell-agent-runner": ROOT / "projects" / "openshell-agent-runner" / "assets",
}


def stage_project_docs(source: Path, destination: Path) -> None:
def stage_project_docs(
source: Path, destination: Path, assets: Path | None = None
) -> None:
"""Replace one generated site mirror with its canonical project-docs tree."""

source = source.resolve()
Expand All @@ -31,7 +33,9 @@ def stage_project_docs(source: Path, destination: Path) -> None:
if not source.is_dir():
raise ValueError(f"documentation source does not exist: {source}")
if destination_is_symlink:
raise ValueError(f"documentation destination must not be a symlink: {destination}")
raise ValueError(
f"documentation destination must not be a symlink: {destination}"
)
if source.is_relative_to(destination) or destination.is_relative_to(source):
raise ValueError("documentation source and destination must not overlap")

Expand All @@ -40,11 +44,27 @@ def stage_project_docs(source: Path, destination: Path) -> None:
paths = "\n".join(f" - {path}" for path in symlinks)
raise ValueError(f"project documentation must not contain symlinks:\n{paths}")

if assets is not None:
if assets.is_symlink() or any(path.is_symlink() for path in assets.rglob("*")):
raise ValueError("project assets must not contain symlinks")
if not assets.is_dir():
raise ValueError(f"project assets directory does not exist: {assets}")
for path in assets.rglob("*"):
if (
path.is_file()
and (source / "assets" / path.relative_to(assets)).exists()
):
raise ValueError(
f"project asset conflicts with documentation asset: {path}"
)

destination.parent.mkdir(parents=True, exist_ok=True)
staging = destination.with_name(f".{destination.name}.staging")
if staging.exists():
shutil.rmtree(staging)
shutil.copytree(source, staging)
if assets is not None:
shutil.copytree(assets, staging / "assets", dirs_exist_ok=True)

if destination.exists():
shutil.rmtree(destination)
Expand All @@ -53,7 +73,9 @@ def stage_project_docs(source: Path, destination: Path) -> None:

def main() -> int:
for project, source in PROJECT_DOCUMENTATION.items():
stage_project_docs(source, DOCUMENTATION_ROOT / project)
stage_project_docs(
source, DOCUMENTATION_ROOT / project, PROJECT_ASSETS.get(project)
)
print(f"Staged {project} documentation from {source}.")
return 0

Expand Down
33 changes: 33 additions & 0 deletions tests/test_stage_project_docs.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,38 @@ def test_stage_rejects_source_inside_destination(self) -> None:
STAGER.stage_project_docs(source, destination)


def test_stage_includes_project_assets_without_changing_canonical_docs(tmp_path):
source = tmp_path / "project" / "docs"
assets = tmp_path / "project" / "assets"
destination = tmp_path / "published"
(source / "assets").mkdir(parents=True)
assets.mkdir()
(source / "index.md").write_text('<img src="assets/logo.svg">')
(source / "assets" / "diagram.svg").write_text("<svg/>")
(assets / "logo.svg").write_text('<svg id="logo"/>')

STAGER.stage_project_docs(source, destination, assets)

assert (destination / "assets" / "logo.svg").read_bytes() == (
assets / "logo.svg"
).read_bytes()
assert (destination / "assets" / "diagram.svg").is_file()
assert not (source / "assets" / "logo.svg").exists()


def test_stage_rejects_conflicting_project_assets(tmp_path):
import pytest

source = tmp_path / "docs"
assets = tmp_path / "assets"
(source / "assets").mkdir(parents=True)
assets.mkdir()
(source / "assets" / "logo.svg").write_text("<svg/>")
(assets / "logo.svg").write_text("<svg/>")

with pytest.raises(ValueError, match="conflicts"):
STAGER.stage_project_docs(source, tmp_path / "published", assets)


if __name__ == "__main__":
unittest.main()
Loading