Skip to content
Draft
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
73 changes: 3 additions & 70 deletions .github/workflows/update-version-dashboard.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,75 +23,8 @@ jobs:
- name: Set up Nix
uses: cachix/install-nix-action@v31

- name: Update version dashboard
run: nix-shell --run 'npm run generate:version-compatibility-dashboard'

- name: Detect changes
id: changes
run: |
if [[ -n "$(git status --porcelain)" ]]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git status --short
git diff --stat
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi

- name: Check whitespace
if: steps.changes.outputs.has_changes == 'true'
run: git diff --check

- name: Create or update pull request
if: steps.changes.outputs.has_changes == 'true'
- name: Generate update pull requests
env:
GH_TOKEN: ${{ github.token }}
PR_BRANCH: version-dashboard/update
PR_TITLE: Update version dashboard data
run: |
set -euo pipefail

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

git switch -c "$PR_BRANCH"
git add config/repo-version-config.json docs-main/snippets/generated/version-dashboard-data.mdx
git commit -m "$PR_TITLE"

if remote_sha="$(git ls-remote --heads origin "$PR_BRANCH" | awk '{print $1}')" && [[ -n "$remote_sha" ]]; then
git push --force-with-lease="refs/heads/$PR_BRANCH:$remote_sha" origin "HEAD:$PR_BRANCH"
else
git push origin "HEAD:$PR_BRANCH"
fi

pr_body="$(mktemp)"
cat > "$pr_body" <<'EOF'
Updates the committed Canton Network version dashboard data from public network, package, and installer sources.

Validation run by the workflow:
- `npm run generate:version-compatibility-dashboard`
- `git diff --check`
EOF

existing_pr_number="$(
gh pr list \
--repo "$GITHUB_REPOSITORY" \
--head "$PR_BRANCH" \
--base main \
--state open \
--json number \
--jq '.[0].number // empty'
)"

if [[ -n "$existing_pr_number" ]]; then
gh pr edit "$existing_pr_number" \
--repo "$GITHUB_REPOSITORY" \
--title "$PR_TITLE" \
--body-file "$pr_body"
else
gh pr create \
--base main \
--head "$PR_BRANCH" \
--repo "$GITHUB_REPOSITORY" \
--title "$PR_TITLE" \
--body-file "$pr_body"
fi
GITHUB_TOKEN: ${{ github.token }}
run: python3 scripts/update_generated_reference_prs.py --targets all
11 changes: 10 additions & 1 deletion scripts/docs_env.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,16 @@
DIRENV_ENV_MARKER = "DIGITAL_ASSET_DOCS_DIRENV"


def already_reentered_direnv() -> bool:
return os.environ.get(DIRENV_ENV_MARKER) == "1"


def repo_direnv_command(repo_root: Path, *args: str) -> list[str]:
if already_reentered_direnv():
if args and args[0] == "x2mdx":
return ["python3", "-m", "x2mdx.cli", *args[1:]]
return list(args)

if args and args[0] == "x2mdx":
return [
"direnv",
Expand All @@ -25,7 +34,7 @@ def repo_direnv_command(repo_root: Path, *args: str) -> list[str]:


def ensure_repo_direnv(*, repo_root: Path, script_path: Path, argv: Sequence[str]) -> None:
if os.environ.get(DIRENV_ENV_MARKER) == "1":
if already_reentered_direnv():
return

env = os.environ.copy()
Expand Down
13 changes: 12 additions & 1 deletion scripts/generate_network_component_versions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@
import argparse
import html
import json
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
from pathlib import Path
from urllib.parse import urlparse
from urllib.request import Request, urlopen


Expand Down Expand Up @@ -119,8 +121,17 @@ def parse_args() -> argparse.Namespace:
return parser.parse_args()


def request_headers(url: str) -> dict[str, str]:
headers = {"User-Agent": USER_AGENT}
token = os.environ.get("GITHUB_TOKEN") or os.environ.get("GH_TOKEN")
if token and urlparse(url).netloc == "api.github.com":
headers["Authorization"] = f"Bearer {token}"
headers["X-GitHub-Api-Version"] = "2022-11-28"
return headers


def request_url(url: str, timeout: float):
request = Request(url, headers={"User-Agent": USER_AGENT})
request = Request(url, headers=request_headers(url))
return urlopen(request, timeout=timeout)


Expand Down
32 changes: 24 additions & 8 deletions scripts/generate_wallet_gateway_openrpc_reference.py
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,14 @@ def write_details_pages(*, output_dir: Path, spec_entries: list[dict[str, Any]])
)


def normalize_generated_mdx(output_dir: Path) -> None:
for path in sorted(output_dir.rglob("*.mdx")):
original = path.read_text(encoding="utf-8")
normalized = "\n".join(line.rstrip() for line in original.splitlines()) + "\n"
if normalized != original:
path.write_text(normalized, encoding="utf-8")


