This commit is contained in:
@@ -20,6 +20,7 @@ troubleshooting the site.
|
||||
| Rollback listing | `sudo rollback-labyricorn` |
|
||||
| Rollback command | `sudo rollback-labyricorn RELEASE_NAME` |
|
||||
| Deployment log | `/srv/labyricorn/logs/deploy.log` |
|
||||
| Remote-project cache | `/srv/labyricorn/project-cache` |
|
||||
| nginx site config | `/etc/nginx/sites-available/labyricorn` |
|
||||
| Lektor executable | `/srv/labyricorn/.local/bin/lektor` |
|
||||
| Deployment account | `labyricorn-deploy` |
|
||||
@@ -27,14 +28,16 @@ troubleshooting the site.
|
||||
## Architecture
|
||||
|
||||
```text
|
||||
Gitea: Labyricorn/labyricorn-site (main)
|
||||
|
|
||||
| authenticated Git fetch
|
||||
v
|
||||
/srv/labyricorn/repo
|
||||
|
|
||||
| Lektor static build
|
||||
v
|
||||
Gitea: Labyricorn/labyricorn-site (main) public project repositories
|
||||
| |
|
||||
| authenticated Git fetch | read-only Git/API sync
|
||||
v v
|
||||
/srv/labyricorn/repo /srv/labyricorn/project-cache
|
||||
| |
|
||||
+---------- validated build input -------+
|
||||
|
|
||||
| isolated Lektor build
|
||||
v
|
||||
/srv/labyricorn/releases/<UTC timestamp>-<commit>
|
||||
|
|
||||
| validated, atomic symlink switch
|
||||
@@ -53,6 +56,9 @@ Gitea: Labyricorn/labyricorn-site (main)
|
||||
Gitea is the source of truth. The production checkout is disposable state and
|
||||
must not contain unpublished edits during deployment. Lektor's development
|
||||
server is not part of production serving; nginx serves generated files only.
|
||||
Remote project repositories remain authoritative for their own exhibition and
|
||||
devlog records. They are never checked out into the site repository and cannot
|
||||
modify trusted site models, templates, scripts, or content.
|
||||
|
||||
## Repository contents
|
||||
|
||||
@@ -63,6 +69,12 @@ content/tags/ Controlled tag vocabulary and dedicated tag routes
|
||||
models/ Lektor content models
|
||||
templates/ Jinja templates
|
||||
assets/static/ CSS, filtering JavaScript, favicon, and static assets
|
||||
configs/project-sources.ini Approved public remote-project registry
|
||||
scripts/project_sources.py Remote-content validator and importer
|
||||
scripts/build_with_projects.py Isolated build/preview entry point
|
||||
ops/deploy-labyricorn Versioned copy of the production deployment command
|
||||
tests/ Importer validation tests
|
||||
.gitea/workflows/ Push, schedule, and manual deployment automation
|
||||
.gitignore Excludes generated and local files
|
||||
README.md This runbook
|
||||
```
|
||||
@@ -82,7 +94,7 @@ git pull --ff-only origin main
|
||||
|
||||
# Edit content, templates, models, or assets.
|
||||
|
||||
lektor build --output-path build
|
||||
python scripts/build_with_projects.py --output-path build
|
||||
git status
|
||||
git add <reviewed-files>
|
||||
git commit -m "Describe the site change"
|
||||
@@ -140,7 +152,7 @@ keeps the selected filter in the `tag` query parameter.
|
||||
|
||||
After taxonomy or filtering changes:
|
||||
|
||||
1. Run `lektor build --output-path build`.
|
||||
1. Run `python scripts/build_with_projects.py --output-path build`.
|
||||
2. Verify `/tags/`, at least one dedicated tag URL, and both section listings.
|
||||
3. Confirm tag counts cover blog entries and articles and that every entry slug
|
||||
has a matching tag record.
|
||||
@@ -150,6 +162,87 @@ After taxonomy or filtering changes:
|
||||
6. If filtering JavaScript or tag styles changed, increment the corresponding
|
||||
cache-busting version in `templates/base.html`.
|
||||
|
||||
## Repository-owned project pages and devlogs
|
||||
|
||||
The site can publish a project page whose source lives in the project's own
|
||||
public Git repository. Thinkloom is the first configured source:
|
||||
|
||||
```text
|
||||
https://git.labyricorn.com/Labyricorn/thinkloom-openai-hackathon
|
||||
.labyricorn/
|
||||
├── AGENTS.md
|
||||
├── project/
|
||||
│ ├── AGENTS.md
|
||||
│ ├── contents.lr
|
||||
│ └── icon.png
|
||||
└── devlog/
|
||||
├── AGENTS.md
|
||||
├── contents.lr
|
||||
└── <entry-slug>/contents.lr
|
||||
```
|
||||
|
||||
The public routes are `/projects/thinkloom/`,
|
||||
`/projects/thinkloom/devlog/`, and
|
||||
`/projects/thinkloom/devlog/<entry-slug>/`. The project repository owns the
|
||||
native Lektor records and approved images. This site owns their models,
|
||||
templates, layout, repository-information card, and import policy.
|
||||
|
||||
`configs/project-sources.ini` is the allowlist. Each source declares a stable
|
||||
project ID, credential-free HTTPS Git URL, public web/API URL, and branch. Add
|
||||
or remove a source only through a reviewed site change. The importer also
|
||||
checks the provider API and refuses a repository reported as private.
|
||||
Remote Git commands run with an isolated empty home/config and interactive
|
||||
credential lookup disabled, so the production account's site-repository
|
||||
credential cannot silently turn a private project fetch into an authenticated
|
||||
one. An anonymous API response of 401, 403, or 404 is treated as a visibility
|
||||
failure rather than a metadata-cache condition.
|
||||
|
||||
The importer reads only these paths:
|
||||
|
||||
- `.labyricorn/project/contents.lr`
|
||||
- `.labyricorn/project/*.{png,jpg,jpeg,webp}`
|
||||
- `.labyricorn/devlog/contents.lr`
|
||||
- `.labyricorn/devlog/<entry-slug>/contents.lr`
|
||||
- approved image types within a devlog entry directory
|
||||
|
||||
Everything else is ignored, including remote `README.md` and `AGENTS.md`
|
||||
instructions. Symbolic links, submodules, executable files, raw HTML,
|
||||
unsupported models or schema versions, invalid slugs, unknown paths,
|
||||
out-of-tree references, and invalid image signatures are rejected. Limits are
|
||||
100 imported files, 5 MiB per file, and 20 MiB total per project snapshot.
|
||||
Devlog `source_commit` values must exist in the repository and be ancestors of
|
||||
the imported branch revision.
|
||||
|
||||
Every build uses a temporary copy of trusted site source, materializes validated
|
||||
remote records into that copy, and invokes Lektor there. Imported files never
|
||||
enter the site working tree. Git mirrors, state, and the last validated snapshot
|
||||
live under the project cache. If a new snapshot or network fetch fails, the
|
||||
build uses the last-known-good validated revision. Initial publication fails
|
||||
when no valid snapshot exists. The generated
|
||||
`.labyricorn-projects.json` records the site revision, imported project
|
||||
revisions, metadata digests, synchronization status, and synchronization time.
|
||||
|
||||
The project card combines repository-owned content with public provider and Git
|
||||
metadata: source and README links, default branch, latest commit, license,
|
||||
languages, open issues, stars, forks, latest release, imported revision, and
|
||||
sync time. API metadata failure uses cached metadata and is identified in the
|
||||
sync status; it never causes an unvalidated new snapshot to replace a working
|
||||
one.
|
||||
|
||||
To preview or build with remote project content, use the wrapper instead of
|
||||
calling Lektor directly:
|
||||
|
||||
```bash
|
||||
python scripts/build_with_projects.py --serve
|
||||
python scripts/build_with_projects.py --output-path build
|
||||
python -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
The default development cache is `.cache/project-sources`, which is ignored by
|
||||
Git. Lektor must be installed because the importer deliberately uses Lektor's
|
||||
native record parser and the wrapper invokes the Lektor build/server. No custom
|
||||
Lektor plugin or third-party project-import package is used.
|
||||
|
||||
## Local development
|
||||
|
||||
Install Lektor in an isolated Python environment. For example:
|
||||
@@ -161,16 +254,16 @@ python -m pip install --upgrade pip
|
||||
python -m pip install lektor
|
||||
```
|
||||
|
||||
Run the local editor and preview server:
|
||||
Run the local editor and preview server with validated project content:
|
||||
|
||||
```bash
|
||||
lektor server
|
||||
python scripts/build_with_projects.py --serve
|
||||
```
|
||||
|
||||
Run a production-style build:
|
||||
|
||||
```bash
|
||||
lektor build --output-path build
|
||||
python scripts/build_with_projects.py --output-path build
|
||||
test -f build/index.html
|
||||
```
|
||||
|
||||
@@ -186,6 +279,7 @@ The project currently has no third-party Lektor packages or plugins.
|
||||
├── logs/
|
||||
│ ├── deploy.log
|
||||
│ └── deploy.lock
|
||||
├── project-cache/ Bare mirrors, metadata, and last-known-good state
|
||||
├── .local/bin/lektor pipx-installed Lektor
|
||||
├── .netrc Git HTTPS credential (0600; secret)
|
||||
├── .ssh/ Reserved deployment SSH material
|
||||
@@ -208,15 +302,19 @@ or credential. nginx cannot modify either source or releases.
|
||||
4. Fetches and prunes `origin`.
|
||||
5. Verifies that `origin/main` exists.
|
||||
6. Switches to local `main` and resets it exactly to `origin/main`.
|
||||
7. Locates exactly one `.lektorproject` file instead of assuming its name.
|
||||
8. Creates a unique release named with UTC time and the 12-character commit ID.
|
||||
9. Runs Lektor with an explicit output directory.
|
||||
10. Requires the build to succeed and contain `index.html`.
|
||||
11. Writes the full commit ID to `.labyricorn-commit` in the release.
|
||||
12. Atomically replaces `/srv/labyricorn/current` with a relative symlink to
|
||||
7. Creates a unique release named with UTC time and the 12-character commit ID.
|
||||
8. Runs `scripts/build_with_projects.py` with the persistent project cache and
|
||||
an explicit output directory.
|
||||
9. Fetches, validates, and imports each approved public project source into an
|
||||
isolated temporary workspace before invoking Lektor.
|
||||
10. Requires `index.html` and `.labyricorn-projects.json`.
|
||||
11. Writes the full site commit ID to `.labyricorn-commit`.
|
||||
12. Compares the site commit plus project commit/metadata digests with the
|
||||
active release and removes the candidate as an unchanged no-op when equal.
|
||||
13. Atomically replaces `/srv/labyricorn/current` with a relative symlink to
|
||||
the new release.
|
||||
13. Keeps the active release plus recent prior releases, targeting five total.
|
||||
14. Logs start, failure, success, release name, and commit.
|
||||
14. Keeps the active release plus recent prior releases, targeting five total.
|
||||
15. Logs start, failure, unchanged, success, release name, and commit.
|
||||
|
||||
Run it with:
|
||||
|
||||
@@ -440,8 +538,10 @@ an unauthenticated editor through nginx.
|
||||
|
||||
## Git authentication and secrets
|
||||
|
||||
The private Gitea repository is accessed over HTTPS because the Gitea SSH port
|
||||
was not reachable through `git.labyricorn.com` during setup.
|
||||
The site repository is accessed over authenticated HTTPS because the Gitea SSH
|
||||
port was not reachable through `git.labyricorn.com` during setup. Approved
|
||||
remote project repositories, including Thinkloom, are public and are fetched
|
||||
without embedding credentials in their configured URLs.
|
||||
|
||||
Credential purpose and location:
|
||||
|
||||
@@ -529,9 +629,12 @@ Run a disposable diagnostic build; do not build into `current`:
|
||||
test_dir=$(mktemp -d /srv/labyricorn/releases/.diagnostic.XXXXXX)
|
||||
chown labyricorn-deploy:labyricorn-deploy "$test_dir"
|
||||
sudo -u labyricorn-deploy env HOME=/srv/labyricorn \
|
||||
/srv/labyricorn/.local/bin/lektor \
|
||||
--project /srv/labyricorn/repo/Labyricorn.lektorproject \
|
||||
build --output-path "$test_dir"
|
||||
/srv/labyricorn/.local/pipx/venvs/lektor/bin/python \
|
||||
/srv/labyricorn/repo/scripts/build_with_projects.py \
|
||||
--site-root /srv/labyricorn/repo \
|
||||
--cache-path /srv/labyricorn/project-cache \
|
||||
--output-path "$test_dir" \
|
||||
--lektor /srv/labyricorn/.local/bin/lektor
|
||||
```
|
||||
|
||||
Inspect the error and source. Remove the diagnostic directory only after
|
||||
@@ -565,14 +668,36 @@ The first origin request should redirect; the simulated Cloudflare HTTPS request
|
||||
should return the site. A different result indicates a proxy/header or nginx
|
||||
configuration problem.
|
||||
|
||||
## Automatic deployment
|
||||
### Remote project synchronization fails
|
||||
|
||||
Pushes to `main` automatically deploy through Gitea Actions. The workflow is
|
||||
versioned with the site at `.gitea/workflows/deploy.yml` and has one job:
|
||||
Inspect the deployment log and active manifest without editing the cache:
|
||||
|
||||
```bash
|
||||
tail -n 100 /srv/labyricorn/logs/deploy.log
|
||||
cat /srv/labyricorn/current/.labyricorn-projects.json
|
||||
```
|
||||
|
||||
`current` means the latest remote revision and provider metadata validated.
|
||||
`metadata-cached` means Git content is current but provider metadata fell back
|
||||
to its cache. `last-known-good` means the latest fetch or candidate snapshot
|
||||
could not safely replace the previously validated commit. A first import with
|
||||
no valid cached snapshot fails the deployment. Do not bypass validation by
|
||||
copying remote files into `content/`, the build workspace, or an active release.
|
||||
|
||||
## Automatic deployment and project refresh
|
||||
|
||||
The workflow at `.gitea/workflows/deploy.yml` runs for three event types:
|
||||
|
||||
- every push to site `main`;
|
||||
- every 30 minutes (`*/30 * * * *`, evaluated by Gitea in UTC);
|
||||
- an authenticated manual `workflow_dispatch`.
|
||||
|
||||
All events run the same one-job deployment:
|
||||
|
||||
1. Run on the repository-scoped runner labelled `labyricorn-deploy`.
|
||||
2. Execute the existing root-owned `deploy-labyricorn` command.
|
||||
3. Confirm `/srv/labyricorn/current/.labyricorn-commit` equals the pushed commit.
|
||||
3. Confirm `/srv/labyricorn/current/.labyricorn-commit` equals the workflow's
|
||||
site commit.
|
||||
4. Request the site through nginx on the local origin and fail if it is unhealthy.
|
||||
|
||||
The action intentionally does not check out the repository into its own
|
||||
@@ -580,7 +705,38 @@ workspace. The deployment script fetches `origin/main` using the dedicated
|
||||
`labyricorn-deploy` account, validates a clean checkout, builds a new release,
|
||||
and atomically changes the `current` symlink. Concurrent action runs share the
|
||||
`labyricorn-production` concurrency group, and the deployment script also uses
|
||||
a filesystem lock.
|
||||
a filesystem lock. A scheduled/manual run updates the site when an approved
|
||||
project commit or public metadata digest changes. When neither the site nor any
|
||||
project input changed, it validates and builds a candidate, removes it, logs an
|
||||
`unchanged` result, and leaves the active symlink untouched.
|
||||
|
||||
### Trigger a refresh
|
||||
|
||||
From Gitea, open **Labyricorn/labyricorn-site → Actions → Deploy production**
|
||||
and choose **Run workflow** for `main`.
|
||||
|
||||
Automation and coding assistants may dispatch the same workflow through
|
||||
Gitea's authenticated API after pushing project publishing content:
|
||||
|
||||
```bash
|
||||
export LABYRICORN_REFRESH_TOKEN='set this outside Git and shell history'
|
||||
curl --fail --silent --show-error \
|
||||
-X POST \
|
||||
-H "Authorization: token $LABYRICORN_REFRESH_TOKEN" \
|
||||
-H 'Content-Type: application/json' \
|
||||
--data '{"ref":"main"}' \
|
||||
https://git.labyricorn.com/api/v1/repos/Labyricorn/labyricorn-site/actions/workflows/deploy.yml/dispatches
|
||||
unset LABYRICORN_REFRESH_TOKEN
|
||||
```
|
||||
|
||||
The token must be supplied through an approved credential store or ephemeral
|
||||
environment and must have permission to dispatch Actions for the site
|
||||
repository. Never put it in the URL, a project repository, `AGENTS.md`, logs,
|
||||
or committed scripts. Send at most one dispatch after the relevant project
|
||||
push, then verify the Action and public project revision. The scheduled run is
|
||||
the fallback when no refresh credential is available. This is deliberately not
|
||||
an unauthenticated public webhook or long-running URL listener, so it cannot be
|
||||
spammed by anonymous requests.
|
||||
|
||||
### Runner installation
|
||||
|
||||
|
||||
Reference in New Issue
Block a user