mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-09-10 18:22:20 +02:00
refactor(docs): separate Pages site source (#6176)
* refactor(docs): separate Pages site source * fix(docs): preserve published guide pages * fix(ci): run asset ownership tests for site changes * fix(docs): track future website media * fix(ci): let Pages deployments finish * build(deps): update Pages checkout action * fix(ci): follow moved setup guide * fix(docs): repair published setup guide * fix(docs): retarget preview encoder
This commit is contained in:
@@ -30,6 +30,8 @@ secrets.env~
|
|||||||
.idea/
|
.idea/
|
||||||
dev-docs/
|
dev-docs/
|
||||||
docs/
|
docs/
|
||||||
|
website/
|
||||||
|
assets/branding/
|
||||||
*.md
|
*.md
|
||||||
*.db
|
*.db
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|||||||
+1
-1
@@ -6,4 +6,4 @@
|
|||||||
# A per-area ownership map (security/auth, CI, frontend, agent internals, with
|
# A per-area ownership map (security/auth, CI, frontend, agent internals, with
|
||||||
# multiple named owners per line) is being worked out in issue #593; once
|
# multiple named owners per line) is being worked out in issue #593; once
|
||||||
# agreed it replaces this file. Until then, required reviews and the security
|
# agreed it replaces this file. Until then, required reviews and the security
|
||||||
# CI gate (docs/security-ci.md) remain in force via branch protection.
|
# CI gate (website/security-ci.md) remain in force via branch protection.
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ jobs:
|
|||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
persist-credentials: false
|
persist-credentials: false
|
||||||
|
|
||||||
# Detect whether this PR only touches documentation files.
|
# Detect whether this PR only touches repository prose outside the Pages site.
|
||||||
# If so, skip the expensive pytest run while still reporting a passing check.
|
# If so, skip the expensive pytest run while still reporting a passing check.
|
||||||
- name: Check for docs-only changes
|
- name: Check for docs-only changes
|
||||||
id: docs-check
|
id: docs-check
|
||||||
@@ -122,9 +122,10 @@ jobs:
|
|||||||
BASE="${{ github.event.before }}"
|
BASE="${{ github.event.before }}"
|
||||||
HEAD="${{ github.sha }}"
|
HEAD="${{ github.sha }}"
|
||||||
fi
|
fi
|
||||||
# List all changed files; if every file matches docs/markdown patterns, skip pytest.
|
# Keep website/ and assets/branding/ out of this bypass: pytest owns
|
||||||
|
# regression guards for their published-file and orphan-asset contracts.
|
||||||
changed=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
|
changed=$(git diff --name-only "$BASE" "$HEAD" 2>/dev/null || git diff --name-only HEAD~1 HEAD)
|
||||||
non_docs=$(echo "$changed" | grep -Ev '^(docs/|.*\.md$|\.github/[^/]+\.md$)' || true)
|
non_docs=$(echo "$changed" | grep -Ev '^(docs/|[^/]+\.md$|\.github/[^/]+\.md$)' || true)
|
||||||
if [ -z "$non_docs" ]; then
|
if [ -z "$non_docs" ]; then
|
||||||
echo "docs_only=true" >> "$GITHUB_OUTPUT"
|
echo "docs_only=true" >> "$GITHUB_OUTPUT"
|
||||||
echo "Docs-only change detected — skipping pytest."
|
echo "Docs-only change detected — skipping pytest."
|
||||||
|
|||||||
@@ -23,12 +23,16 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
- 'website/**'
|
||||||
|
- 'assets/branding/**'
|
||||||
- '.github/ISSUE_TEMPLATE/**'
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
- 'website/**'
|
||||||
|
- 'assets/branding/**'
|
||||||
- '.github/ISSUE_TEMPLATE/**'
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
name: Deploy GitHub Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
paths:
|
||||||
|
- 'website/**'
|
||||||
|
- '.github/workflows/deploy-pages.yml'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions: {}
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: pages
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Package static site
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
pages: read
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0
|
||||||
|
- uses: actions/jekyll-build-pages@44a6e6beabd48582f863aeeb6cb2151cc1716697 # v1.0.13
|
||||||
|
with:
|
||||||
|
source: website
|
||||||
|
destination: _site
|
||||||
|
- uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||||
|
with:
|
||||||
|
path: _site
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
name: Deploy static site
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- name: Deploy to GitHub Pages
|
||||||
|
id: deployment
|
||||||
|
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0
|
||||||
@@ -14,6 +14,8 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '**.md'
|
- '**.md'
|
||||||
- 'docs/**'
|
- 'docs/**'
|
||||||
|
- 'website/**'
|
||||||
|
- 'assets/branding/**'
|
||||||
- '.github/ISSUE_TEMPLATE/**'
|
- '.github/ISSUE_TEMPLATE/**'
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
|
|||||||
+18
@@ -85,6 +85,24 @@ output.txt.txt
|
|||||||
!docs/**/*.gif
|
!docs/**/*.gif
|
||||||
!docs/**/*.webp
|
!docs/**/*.webp
|
||||||
|
|
||||||
|
# …except shipped website and branding media.
|
||||||
|
!website/**/*.jpg
|
||||||
|
!website/**/*.jpeg
|
||||||
|
!website/**/*.png
|
||||||
|
!website/**/*.gif
|
||||||
|
!website/**/*.bmp
|
||||||
|
!website/**/*.webp
|
||||||
|
!website/**/*.tiff
|
||||||
|
!website/**/*.pdf
|
||||||
|
!assets/branding/**/*.jpg
|
||||||
|
!assets/branding/**/*.jpeg
|
||||||
|
!assets/branding/**/*.png
|
||||||
|
!assets/branding/**/*.gif
|
||||||
|
!assets/branding/**/*.bmp
|
||||||
|
!assets/branding/**/*.webp
|
||||||
|
!assets/branding/**/*.tiff
|
||||||
|
!assets/branding/**/*.pdf
|
||||||
|
|
||||||
# Reports and temp files
|
# Reports and temp files
|
||||||
reports/
|
reports/
|
||||||
tasks/
|
tasks/
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/odysseus-wordmark.png" alt="Odysseus" width="238">
|
<img src="assets/branding/odysseus-wordmark.png" alt="Odysseus" width="238">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="#quick-start">Quick Start</a> ·
|
<a href="#quick-start">Quick Start</a> ·
|
||||||
<a href="docs/setup.md">Setup Guide</a> ·
|
<a href="website/setup.md">Setup Guide</a> ·
|
||||||
<a href="CONTRIBUTING.md">Contributing</a> ·
|
<a href="CONTRIBUTING.md">Contributing</a> ·
|
||||||
<a href="ROADMAP.md">Roadmap</a>
|
<a href="ROADMAP.md">Roadmap</a>
|
||||||
</p>
|
</p>
|
||||||
@@ -18,7 +18,7 @@
|
|||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<img src="docs/odysseus-browser.jpg" alt="Odysseus interface">
|
<img src="assets/branding/odysseus-browser.jpg" alt="Odysseus interface">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -36,7 +36,7 @@ docker compose up -d --build
|
|||||||
|
|
||||||
Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`.
|
Open `http://localhost:7000` when the containers are healthy. The first admin password is printed in `docker compose logs odysseus`.
|
||||||
|
|
||||||
Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](docs/setup.md).
|
Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration live in the [setup guide](website/setup.md).
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -51,7 +51,7 @@ Native installs, GPU notes, Windows/macOS instructions, HTTPS, and configuration
|
|||||||
|
|
||||||
## Demo
|
## Demo
|
||||||
|
|
||||||
A full hover-to-play tour lives on the landing page: [`docs/index.html`](docs/index.html).
|
A full hover-to-play tour lives on the [Odysseus landing page](https://odysseus-dev.github.io/odysseus/). Its source lives under [`website/`](website/).
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
@@ -64,7 +64,7 @@ Odysseus is a self-hosted workspace with powerful local tools. Keep auth enabled
|
|||||||
- Keep `AUTH_ENABLED=true` for any network-accessible deployment.
|
- Keep `AUTH_ENABLED=true` for any network-accessible deployment.
|
||||||
- Keep `LOCALHOST_BYPASS=false` outside local development.
|
- Keep `LOCALHOST_BYPASS=false` outside local development.
|
||||||
|
|
||||||
Deployment details are in the [setup guide](docs/setup.md#security-notes).
|
Deployment details are in the [setup guide](website/setup.md#security-notes).
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 185 KiB After Width: | Height: | Size: 185 KiB |
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 79 KiB |
+4
-4
@@ -27,13 +27,13 @@ echo " port: $PORT"
|
|||||||
rm -rf "$APP"
|
rm -rf "$APP"
|
||||||
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
|
||||||
|
|
||||||
# ── Icon (best effort) — center-crop docs/odysseus.jpg to a square .icns ──
|
# ── Icon (best effort) — center-crop the branding image to a square .icns ──
|
||||||
if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
if [ -f "$REPO_DIR/assets/branding/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
||||||
TMPIMG="$(mktemp -d)"
|
TMPIMG="$(mktemp -d)"
|
||||||
# Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and
|
# Center-crop to a square, scale to 512 (sips' icns encoder caps at 512), and
|
||||||
# let sips emit the .icns directly — more robust across macOS versions than
|
# let sips emit the .icns directly — more robust across macOS versions than
|
||||||
# building an .iconset by hand.
|
# building an .iconset by hand.
|
||||||
sips -c 720 720 "$REPO_DIR/docs/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/docs/odysseus.jpg" "$TMPIMG/sq.png"
|
sips -c 720 720 "$REPO_DIR/assets/branding/odysseus.jpg" --out "$TMPIMG/sq.png" >/dev/null 2>&1 || cp "$REPO_DIR/assets/branding/odysseus.jpg" "$TMPIMG/sq.png"
|
||||||
sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1
|
sips -z 512 512 "$TMPIMG/sq.png" --out "$TMPIMG/icon.png" >/dev/null 2>&1
|
||||||
if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then
|
if sips -s format icns "$TMPIMG/icon.png" --out "$APP/Contents/Resources/odysseus.icns" >/dev/null 2>&1; then
|
||||||
echo " icon: odysseus.icns"
|
echo " icon: odysseus.icns"
|
||||||
@@ -42,7 +42,7 @@ if [ -f "$REPO_DIR/docs/odysseus.jpg" ] && command -v sips >/dev/null 2>&1; then
|
|||||||
fi
|
fi
|
||||||
rm -rf "$TMPIMG"
|
rm -rf "$TMPIMG"
|
||||||
else
|
else
|
||||||
echo " icon: (skipped — no docs/odysseus.jpg)"
|
echo " icon: (skipped — no assets/branding/odysseus.jpg)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# ── Info.plist ──
|
# ── Info.plist ──
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Encode a source screen-recording (.mkv) into web-optimized preview clips for
|
# Encode a source screen-recording (.mkv) into web-optimized preview clips for
|
||||||
# the landing page: docs/<name>.webm (VP9) + docs/<name>.mp4 (H.264).
|
# the landing page: website/<name>.webm (VP9) + website/<name>.mp4 (H.264).
|
||||||
#
|
#
|
||||||
# ./encode_previews.sh <input> <name> [max_secs]
|
# ./encode_previews.sh <input> <name> [max_secs]
|
||||||
#
|
#
|
||||||
@@ -13,7 +13,7 @@ set -euo pipefail
|
|||||||
IN="${1:?input file}"
|
IN="${1:?input file}"
|
||||||
NAME="${2:?output basename}"
|
NAME="${2:?output basename}"
|
||||||
MAX="${3:-30}"
|
MAX="${3:-30}"
|
||||||
OUT_DIR="$(cd "$(dirname "$0")/../docs" && pwd)"
|
OUT_DIR="$(cd "$(dirname "$0")/../website" && pwd)"
|
||||||
|
|
||||||
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN" | cut -d. -f1)
|
dur=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$IN" | cut -d. -f1)
|
||||||
dur=${dur:-0}
|
dur=${dur:-0}
|
||||||
|
|||||||
@@ -1,30 +1,39 @@
|
|||||||
"""Regression guard for issue #1335 — PR review screenshots were committed into
|
"""Repository asset ownership guards for issues #1335 and #6175.
|
||||||
docs/ (docs/a11y/*.png from #738, docs/gallery-314-*.png from #644) where they
|
|
||||||
served no purpose: nothing in the repo referenced them, so they just showed up
|
|
||||||
as "random images" in the doc folder.
|
|
||||||
|
|
||||||
This test fails if any image under docs/ is orphaned — present in the tree but
|
Public Markdown and landing-page media belong in website/, while shared
|
||||||
referenced by no tracked text file. The intended doc assets (the README hero
|
README/packaging imagery belongs in assets/branding/. Images in either managed
|
||||||
image and the feature preview clips) are referenced, so they pass; a stray
|
root must be referenced by tracked text, and every tracked website video must
|
||||||
screenshot dropped in by a future PR would not.
|
be referenced by the site's entry point.
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from urllib.parse import urlsplit
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
IMAGE_EXTS = {".png", ".jpg", ".jpeg", ".gif", ".webp", ".bmp"}
|
||||||
|
VIDEO_EXTS = {".webm", ".mp4", ".mov", ".m4v"}
|
||||||
|
PUBLIC_GUIDES = {
|
||||||
|
"agent-migration.md",
|
||||||
|
"attachments.md",
|
||||||
|
"backup-restore.md",
|
||||||
|
"email-outlook.md",
|
||||||
|
"pr-blocker-audit.md",
|
||||||
|
"security-ci.md",
|
||||||
|
"setup.md",
|
||||||
|
}
|
||||||
# Files a referenced image name could legitimately appear in.
|
# Files a referenced image name could legitimately appear in.
|
||||||
TEXT_EXTS = {".md", ".html", ".htm", ".js", ".ts", ".css", ".py", ".sh",
|
TEXT_EXTS = {".md", ".html", ".htm", ".js", ".ts", ".css", ".py", ".sh",
|
||||||
".json", ".yml", ".yaml", ".txt"}
|
".json", ".yml", ".yaml", ".txt"}
|
||||||
|
|
||||||
|
|
||||||
def _tracked(paths_under):
|
def _tracked(*paths_under):
|
||||||
"""Git-tracked files under a path, or None if git isn't available."""
|
"""Git-tracked files under paths, or None if git isn't available."""
|
||||||
try:
|
try:
|
||||||
out = subprocess.run(
|
out = subprocess.run(
|
||||||
["git", "ls-files", paths_under],
|
["git", "ls-files", "--", *paths_under],
|
||||||
cwd=REPO, capture_output=True, text=True, timeout=30,
|
cwd=REPO, capture_output=True, text=True, timeout=30,
|
||||||
)
|
)
|
||||||
except (OSError, subprocess.SubprocessError):
|
except (OSError, subprocess.SubprocessError):
|
||||||
@@ -34,12 +43,14 @@ def _tracked(paths_under):
|
|||||||
return [REPO / line for line in out.stdout.splitlines() if line.strip()]
|
return [REPO / line for line in out.stdout.splitlines() if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
def test_no_orphan_images_in_docs():
|
def test_no_orphan_documentation_or_branding_images():
|
||||||
docs_images = _tracked("docs")
|
managed_files = _tracked("website", "assets/branding")
|
||||||
if docs_images is None:
|
if managed_files is None:
|
||||||
pytest.skip("not a git checkout")
|
pytest.skip("not a git checkout")
|
||||||
docs_images = [p for p in docs_images if p.suffix.lower() in IMAGE_EXTS]
|
managed_images = [p for p in managed_files if p.suffix.lower() in IMAGE_EXTS]
|
||||||
assert docs_images, "expected docs/ to still contain referenced doc assets"
|
assert any("assets/branding" in p.as_posix() for p in managed_images), (
|
||||||
|
"expected assets/branding/ to contain the shared project imagery"
|
||||||
|
)
|
||||||
|
|
||||||
# All tracked text we might reference an image from.
|
# All tracked text we might reference an image from.
|
||||||
all_tracked = _tracked(".") or []
|
all_tracked = _tracked(".") or []
|
||||||
@@ -55,10 +66,117 @@ def test_no_orphan_images_in_docs():
|
|||||||
|
|
||||||
orphans = [
|
orphans = [
|
||||||
str(img.relative_to(REPO))
|
str(img.relative_to(REPO))
|
||||||
for img in docs_images
|
for img in managed_images
|
||||||
if img.name not in blob
|
if img.name not in blob
|
||||||
]
|
]
|
||||||
assert not orphans, (
|
assert not orphans, (
|
||||||
"unreferenced image(s) committed under docs/ — likely PR screenshots "
|
"unreferenced image(s) committed under website/ or assets/branding/ "
|
||||||
f"added by accident (see #1335): {orphans}"
|
f"(see #1335 and #6175): {orphans}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_pages_site_owns_its_entrypoint_and_media():
|
||||||
|
docs_files = _tracked("docs")
|
||||||
|
website_files = _tracked("website")
|
||||||
|
if docs_files is None or website_files is None:
|
||||||
|
pytest.skip("not a git checkout")
|
||||||
|
|
||||||
|
assert REPO / "website/index.html" in website_files
|
||||||
|
assert REPO / "docs/index.html" not in docs_files
|
||||||
|
assert not [p for p in docs_files if p.suffix.lower() in VIDEO_EXTS | {".md"}]
|
||||||
|
|
||||||
|
website_paths = {p.relative_to(REPO / "website").as_posix() for p in website_files}
|
||||||
|
assert PUBLIC_GUIDES <= website_paths
|
||||||
|
for guide in PUBLIC_GUIDES:
|
||||||
|
text = (REPO / "website" / guide).read_text(encoding="utf-8")
|
||||||
|
assert text.startswith("---\nlayout: default\n---\n"), guide
|
||||||
|
|
||||||
|
website_videos = [p for p in website_files if p.suffix.lower() in VIDEO_EXTS]
|
||||||
|
assert website_videos, "expected website/ to contain the landing-page videos"
|
||||||
|
|
||||||
|
entrypoint = (REPO / "website/index.html").read_text(encoding="utf-8")
|
||||||
|
unreferenced = [
|
||||||
|
str(video.relative_to(REPO))
|
||||||
|
for video in website_videos
|
||||||
|
if video.name not in entrypoint
|
||||||
|
]
|
||||||
|
assert not unreferenced, f"unreferenced website video(s): {unreferenced}"
|
||||||
|
|
||||||
|
workflow = (REPO / ".github/workflows/deploy-pages.yml").read_text(encoding="utf-8")
|
||||||
|
assert "actions/jekyll-build-pages@" in workflow
|
||||||
|
assert "source: website" in workflow
|
||||||
|
assert "destination: _site" in workflow
|
||||||
|
assert "path: _site" in workflow
|
||||||
|
assert "cancel-in-progress: false" in workflow
|
||||||
|
|
||||||
|
|
||||||
|
def test_pages_guides_keep_relative_links_inside_site():
|
||||||
|
site_root = (REPO / "website").resolve()
|
||||||
|
|
||||||
|
for guide in sorted(PUBLIC_GUIDES):
|
||||||
|
guide_path = REPO / "website" / guide
|
||||||
|
text = guide_path.read_text(encoding="utf-8")
|
||||||
|
for target in re.findall(r"\]\(([^)]+)\)", text):
|
||||||
|
parsed = urlsplit(target)
|
||||||
|
if parsed.scheme or parsed.netloc or not parsed.path:
|
||||||
|
continue
|
||||||
|
|
||||||
|
resolved = (guide_path.parent / parsed.path).resolve()
|
||||||
|
assert resolved.is_relative_to(site_root), (
|
||||||
|
f"{guide} links outside the Pages source: {target}"
|
||||||
|
)
|
||||||
|
assert resolved.exists(), f"{guide} has a missing local link: {target}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_setup_preserves_docker_go_template_literal():
|
||||||
|
setup = (REPO / "website/setup.md").read_text(encoding="utf-8")
|
||||||
|
guarded_command = """<!-- {% raw %} -->
|
||||||
|
```bash
|
||||||
|
docker info --format '{{.DockerRootDir}}'
|
||||||
|
```
|
||||||
|
<!-- {% endraw %} -->"""
|
||||||
|
|
||||||
|
assert guarded_command in setup
|
||||||
|
|
||||||
|
|
||||||
|
def test_preview_encoder_targets_pages_source():
|
||||||
|
encoder = (REPO / "scripts/encode_previews.sh").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
assert "landing page: website/<name>.webm" in encoder
|
||||||
|
assert 'OUT_DIR="$(cd "$(dirname "$0")/../website" && pwd)"' in encoder
|
||||||
|
|
||||||
|
|
||||||
|
def test_ci_runs_asset_ownership_guards_for_managed_roots():
|
||||||
|
workflow = (REPO / ".github/workflows/ci.yml").read_text(encoding="utf-8")
|
||||||
|
match = re.search(r"grep -Ev '([^']+)'", workflow)
|
||||||
|
assert match, "expected the docs-only path classifier in CI"
|
||||||
|
docs_only = re.compile(match.group(1))
|
||||||
|
|
||||||
|
assert docs_only.match("README.md")
|
||||||
|
assert docs_only.match("docs/example.md")
|
||||||
|
assert not docs_only.match("website/setup.md")
|
||||||
|
assert not docs_only.match("website/new-preview.webm")
|
||||||
|
assert not docs_only.match("assets/branding/new-logo.png")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"path",
|
||||||
|
[
|
||||||
|
"website/favicon.png",
|
||||||
|
"website/media/social-card.jpg",
|
||||||
|
"website/guides/reference.pdf",
|
||||||
|
"assets/branding/new-logo.gif",
|
||||||
|
"assets/branding/print/logo.tiff",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_managed_site_media_is_not_ignored(path):
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "check-ignore", "--no-index", "--quiet", path],
|
||||||
|
cwd=REPO,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
if result.returncode == 128:
|
||||||
|
pytest.skip("not a git checkout")
|
||||||
|
assert result.returncode == 1, f"{path} is unexpectedly ignored"
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ def test_kokoro_feature_markers_match_supported_python_range(python_version, sel
|
|||||||
|
|
||||||
|
|
||||||
def test_setup_documents_container_constraint_and_install_command():
|
def test_setup_documents_container_constraint_and_install_command():
|
||||||
setup = (ROOT / "docs" / "setup.md").read_text(encoding="utf-8")
|
setup = (ROOT / "website" / "setup.md").read_text(encoding="utf-8")
|
||||||
|
|
||||||
assert "pip install -r requirements-optional.txt" in setup
|
assert "pip install -r requirements-optional.txt" in setup
|
||||||
assert "default Docker image currently uses Python 3.14" in setup
|
assert "default Docker image currently uses Python 3.14" in setup
|
||||||
|
|||||||
@@ -123,10 +123,10 @@ def test_docker_compose_binds_web_ui_to_loopback_by_default():
|
|||||||
|
|
||||||
|
|
||||||
def test_readme_native_quickstart_uses_loopback():
|
def test_readme_native_quickstart_uses_loopback():
|
||||||
# The README refresh (#4306) moved the native quickstart into docs/setup.md,
|
# The Pages source split (#6175) moved the native quickstart into website/setup.md,
|
||||||
# so accept the loopback guidance from either the README or the setup guide.
|
# so accept the loopback guidance from either the README or the setup guide.
|
||||||
docs = Path("README.md").read_text(encoding="utf-8")
|
docs = Path("README.md").read_text(encoding="utf-8")
|
||||||
docs += "\n" + Path("docs/setup.md").read_text(encoding="utf-8")
|
docs += "\n" + Path("website/setup.md").read_text(encoding="utf-8")
|
||||||
assert "python -m uvicorn app:app --host 127.0.0.1 --port 7000" in docs
|
assert "python -m uvicorn app:app --host 127.0.0.1 --port 7000" in docs
|
||||||
assert "0.0.0.0` only when you intentionally want" in docs
|
assert "0.0.0.0` only when you intentionally want" in docs
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ def test_create_default_admin_normalizes_env_username(tmp_path, monkeypatch):
|
|||||||
def test_main_loads_admin_password_from_env_file(tmp_path, monkeypatch):
|
def test_main_loads_admin_password_from_env_file(tmp_path, monkeypatch):
|
||||||
"""Regression: setup.py must honor an admin password pre-seeded in .env on
|
"""Regression: setup.py must honor an admin password pre-seeded in .env on
|
||||||
native installs, even when the var is not exported into the shell
|
native installs, even when the var is not exported into the shell
|
||||||
(docs/setup.md documents this). Previously setup.py never called
|
(website/setup.md documents this). Previously setup.py never called
|
||||||
load_dotenv(), so os.getenv() saw nothing and a random password was
|
load_dotenv(), so os.getenv() saw nothing and a random password was
|
||||||
generated instead."""
|
generated instead."""
|
||||||
import bcrypt
|
import bcrypt
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
title: Odysseus
|
||||||
|
description: A self-hosted AI workspace for chat, agents, tools, and local models.
|
||||||
|
theme: jekyll-theme-primer
|
||||||
|
|
||||||
|
plugins:
|
||||||
|
- jekyll-relative-links
|
||||||
|
|
||||||
|
relative_links:
|
||||||
|
enabled: true
|
||||||
|
collections: true
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Agent migration manifests
|
# Agent migration manifests
|
||||||
|
|
||||||
Odysseus should be able to learn from another agent without blindly trusting
|
Odysseus should be able to learn from another agent without blindly trusting
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Attachment References and Upload Storage
|
# Attachment References and Upload Storage
|
||||||
|
|
||||||
Odysseus stores uploaded bytes once under the configured upload directory and
|
Odysseus stores uploaded bytes once under the configured upload directory and
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Backup & Restore
|
# Backup & Restore
|
||||||
|
|
||||||
Odysseus keeps all of your state in the `data/` directory — the SQLite database
|
Odysseus keeps all of your state in the `data/` directory — the SQLite database
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Outlook / Office 365 email accounts
|
# Outlook / Office 365 email accounts
|
||||||
|
|
||||||
Odysseus email accounts currently use IMAP and SMTP with username/password
|
Odysseus email accounts currently use IMAP and SMTP with username/password
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# PR Blocker Audit
|
# PR Blocker Audit
|
||||||
|
|
||||||
`scripts/pr_blocker_audit.py` is a small, read-only triage helper for maintainers who need to inspect open pull request overlap before reviewing or starting related work.
|
`scripts/pr_blocker_audit.py` is a small, read-only triage helper for maintainers who need to inspect open pull request overlap before reviewing or starting related work.
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Security CI guide
|
# Security CI guide
|
||||||
|
|
||||||
This project runs a set of automated security checks on pull requests and
|
This project runs a set of automated security checks on pull requests and
|
||||||
@@ -1,3 +1,7 @@
|
|||||||
|
---
|
||||||
|
layout: default
|
||||||
|
---
|
||||||
|
|
||||||
# Odysseus Setup Guide
|
# Odysseus Setup Guide
|
||||||
|
|
||||||
This page keeps the detailed install, deployment, troubleshooting, and configuration notes out of the front README.
|
This page keeps the detailed install, deployment, troubleshooting, and configuration notes out of the front README.
|
||||||
@@ -15,8 +19,7 @@ On first setup, Odysseus creates an admin account (`admin` unless
|
|||||||
For Docker installs, the same line is in `docker compose logs odysseus`.
|
For Docker installs, the same line is in `docker compose logs odysseus`.
|
||||||
Use that for the first login, then change it in **Settings**.
|
Use that for the first login, then change it in **Settings**.
|
||||||
|
|
||||||
Contributing? See [CONTRIBUTING.md](../CONTRIBUTING.md) for setup, testing, and
|
Contributing? See [CONTRIBUTING.md](https://github.com/odysseus-dev/odysseus/blob/dev/CONTRIBUTING.md) for setup, testing, and pull request guidelines.
|
||||||
pull request guidelines.
|
|
||||||
|
|
||||||
### Docker (recommended)
|
### Docker (recommended)
|
||||||
```bash
|
```bash
|
||||||
@@ -205,9 +208,11 @@ failed to fulfil mount request: open /usr/lib/wsl/lib/libdxcore.so: no such file
|
|||||||
|
|
||||||
Check with `snap list docker` or:
|
Check with `snap list docker` or:
|
||||||
|
|
||||||
|
<!-- {% raw %} -->
|
||||||
```bash
|
```bash
|
||||||
docker info --format '{{.DockerRootDir}}'
|
docker info --format '{{.DockerRootDir}}'
|
||||||
```
|
```
|
||||||
|
<!-- {% endraw %} -->
|
||||||
|
|
||||||
A Docker root under `/var/snap/docker/` means snap confinement can prevent
|
A Docker root under `/var/snap/docker/` means snap confinement can prevent
|
||||||
Docker from seeing WSL2's `/usr/lib/wsl/lib` GPU libraries even when the files
|
Docker from seeing WSL2's `/usr/lib/wsl/lib` GPU libraries even when the files
|
||||||
@@ -476,7 +481,7 @@ uv pip sync requirements.lock # reproduce it exactly la
|
|||||||
### Outlook / Office 365 email
|
### Outlook / Office 365 email
|
||||||
Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook
|
Odysseus email accounts currently use IMAP/SMTP username-password auth. Outlook
|
||||||
and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox
|
and Microsoft 365 generally require OAuth instead, so normal Microsoft mailbox
|
||||||
passwords will fail. See [docs/email-outlook.md](docs/email-outlook.md) for the
|
passwords will fail. See [the Outlook email guide](email-outlook.md) for the
|
||||||
current limitation and the planned integration direction.
|
current limitation and the planned integration direction.
|
||||||
|
|
||||||
## Security Notes
|
## Security Notes
|
||||||
@@ -733,7 +738,7 @@ src/ llm_core, agent_loop, agent_tools, chat_processor, search/
|
|||||||
routes/ chat, session, document, memory, model … endpoints
|
routes/ chat, session, document, memory, model … endpoints
|
||||||
services/ docs, memory, search, hwfit (Cookbook) …
|
services/ docs, memory, search, hwfit (Cookbook) …
|
||||||
static/ index.html + app.js + style.css + js/ (modular front-end)
|
static/ index.html + app.js + style.css + js/ (modular front-end)
|
||||||
docs/ landing page (index.html) + preview clips
|
website/ landing page (index.html) + preview clips
|
||||||
```
|
```
|
||||||
|
|
||||||
## Data
|
## Data
|
||||||
Reference in New Issue
Block a user