Prepare automated manual build and Chapter 3 draft
Build SmartPark manual / manual (push) Successful in 51s
Build SmartPark manual / manual (push) Successful in 51s
This commit is contained in:
@@ -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()
|
||||
@@ -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
|
||||
@@ -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."
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user