Compare commits
13 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2072db4980 | |||
| 00527e46b1 | |||
| 34a278870d | |||
| 736f40f953 | |||
| 73a8b5834a | |||
| b2714ab5af | |||
| bf96cd10e7 | |||
| bb57f319c5 | |||
| e838d3b105 | |||
| 217d633853 | |||
| 3d7af25aed | |||
| 47e5127299 | |||
| 2103a423fe |
@@ -0,0 +1,99 @@
|
|||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
paths:
|
||||||
|
- Dockerfile
|
||||||
|
- "pipelines/**/requirements.txt"
|
||||||
|
- .gitea/workflows/build-image.yml
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
# Daemonless image builder: kaniko :debug plus the /bin/sleep symlink
|
||||||
|
# act needs for job-container PID 1 (see ci/Dockerfile.kaniko-act).
|
||||||
|
# No docker socket, no privileges.
|
||||||
|
image: pi5-16.local:3005/mikkeli/kaniko-act:v1.23.2-r2
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Fetch source, build, push
|
||||||
|
# kaniko image has no bash — busybox sh only
|
||||||
|
shell: sh
|
||||||
|
env:
|
||||||
|
# package r/w PAT (user-level secret) — registry push only
|
||||||
|
PKG_TOKEN: ${{ secrets.PKGRW_PAT }}
|
||||||
|
# per-run auto-token, read access to this repo only — source fetch
|
||||||
|
JOB_TOKEN: ${{ github.token }}
|
||||||
|
URL_TO_GITEA: ${{ vars.URL_TO_GITEA }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
HOST="${URL_TO_GITEA#http://}"
|
||||||
|
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||||
|
IMAGE="${HOST}/mikkeli/novoyuuparosk-wiki-runner"
|
||||||
|
|
||||||
|
# Source via Gitea archive API (no git/node in this image; repo is
|
||||||
|
# private — authenticate with the job's own per-run token)
|
||||||
|
wget -qO /tmp/src.tar.gz \
|
||||||
|
"http://mikkeli:${JOB_TOKEN}@${HOST}/api/v1/repos/mikkeli/novoyuuparosk-auto-wiki/archive/${GITHUB_SHA}.tar.gz"
|
||||||
|
mkdir -p /tmp/src
|
||||||
|
tar -xzf /tmp/src.tar.gz -C /tmp/src --strip-components=1
|
||||||
|
|
||||||
|
# Registry auth for the push
|
||||||
|
AUTH=$(printf '%s' "mikkeli:${PKG_TOKEN}" | base64 | tr -d '\n')
|
||||||
|
printf '{"auths":{"%s":{"auth":"%s"}}}' "${HOST}" "${AUTH}" \
|
||||||
|
> /kaniko/.docker/config.json
|
||||||
|
|
||||||
|
# --insecure: push target is the plain-HTTP Gitea registry.
|
||||||
|
# Base image pull (docker.io) stays HTTPS — no --insecure-pull.
|
||||||
|
# :latest is a convenience tag for internal CI (the pin job below
|
||||||
|
# runs in it). Publish workflows stay pinned to the immutable sha.
|
||||||
|
/kaniko/executor \
|
||||||
|
--context dir:///tmp/src \
|
||||||
|
--dockerfile /tmp/src/Dockerfile \
|
||||||
|
--destination "${IMAGE}:${SHORT_SHA}" \
|
||||||
|
--destination "${IMAGE}:latest" \
|
||||||
|
--insecure
|
||||||
|
|
||||||
|
echo "Pushed ${IMAGE}:${SHORT_SHA} and ${IMAGE}:latest"
|
||||||
|
|
||||||
|
pin:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container:
|
||||||
|
# Freshly built runner image: git + GNU sed baked in, and it has no
|
||||||
|
# non-shell ENTRYPOINT, so it works as a job container as-is (unlike the
|
||||||
|
# kaniko image above). Pulls :latest that the build job just pushed.
|
||||||
|
image: pi5-16.local:3005/mikkeli/novoyuuparosk-wiki-runner:latest
|
||||||
|
steps:
|
||||||
|
- name: Repoint publish workflows at the new image tag
|
||||||
|
env:
|
||||||
|
# Full-access PAT — needs contents:write to push the pin commit.
|
||||||
|
# github.token is read-only here; PKGRW_PAT is registry-only.
|
||||||
|
FAPAT: ${{ secrets.FAPAT }}
|
||||||
|
URL_TO_GITEA: ${{ vars.URL_TO_GITEA }}
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
HOST="${URL_TO_GITEA#http://}"
|
||||||
|
SHORT_SHA=$(echo "${GITHUB_SHA}" | cut -c1-7)
|
||||||
|
|
||||||
|
git clone "http://mikkeli:${FAPAT}@${HOST}/mikkeli/novoyuuparosk-auto-wiki" repo
|
||||||
|
cd repo
|
||||||
|
|
||||||
|
for f in .gitea/workflows/publish-songs.yml \
|
||||||
|
.gitea/workflows/publish-ses.yml \
|
||||||
|
.gitea/workflows/publish-tech.yml; do
|
||||||
|
sed -i -E "s#(novoyuuparosk-wiki-runner:)[A-Za-z0-9._-]+#\1${SHORT_SHA}#" "$f"
|
||||||
|
done
|
||||||
|
|
||||||
|
if git diff --quiet; then
|
||||||
|
echo "Pins already at ${SHORT_SHA}; nothing to commit."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
git config user.email "actions@novoyuuparosk.org"
|
||||||
|
git config user.name "wiki-runner image bot"
|
||||||
|
git commit -am "build: pin runner image to ${SHORT_SHA}"
|
||||||
|
# Pin commit only touches publish-*.yml (on: workflow_call) — triggers
|
||||||
|
# nothing, so no rebuild loop. Fails loudly if master moved meanwhile.
|
||||||
|
git push origin HEAD:master
|
||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: novoyuuparosk-wiki-runner:latest
|
image: pi5-16.local:3005/mikkeli/novoyuuparosk-wiki-runner:b2714ab
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ses-light-novel
|
- name: Checkout ses-light-novel
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: novoyuuparosk-wiki-runner:latest
|
image: pi5-16.local:3005/mikkeli/novoyuuparosk-wiki-runner:b2714ab
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout ncmr-songs
|
- name: Checkout ncmr-songs
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ jobs:
|
|||||||
publish:
|
publish:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
container:
|
container:
|
||||||
image: novoyuuparosk-wiki-runner:latest
|
image: pi5-16.local:3005/mikkeli/novoyuuparosk-wiki-runner:b2714ab
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout tech-blogs
|
- name: Checkout tech-blogs
|
||||||
|
|||||||
+4
-1
@@ -1,2 +1,5 @@
|
|||||||
# claude local settings
|
# claude local settings
|
||||||
.claude/
|
.claude/
|
||||||
|
|
||||||
|
# python
|
||||||
|
__pycache__/
|
||||||
@@ -8,7 +8,7 @@ CI/CD pipelines that auto-apply commits to https://wiki.novoyuuparosk.org from u
|
|||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| [`pipelines/songs/`](pipelines/songs/) | `mikkeli/ncmr-songs` | Song lyric pages | v1 live |
|
| [`pipelines/songs/`](pipelines/songs/) | `mikkeli/ncmr-songs` | Song lyric pages | v1 live |
|
||||||
| [`pipelines/ses/`](pipelines/ses/) | `mikkeli/ses-light-novel` | SES light novel pages | v1 in development |
|
| [`pipelines/ses/`](pipelines/ses/) | `mikkeli/ses-light-novel` | SES light novel pages | v1 in development |
|
||||||
| [`pipelines/tech/`](pipelines/tech/) | `mikkeli/tech-blogs` | Tech blog posts | v1 ready (source repo not yet initialised) |
|
| [`pipelines/tech/`](pipelines/tech/) | `mikkeli/tech-blogs` | Tech blog posts | v1 live |
|
||||||
|
|
||||||
Per-pipeline READMEs cover everything specific to that pipeline (source schema, renderer, runtime, decisions). This root README covers only what's cross-cutting.
|
Per-pipeline READMEs cover everything specific to that pipeline (source schema, renderer, runtime, decisions). This root README covers only what's cross-cutting.
|
||||||
|
|
||||||
@@ -49,14 +49,11 @@ Container network mode: `host` — required so job containers can reach `localho
|
|||||||
|
|
||||||
### Job container image
|
### Job container image
|
||||||
|
|
||||||
All pipelines share a single pre-built Docker image: `novoyuuparosk-wiki-runner:latest`. The `Dockerfile` is at the repo root. It bakes in system deps (git, pandoc, ca-certificates) and all pipeline Python packages so job containers start instantly with no install steps.
|
All pipelines share a single pre-built Docker image, served from the Gitea registry at `pi5-16.local:3005/mikkeli/novoyuuparosk-wiki-runner`. The `Dockerfile` is at the repo root. It bakes in system deps (git, pandoc, ca-certificates) and all pipeline Python packages so job containers start instantly with no install steps.
|
||||||
|
|
||||||
The image is built manually on the Pi and stored in the local Docker daemon (`pull_image: false` in act_runner config). Rebuild after any change to the `Dockerfile` or a pipeline `requirements.txt`:
|
The image builds automatically via [`.gitea/workflows/build-image.yml`](.gitea/workflows/build-image.yml), which triggers on pushes that touch the `Dockerfile`, any pipeline `requirements.txt`, or that workflow itself. It uses kaniko (daemonless, unprivileged) to build and push two tags: an immutable `:<short-sha>` and a moving `:latest`.
|
||||||
|
|
||||||
```bash
|
A follow-up `pin` job then rewrites the `image:` pin in each `publish-*.yml` to the new `:<short-sha>` and commits it back to `master` (using `FAPAT` for contents write). The publish workflows therefore always reference an immutable tag, kept current automatically — no manual bump. The pin commit only touches `workflow_call` files, so it triggers no further runs.
|
||||||
docker build -t novoyuuparosk-wiki-runner:latest \
|
|
||||||
/home/mikkeli/dev/novoyuuparosk-auto-wiki
|
|
||||||
```
|
|
||||||
|
|
||||||
## Gitea Actions setup (cross-cutting)
|
## Gitea Actions setup (cross-cutting)
|
||||||
|
|
||||||
@@ -89,6 +86,7 @@ Secrets and variables are scoped to user `mikkeli` (no orgs on this instance), i
|
|||||||
| MediaWiki API path | `api.php` (classic action API) | 2026-06-09 |
|
| MediaWiki API path | `api.php` (classic action API) | 2026-06-09 |
|
||||||
| Branch naming (this repo) | `automation/<pipeline>` for pipeline-development branches | 2026-06-09 |
|
| Branch naming (this repo) | `automation/<pipeline>` for pipeline-development branches | 2026-06-09 |
|
||||||
| Variable naming | `URL_TO_GITEA` not `GITEA_URL` — Gitea blocks `GITEA_`/`GITHUB_` prefixes | 2026-06-09 |
|
| Variable naming | `URL_TO_GITEA` not `GITEA_URL` — Gitea blocks `GITEA_`/`GITHUB_` prefixes | 2026-06-09 |
|
||||||
|
| Columns shorthand | Side-by-side columns authored as a ` ```columns ` fenced block, expanded post-Pandoc in shared `lib/wiki.py` (universal across pipelines). No wiki template or PHP extension — runs Pi-side before the API call | 2026-06-14 |
|
||||||
|
|
||||||
Per-pipeline decisions live in each pipeline's README.
|
Per-pipeline decisions live in each pipeline's README.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
# Kaniko executor adapted for Gitea act_runner job containers.
|
||||||
|
#
|
||||||
|
# act starts job containers with entrypoint /bin/sleep and sets
|
||||||
|
# RUNNER_TEMP=/tmp; kaniko's scratch-based image has neither /bin/sleep
|
||||||
|
# (busybox lives under /busybox; /bin/sh is already symlinked) nor /tmp.
|
||||||
|
# This wrapper adds exactly those two. Nothing else changes.
|
||||||
|
#
|
||||||
|
# Bootstrap: the first build of this image is done manually on the host
|
||||||
|
# (docker build -f ci/Dockerfile.kaniko-act -t <registry>/mikkeli/kaniko-act:<ver> .)
|
||||||
|
# because no builder image exists yet. Later version bumps can be built by
|
||||||
|
# the build-image workflow itself, using the previous kaniko-act.
|
||||||
|
FROM gcr.io/kaniko-project/executor:v1.23.2-debug
|
||||||
|
SHELL ["/busybox/sh", "-c"]
|
||||||
|
RUN ln -sf /busybox/sleep /bin/sleep && mkdir -p -m 1777 /tmp
|
||||||
+45
-1
@@ -1,6 +1,8 @@
|
|||||||
"""Shared utilities for novoyuuparosk-auto-wiki pipelines."""
|
"""Shared utilities for novoyuuparosk-auto-wiki pipelines."""
|
||||||
|
|
||||||
|
import html
|
||||||
import os
|
import os
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -8,6 +10,25 @@ import mwclient
|
|||||||
|
|
||||||
AUTO_BANNER_PREFIX = "{{Auto-generated"
|
AUTO_BANNER_PREFIX = "{{Auto-generated"
|
||||||
|
|
||||||
|
# A ```columns fenced block is passed through Pandoc verbatim as
|
||||||
|
# <pre class="columns">…</pre>; columns within it are separated by a line of ===.
|
||||||
|
_COLUMNS_BLOCK_RE = re.compile(r'<pre class="columns">(.*?)</pre>', re.DOTALL)
|
||||||
|
_COLUMN_SEP_RE = re.compile(r"^\s*===\s*$", re.MULTILINE)
|
||||||
|
|
||||||
|
# Minimal Markdown emphasis → wikitext, for use inside a verbatim columns block
|
||||||
|
# (Pandoc doesn't process Markdown there). Asterisk style only, single line.
|
||||||
|
# Order matters: bold-italic (***) before bold (**) before italic (*).
|
||||||
|
_BOLD_ITALIC_RE = re.compile(r"\*\*\*(.+?)\*\*\*")
|
||||||
|
_BOLD_RE = re.compile(r"\*\*(.+?)\*\*")
|
||||||
|
_ITALIC_RE = re.compile(r"\*(.+?)\*")
|
||||||
|
|
||||||
|
|
||||||
|
def _md_emphasis_to_wikitext(text: str) -> str:
|
||||||
|
text = _BOLD_ITALIC_RE.sub(r"'''''\1'''''", text)
|
||||||
|
text = _BOLD_RE.sub(r"'''\1'''", text)
|
||||||
|
text = _ITALIC_RE.sub(r"''\1''", text)
|
||||||
|
return text
|
||||||
|
|
||||||
|
|
||||||
def strip_first_h1(text: str) -> str:
|
def strip_first_h1(text: str) -> str:
|
||||||
"""Remove the first '# Heading' line and any immediately following blank line."""
|
"""Remove the first '# Heading' line and any immediately following blank line."""
|
||||||
@@ -21,6 +42,29 @@ def strip_first_h1(text: str) -> str:
|
|||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|
||||||
|
|
||||||
|
def expand_columns(wikitext: str) -> str:
|
||||||
|
"""Expand ```columns fenced blocks into a flex row of <poem> columns.
|
||||||
|
|
||||||
|
Authors write a fenced code block tagged ``columns``; Pandoc passes its body
|
||||||
|
through verbatim as ``<pre class="columns">…</pre>`` (line breaks and blank
|
||||||
|
lines preserved, inline markup entity-escaped). Columns within the block are
|
||||||
|
separated by a line containing only ``===``. Each column is wrapped in
|
||||||
|
<poem> so its line breaks survive MediaWiki parsing. Content is
|
||||||
|
HTML-unescaped (so inline markup such as <b>…</b> renders rather than
|
||||||
|
appearing as literal text) and Markdown emphasis (``*italic*``, ``**bold**``,
|
||||||
|
``***bold-italic***``) is converted to wikitext, since Pandoc does not
|
||||||
|
process Markdown inside the verbatim block.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def render(match: re.Match) -> str:
|
||||||
|
body = _md_emphasis_to_wikitext(html.unescape(match.group(1)))
|
||||||
|
columns = _COLUMN_SEP_RE.split(body)
|
||||||
|
poems = "".join("<poem>\n" + col.strip("\n") + "\n</poem>" for col in columns)
|
||||||
|
return '<div style="display:flex; gap:3em; align-items:flex-start">' + poems + "</div>"
|
||||||
|
|
||||||
|
return _COLUMNS_BLOCK_RE.sub(render, wikitext)
|
||||||
|
|
||||||
|
|
||||||
def markdown_to_wikitext(body: str) -> str:
|
def markdown_to_wikitext(body: str) -> str:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["pandoc", "-f", "markdown", "-t", "mediawiki"],
|
["pandoc", "-f", "markdown", "-t", "mediawiki"],
|
||||||
@@ -30,7 +74,7 @@ def markdown_to_wikitext(body: str) -> str:
|
|||||||
)
|
)
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
raise RuntimeError(f"pandoc failed: {result.stderr.strip()}")
|
raise RuntimeError(f"pandoc failed: {result.stderr.strip()}")
|
||||||
return result.stdout
|
return expand_columns(result.stdout)
|
||||||
|
|
||||||
|
|
||||||
def connect_wiki() -> mwclient.Site:
|
def connect_wiki() -> mwclient.Site:
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ The pipeline always runs `--all`: every file with `wiki.publish: true` (not unde
|
|||||||
| Publish mode | Always `--all`; no diff detection — small repo, simpler than fragile git-diff gating | 2026-06-09 |
|
| Publish mode | Always `--all`; no diff detection — small repo, simpler than fragile git-diff gating | 2026-06-09 |
|
||||||
| Manual-page protection | Bot skips pages without the `{{Auto-generated` banner to avoid overwriting hand-written content | 2026-06-09 |
|
| Manual-page protection | Bot skips pages without the `{{Auto-generated` banner to avoid overwriting hand-written content | 2026-06-09 |
|
||||||
| `workflow_call` abandoned | Cross-repo `workflow_call` fails — run token scoped to triggering repo; cannot clone private callee | 2026-06-09 |
|
| `workflow_call` abandoned | Cross-repo `workflow_call` fails — run token scoped to triggering repo; cannot clone private callee | 2026-06-09 |
|
||||||
|
| Side-by-side columns | Authored as a ` ```columns ` fenced block (separator `===`); Pandoc emits `<pre class="columns">`, expanded post-Pandoc into a flex `<div>` of `<poem>` columns. No wiki template or extension — all Pi-side before the API write. See [SCHEMA.md](SCHEMA.md#columns-side-by-side-shorthand) | 2026-06-14 |
|
||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
|
|||||||
@@ -122,20 +122,46 @@ The siblings list is a bill of materials — declaring a sibling has no effect u
|
|||||||
|
|
||||||
## Body
|
## Body
|
||||||
|
|
||||||
Plain markdown. The renderer applies two transformations before and after Pandoc:
|
Plain markdown. The renderer applies these transformations before and after Pandoc:
|
||||||
|
|
||||||
1. **Pre-Pandoc**: strip the first-line `# Heading` if present.
|
1. **Pre-Pandoc**: strip the first-line `# Heading` if present.
|
||||||
2. **Post-Pandoc**: substitute `TAG` placeholders declared in `wiki.siblings`.
|
2. **Post-Pandoc**: expand `columns` fenced blocks into side-by-side wikitext (see below).
|
||||||
|
3. **Post-Pandoc**: substitute `TAG` placeholders declared in `wiki.siblings`.
|
||||||
|
|
||||||
Do not embed raw wikitext-specific syntax (`{{Template}}`, raw `[[Wikilink]]` not declared via `siblings`, etc.) in the body unless you intend the literal output. Use markdown idioms; the renderer adds the metadata-derived bits (banner, LRC link, categories, sibling resolution) around Pandoc's output.
|
Do not embed raw wikitext-specific syntax (`{{Template}}`, raw `[[Wikilink]]` not declared via `siblings`, etc.) in the body unless you intend the literal output. Use markdown idioms; the renderer adds the metadata-derived bits (banner, LRC link, categories, sibling resolution) around Pandoc's output.
|
||||||
|
|
||||||
|
### Columns (side-by-side) shorthand
|
||||||
|
|
||||||
|
For parallel content — e.g. an original and its translation — write a fenced code block tagged `columns` and separate the columns with a line containing only `===`:
|
||||||
|
|
||||||
|
````markdown
|
||||||
|
```columns
|
||||||
|
**原題**
|
||||||
|
|
||||||
|
一行目
|
||||||
|
二行目
|
||||||
|
===
|
||||||
|
**Title**
|
||||||
|
|
||||||
|
first line
|
||||||
|
second line
|
||||||
|
```
|
||||||
|
````
|
||||||
|
|
||||||
|
The renderer turns this into a flexbox row of `<poem>` columns (one per `===`-delimited section). Notes:
|
||||||
|
|
||||||
|
- **Line breaks and blank lines are preserved verbatim** — that's the point of using a fenced block; Pandoc passes the body through untouched, and each column is wrapped in `<poem>` so MediaWiki keeps the line breaks.
|
||||||
|
- **N columns**: use N−1 `===` separators. Two is the common case (original / translation).
|
||||||
|
- **Emphasis in Markdown**: write `*italic*`, `**bold**`, or `***bold-italic***` (asterisk style, single line) — these are converted to wikitext. Raw inline HTML such as `<b>…</b>` or `<br>` also still works (it's HTML-unescaped on the way out), but you shouldn't need it.
|
||||||
|
- This is a shared transform (`lib/wiki.py`), so it works for any pipeline, not just songs. Column width/gap styling currently lives in that transform.
|
||||||
|
|
||||||
## Renderer behaviour
|
## Renderer behaviour
|
||||||
|
|
||||||
For each `.md` with `wiki.publish: true` and not under `wip/`:
|
For each `.md` with `wiki.publish: true` and not under `wip/`:
|
||||||
|
|
||||||
1. Parse and validate frontmatter.
|
1. Parse and validate frontmatter.
|
||||||
2. Strip the leading `# Heading` from the body if present.
|
2. Strip the leading `# Heading` from the body if present.
|
||||||
3. Pipe the body through `pandoc -f markdown -t mediawiki`.
|
3. Pipe the body through `pandoc -f markdown -t mediawiki`, then expand any `columns` fenced blocks into side-by-side `<poem>` columns.
|
||||||
4. Substitute `TAG` placeholders with resolved `[[Page|Display]]` wikilinks (or fallback text).
|
4. Substitute `TAG` placeholders with resolved `[[Page|Display]]` wikilinks (or fallback text).
|
||||||
5. Prepend the auto-generated banner: `{{Auto-generated|source=<source URL>|commit=<sha>}}`.
|
5. Prepend the auto-generated banner: `{{Auto-generated|source=<source URL>|commit=<sha>}}`.
|
||||||
6. If `lrc` is declared: upload the LRC file if its SHA1 has changed; append a `[[Media:…]]` link.
|
6. If `lrc` is declared: upload the LRC file if its SHA1 has changed; append a `[[Media:…]]` link.
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ Publishes tech blog posts from `mikkeli/tech-blogs` to `https://wiki.novoyuuparo
|
|||||||
- YAML frontmatter parsing per [SCHEMA.md](SCHEMA.md)
|
- YAML frontmatter parsing per [SCHEMA.md](SCHEMA.md)
|
||||||
- Pandoc-based markdown → wikitext body rendering (h1 elements preserved)
|
- Pandoc-based markdown → wikitext body rendering (h1 elements preserved)
|
||||||
- Auto-generated banner (`{{Auto-generated|source=...|commit=<sha>}}`)
|
- Auto-generated banner (`{{Auto-generated|source=...|commit=<sha>}}`)
|
||||||
- Category injection: `[[Category:Tech blog]]` on every page; `[[Category:Blog:<year>]]` from `date` field; `[[Category:<tag>]]` per tag entry
|
- Category injection: `[[Category:Tech blog]]` on every page; `[[Category:Blog:<year>]]` from `date` field (falling back to the action's execution year); `[[Category:<tag>]]` per tag entry
|
||||||
- No title prefix — pages live at their bare title
|
- No title prefix — pages live at their bare title
|
||||||
- MediaWiki bot API write with idempotency — no-op if wiki content matches generated output
|
- MediaWiki bot API write with idempotency — no-op if wiki content matches generated output
|
||||||
- Files without `wiki.publish: true` silently skipped
|
- Files without `wiki.publish: true` silently skipped
|
||||||
@@ -65,7 +65,7 @@ Shared code lives in `lib/wiki.py` (repo root).
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| No title prefix | Tech posts live at bare titles — no namespace needed | 2026-06-10 |
|
| No title prefix | Tech posts live at bare titles — no namespace needed | 2026-06-10 |
|
||||||
| Fixed category | Every post gets `Category:Tech blog` | 2026-06-10 |
|
| Fixed category | Every post gets `Category:Tech blog` | 2026-06-10 |
|
||||||
| Year category | `Category:Blog:<year>` from `date` field; omitted if no date | 2026-06-10 |
|
| Year category | `Category:Blog:<year>` from `date` field; falls back to the execution year if no date | 2026-06-10 |
|
||||||
| Tag categories | Each tag → `[[Category:<tag>]]` with no prefix | 2026-06-10 |
|
| Tag categories | Each tag → `[[Category:<tag>]]` with no prefix | 2026-06-10 |
|
||||||
| WIP exclusion | None — `wiki.publish: false` is the only gate | 2026-06-10 |
|
| WIP exclusion | None — `wiki.publish: false` is the only gate | 2026-06-10 |
|
||||||
| Publish mode | Always `--all` | 2026-06-10 |
|
| Publish mode | Always `--all` | 2026-06-10 |
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ wiki:
|
|||||||
|
|
||||||
#### `date` (ISO 8601 date)
|
#### `date` (ISO 8601 date)
|
||||||
|
|
||||||
Publication date in `YYYY-MM-DD` format. Drives `[[Category:Blog:<year>]]` injection. If omitted, no year category is added.
|
Publication date in `YYYY-MM-DD` format. Drives `[[Category:Blog:<year>]]` injection. If omitted, the year falls back to the time the publish action runs.
|
||||||
|
|
||||||
#### `tags` (list of strings)
|
#### `tags` (list of strings)
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ For each `.md` with `wiki.publish: true`:
|
|||||||
1. Parse and validate frontmatter.
|
1. Parse and validate frontmatter.
|
||||||
2. Pipe the body through `pandoc -f markdown -t mediawiki`.
|
2. Pipe the body through `pandoc -f markdown -t mediawiki`.
|
||||||
4. Prepend the auto-generated banner: `{{Auto-generated|source=<source URL>|commit=<sha>}}`.
|
4. Prepend the auto-generated banner: `{{Auto-generated|source=<source URL>|commit=<sha>}}`.
|
||||||
5. Append category tags: `[[Category:Tech blog]]`; `[[Category:Blog:<year>]]` if `date` is set; `[[Category:<tag>]]` for each tag.
|
5. Append category tags: `[[Category:Tech blog]]`; `[[Category:Blog:<year>]]` (from `date`, or the action's execution year if `date` is absent); `[[Category:<tag>]]` for each tag.
|
||||||
6. Read the current wiki page content; if identical, skip the write (idempotency).
|
6. Read the current wiki page content; if identical, skip the write (idempotency).
|
||||||
7. If the page exists without `{{Auto-generated`, skip with a warning (manual page protection).
|
7. If the page exists without `{{Auto-generated`, skip with a warning (manual page protection).
|
||||||
8. Otherwise, write with an edit summary referencing the source commit.
|
8. Otherwise, write with an edit summary referencing the source commit.
|
||||||
@@ -80,7 +80,7 @@ For each `.md` with `wiki.publish: true`:
|
|||||||
- Pandoc-based markdown → wikitext rendering (h1 elements preserved)
|
- Pandoc-based markdown → wikitext rendering (h1 elements preserved)
|
||||||
- Banner template injection
|
- Banner template injection
|
||||||
- `Category:Tech blog` on every published page
|
- `Category:Tech blog` on every published page
|
||||||
- `Category:Blog:<year>` from `date` field
|
- `Category:Blog:<year>` from `date` field, falling back to the execution year
|
||||||
- `Category:<tag>` per entry in `tags`
|
- `Category:<tag>` per entry in `tags`
|
||||||
- Idempotent writes
|
- Idempotent writes
|
||||||
- Manual-page protection
|
- Manual-page protection
|
||||||
|
|||||||
@@ -41,9 +41,10 @@ def build_wikitext(fm: dict, body_wikitext: str, source_url: str, source_ref: st
|
|||||||
banner = f"{{{{Auto-generated|source={source_url}|commit={source_ref}}}}}"
|
banner = f"{{{{Auto-generated|source={source_url}|commit={source_ref}}}}}"
|
||||||
cat_parts = ["[[Category:Tech blog]]"]
|
cat_parts = ["[[Category:Tech blog]]"]
|
||||||
date_val = fm.get("date")
|
date_val = fm.get("date")
|
||||||
if date_val:
|
# Fall back to the action's execution year when no date is declared, so
|
||||||
year = str(date_val)[:4]
|
# every post still lands in a Blog:<year> category.
|
||||||
cat_parts.append(f"[[Category:Blog:{year}]]")
|
year = str(date_val)[:4] if date_val else str(datetime.now().year)
|
||||||
|
cat_parts.append(f"[[Category:Blog:{year}]]")
|
||||||
for tag in (fm.get("tags") or []):
|
for tag in (fm.get("tags") or []):
|
||||||
cat_parts.append(f"[[Category:{tag}]]")
|
cat_parts.append(f"[[Category:{tag}]]")
|
||||||
categories = "\n".join(cat_parts) + "\n"
|
categories = "\n".join(cat_parts) + "\n"
|
||||||
|
|||||||
Reference in New Issue
Block a user