Prepare automated manual build and Chapter 3 draft
Build SmartPark manual / manual (push) Successful in 51s

This commit is contained in:
2026-10-09 21:01:49 -04:00
parent 2c9ddea66f
commit efe92994f6
11 changed files with 662 additions and 1 deletions
+86
View File
@@ -0,0 +1,86 @@
#!/usr/bin/env python3
"""Build only from a fresh temporary copy; publish output only on success."""
from pathlib import Path
import datetime, os, re, shutil, subprocess, tempfile
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / 'latex-manual'
OUT = ROOT / 'build' / 'manual'
def run(args, cwd=ROOT):
print('+ ' + ' '.join(args), flush=True)
subprocess.run(args, cwd=cwd, check=True)
def git(*args):
return subprocess.check_output(['git', *args], cwd=ROOT, text=True).strip()
def main():
OUT.mkdir(parents=True, exist_ok=True)
# Only remove this script's two fixed output files, so a failed run cannot
# leave a stale deliverable looking successful.
for name in ('SmartPark-Manual.pdf', 'build-info.txt'):
(OUT / name).unlink(missing_ok=True)
sha = git('rev-parse', 'HEAD')
expected = os.environ.get('MANUAL_COMMIT', '')
ci = os.environ.get('MANUAL_CI', '') == 'true'
dirty = bool(git('status', '--porcelain', '--untracked-files=normal'))
if ci and (not expected or expected != sha or dirty):
raise RuntimeError('CI checkout must be clean and match MANUAL_COMMIT')
number = os.environ.get('MANUAL_RUN_NUMBER', 'local')
if ci and not number.isdigit():
raise RuntimeError('Missing numeric Gitea run number')
if not re.fullmatch(r'[A-Za-z0-9.-]+', number):
raise RuntimeError('Unsafe run identifier')
revision = sha[:12] + ('-dirty' if dirty else '')
build_time = datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%d %H:%M UTC')
for tool in ('latexmk', 'pdflatex', 'bibtex', 'makeglossaries', 'makeindex', 'pygmentize'):
if shutil.which(tool) is None:
raise RuntimeError('Missing tool: ' + tool)
with tempfile.TemporaryDirectory(prefix='manual-', dir=ROOT / 'build') as temp:
work = Path(temp)
shutil.copytree(SOURCE, work, dirs_exist_ok=True,
ignore=shutil.ignore_patterns('*.aux', '*.log', '*.pdf', '*.toc', '*.out',
'*.bbl', '*.blg', '*.fdb_latexmk', '*.fls', '*.idx', '*.ind', '*.ilg',
'*.glo', '*.gls', '*.glg', '*.ist', '*.acn', '*.acr', '*.alg',
'*.lof', '*.lot', '_minted-*', 'build-metadata.tex'))
# Do not discard legitimate PDF figures: restore tracked/source PDFs
# except the old root document output.
for source_pdf in SOURCE.rglob('*.pdf'):
if source_pdf == SOURCE / 'devopsAssignment.pdf':
continue
target = work / source_pdf.relative_to(SOURCE)
target.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(source_pdf, target)
(work / 'build-metadata.tex').write_text(
'\\renewcommand{\\BuildRevision}{' + revision + '}\n' +
'\\renewcommand{\\BuildRun}{' + number + '}\n' +
'\\renewcommand{\\BuildUTC}{' + build_time + '}\n', encoding='utf-8')
command = ['latexmk', '-pdf', '-interaction=nonstopmode', '-halt-on-error',
'-file-line-error', '-shell-escape', 'devopsAssignment.tex']
run(command, work)
run(['makeglossaries', 'devopsAssignment'], work)
if (work / 'devopsAssignment.idx').stat().st_size:
run(['makeindex', 'devopsAssignment.idx'], work)
run(command, work)
pdf = work / 'devopsAssignment.pdf'
if not pdf.is_file() or pdf.stat().st_size < 5 or pdf.read_bytes()[:5] != b'%PDF-':
raise RuntimeError('Expected nonempty PDF was not produced')
log = (work / 'devopsAssignment.log').read_text(errors='replace')
if 'There were undefined references' in log or 'There were undefined citations' in log:
raise RuntimeError('PDF still contains unresolved references/citations; inspect source')
shutil.copy2(pdf, OUT / 'SmartPark-Manual.pdf')
info = f'commit={sha}\nrevision={revision}\nrun={number}\nutc={build_time}\n'
info += 'mode=' + ('Gitea Actions' if ci else 'LOCAL TEST - not Actions evidence') + '\n'
for tool, flag in [('latexmk','-v'), ('pdflatex','--version'), ('pygmentize','-V')]:
result = subprocess.check_output([tool, flag], text=True)
info += result
packages = subprocess.check_output(['dpkg-query', '-W',
'-f=${Package}\t${Version}\n', 'latexmk', 'texlive-base',
'texlive-latex-extra', 'texlive-fonts-recommended', 'lmodern', 'python3-pygments'], text=True)
info += '\nUbuntu package versions:\n' + packages
(OUT / 'build-info.txt').write_text(info, encoding='utf-8')
print(info)
print('SUCCESS: ' + str(OUT / 'SmartPark-Manual.pdf'))
if __name__ == '__main__':
main()
+13
View File
@@ -0,0 +1,13 @@
log:
level: info
runner:
file: /var/lib/smartpark-runner/.runner
capacity: 1
timeout: 20m
shutdown_timeout: 25m
labels:
- smartpark-latex:host
cache:
enabled: false
host:
workdir_parent: /var/lib/smartpark-runner/work
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
# Run as your normal Ubuntu user, not as root. Uses sudo for installation.
set -euo pipefail
[[ $(uname -m) == x86_64 ]] || { echo 'This installer targets x86_64 only'; exit 1; }
base=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
[[ ! -e /etc/smartpark-runner/config.yaml ]] || { echo 'Runner already configured; inspect it rather than overwrite.'; exit 1; }
sudo apt-get update
sudo apt-get install --no-install-recommends curl ca-certificates xz-utils
downloads=$(mktemp -d)
cd "$downloads"
# Resolve the current Node 22 release from its official checksum list, then
# install and record that exact release. No downloaded install script is run.
curl -fSLO https://nodejs.org/dist/latest-v22.x/SHASUMS256.txt
node_archive=$(awk '$2 ~ /^node-v22\.[0-9]+\.[0-9]+-linux-x64\.tar\.xz$/ {print $2}' SHASUMS256.txt)
[[ -n "$node_archive" && "$node_archive" != *$'\n'* ]]
node_version=${node_archive#node-}
node_version=${node_version%-linux-x64.tar.xz}
curl -fSLO "https://nodejs.org/dist/$node_version/$node_archive"
awk -v f="$node_archive" '$2 == f' SHASUMS256.txt | sha256sum -c -
tar -xf "$node_archive"
[[ ! -e /opt/smartpark-node ]] || { echo '/opt/smartpark-node exists; inspect before continuing'; exit 1; }
sudo mv "${node_archive%.tar.xz}" /opt/smartpark-node
sudo chown -R root:root /opt/smartpark-node
runner_version=2.3.0
runner_file="gitea-runner-$runner_version-linux-amd64"
curl -fSLO "https://dl.gitea.com/gitea-runner/$runner_version/$runner_file"
curl -fSLO "https://dl.gitea.com/gitea-runner/$runner_version/$runner_file.sha256"
sha256sum -c "$runner_file.sha256"
sudo install -m 0755 "$runner_file" /usr/local/bin/gitea-runner
if id smartpark-runner >/dev/null 2>&1; then
echo 'smartpark-runner account already exists; inspect its permissions before continuing.'
exit 1
fi
sudo useradd --system --create-home --home-dir /var/lib/smartpark-runner --shell /usr/sbin/nologin smartpark-runner
sudo chmod 700 /var/lib/smartpark-runner
sudo install -d -o smartpark-runner -g smartpark-runner -m 700 /var/lib/smartpark-runner/work
sudo install -d -o root -g smartpark-runner -m 750 /etc/smartpark-runner
sudo install -o root -g smartpark-runner -m 640 "$base/runner-config.yaml" /etc/smartpark-runner/config.yaml
sudo install -m 644 "$base/smartpark-runner.service" /etc/systemd/system/smartpark-runner.service
sudo systemctl daemon-reload
sudo -u smartpark-runner /opt/smartpark-node/bin/node --version
sudo -u smartpark-runner /usr/local/bin/gitea-runner --version
sudo -u smartpark-runner latexmk -v
echo 'Installed, but NOT registered or started. Registration requires an authorized token.'
echo "Verified downloads remain in $downloads; remove those files later using the Files app if desired."
+23
View File
@@ -0,0 +1,23 @@
[Unit]
Description=SmartPark Gitea manual runner
After=network-online.target
Wants=network-online.target
[Service]
User=smartpark-runner
Group=smartpark-runner
WorkingDirectory=/var/lib/smartpark-runner
Environment=HOME=/var/lib/smartpark-runner
Environment=PATH=/opt/smartpark-node/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/usr/local/bin/gitea-runner daemon --config /etc/smartpark-runner/config.yaml
Restart=on-failure
RestartSec=10
UMask=0077
NoNewPrivileges=true
PrivateTmp=true
ProtectHome=true
ProtectSystem=full
TimeoutStopSec=30min
[Install]
WantedBy=multi-user.target