def prune_nav_items(items: list[Any], *, page_refs: set[str], group_labels: set[str]) -> list[Any]:
pruned: list[Any] = []
for item in items:
Expand Down Expand Up @@ -269,14 +277,21 @@ def update_docs_navigation(
if not isinstance(navigation, dict):
raise ValueError(f"docs.json navigation must be an object: {docs_json_path}")
dropdowns = navigation.get("dropdowns")
if not isinstance(dropdowns, list):
raise ValueError(f"docs.json navigation.dropdowns must be a list: {docs_json_path}")
dropdown = next((item for item in dropdowns if isinstance(item, dict) and item.get("dropdown") == dropdown_label), None)
if dropdown is None:
raise ValueError(f"Dropdown not found in docs.json: {dropdown_label}")
pages = dropdown.get("pages")
if isinstance(dropdowns, list):
nav_root = next((item for item in dropdowns if isinstance(item, dict) and item.get("dropdown") == dropdown_label), None)
if nav_root is None:
raise ValueError(f"Dropdown not found in docs.json: {dropdown_label}")
else:
products = navigation.get("products")
if not isinstance(products, list):
raise ValueError(f"docs.json navigation must define dropdowns or products: {docs_json_path}")
nav_root = next((item for item in products if isinstance(item, dict) and item.get("product") == dropdown_label), None)
if nav_root is None:
raise ValueError(f"Product not found in docs.json: {dropdown_label}")

pages = nav_root.get("pages")
if not isinstance(pages, list):
raise ValueError(f"Dropdown does not expose a pages list: {dropdown_label}")
raise ValueError(f"Navigation root does not expose a pages list: {dropdown_label}")

refs = {overview_page_ref(output_dir, docs_json_path), docs_json_page_ref(output_dir / "operations" / "details.mdx", docs_json_path)}
refs.update(spec_page_ref(output_dir, docs_json_path, spec["spec_id"]) for spec in spec_entries)
Expand Down Expand Up @@ -310,7 +325,7 @@ def update_docs_navigation(
break
for offset, wallet_group in enumerate(wallet_groups):
pruned_pages.insert(min(insert_at + offset, len(pruned_pages)), wallet_group)
dropdown["pages"] = pruned_pages
nav_root["pages"] = pruned_pages
docs_json_path.write_text(json.dumps(docs, indent=2) + "\n", encoding="utf-8")
print(f"Updated docs navigation: {docs_json_path}")

Expand Down Expand Up @@ -453,6 +468,7 @@ def main() -> int:
output_dir=Path(args.output_dir).resolve(),
spec_entries=spec_entries,
)
normalize_generated_mdx(Path(args.output_dir).resolve())
update_docs_navigation(
docs_json_path=Path(args.docs_json).resolve(),
dropdown_label=args.nav_dropdown,
Expand Down
148 changes: 148 additions & 0 deletions scripts/generated_reference_pr_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
from __future__ import annotations

import subprocess
import tempfile
from pathlib import Path
from typing import Sequence


REPO_ROOT = Path(__file__).resolve().parents[1]


def run(command: Sequence[str], *, capture: bool = False) -> str:
kwargs: dict[str, object] = {
"cwd": REPO_ROOT,
"check": True,
"text": True,
}
if capture:
kwargs["stdout"] = subprocess.PIPE
completed = subprocess.run(list(command), **kwargs)
return completed.stdout.strip() if capture else ""


def git(*args: str, capture: bool = False) -> str:
return run(("git", *args), capture=capture)


def gh(*args: str, capture: bool = False) -> str:
return run(("gh", *args), capture=capture)


def current_repository() -> str:
return gh("repo", "view", "--json", "nameWithOwner", "--jq", ".nameWithOwner", capture=True)


def reset_to_base(*, base_sha: str, clean_paths: Sequence[str]) -> None:
git("switch", "--detach", base_sha)
git("reset", "--hard", base_sha)
git("clean", "-fd", "--", *clean_paths)


def write_base_file(base_sha: str, relative_path: str) -> Path:
before = tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False)
before_path = Path(before.name)
before.write(git("show", f"{base_sha}:{relative_path}", capture=True))
before.close()
return before_path


def has_changes(paths: Sequence[str]) -> bool:
output = git("status", "--porcelain", "--", *paths, capture=True)
return bool(output)


def push_branch(branch: str) -> None:
remote_output = git("ls-remote", "--heads", "origin", branch, capture=True)
remote_sha = remote_output.split()[0] if remote_output else ""
if remote_sha:
git(
"push",
f"--force-with-lease=refs/heads/{branch}:{remote_sha}",
"origin",
f"HEAD:{branch}",
)
else:
git("push", "origin", f"HEAD:{branch}")


def create_or_update_pull_request(
*,
title: str,
branch: str,
paths: Sequence[str],
body_path: Path,
base_branch: str,
repository: str,
) -> None:
if not has_changes(paths):
print(f"No changes for {title}")
return

git("status", "--short", "--", *paths)
git("switch", "-c", branch)
git("add", "--", *paths)
git("diff", "--cached", "--stat")
git("diff", "--cached", "--check")
git("commit", "-m", title)
push_branch(branch)

existing_pr_number = gh(
"pr",
"list",
"--repo",
repository,
"--head",
branch,
"--base",
base_branch,
"--state",
"open",
"--json",
"number",
"--jq",
".[0].number // empty",
capture=True,
)
if existing_pr_number:
gh(
"pr",
"edit",
existing_pr_number,
"--repo",
repository,
"--title",
title,
"--body-file",
str(body_path),
)
subprocess.run(
[
"gh",
"pr",
"ready",
existing_pr_number,
"--repo",
repository,
"--undo",
],
cwd=REPO_ROOT,
check=False,
)
return

gh(
"pr",
"create",
"--base",
base_branch,
"--head",
branch,
"--repo",
repository,
"--draft",
"--title",
title,
"--body-file",
str(body_path),
)
1 change: 1 addition & 0 deletions scripts/generated_reference_sources/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Generated-reference source update helpers."""
25 changes: 25 additions & 0 deletions scripts/generated_reference_sources/common.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
from __future__ import annotations

import json
from dataclasses import dataclass
from pathlib import Path


@dataclass(frozen=True)
class SourceUpdate:
source: str
path: Path
field: str
previous: str
current: str


def load_json(path: Path) -> dict[str, object]:
payload = json.loads(path.read_text(encoding="utf-8"))
if not isinstance(payload, dict):
raise ValueError(f"Expected JSON object in {path}")
return payload


def write_json(path: Path, payload: dict[str, object]) -> None:
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
Loading