Compare commits

...

8 Commits

Author SHA1 Message Date
user
f533aad0fa fix: add language tags to remaining bare code fences
All checks were successful
ci / link-check (push) Successful in 16s
ci / lint (push) Successful in 20s
2026-02-22 08:19:59 +01:00
user
fa82dd816d fix: resolve markdownlint errors
Some checks failed
ci / link-check (push) Successful in 16s
ci / lint (push) Failing after 19s
Disable MD060 (table column alignment) and MD032 (list blank lines).
Add language tags to bare fenced code blocks (MD040).
2026-02-22 08:04:08 +01:00
user
0a574734f4 fix: use node:22-alpine for lint job
Some checks failed
ci / link-check (push) Successful in 16s
ci / lint (push) Failing after 26s
markdownlint-cli2 image runs as non-root, blocking apk install.
Use node-alpine base with npx instead.
2026-02-22 07:38:47 +01:00
user
53aa4ef1fc fix: use container: directive instead of podman run on host
Some checks failed
ci / lint (push) Failing after 12s
ci / link-check (push) Successful in 18s
Match s5p CI pattern — run jobs inside containers managed by the
runner rather than invoking podman directly on the host. Avoids
rootless namespace permission issues.
2026-02-22 07:37:19 +01:00
user
1a1c20c734 fix: use podman containers for checkout on minimal host runner
Some checks failed
CI / lint (push) Failing after 1s
CI / link-check (push) Failing after 1s
Host runner lacks git and node. Fetch repo archive via wget in
alpine container, then run lint/link-check against extracted files.
Single-line commands to avoid multiline escaping issues.
2026-02-22 06:38:14 +01:00
user
9e652d76e6 fix: replace actions/checkout with git clone for host runner
Some checks failed
CI / lint (push) Failing after 1s
CI / link-check (push) Failing after 1s
The linux runner lacks node, so actions/checkout@v4 fails.
Use plain git clone instead.
2026-02-22 06:35:09 +01:00
user
a010db3450 feat: add Gitea CI workflow with podman-based lint and link check
Some checks failed
CI / lint (push) Failing after 2s
CI / link-check (push) Failing after 2s
- Markdown lint via markdownlint-cli2 container
- Internal cross-reference checker for See Also sections
- Both jobs run on linux runner using podman containers
2026-02-22 06:33:28 +01:00
user
7e8661f68b docs: add non-interactive ssh-add patterns to ssh howto
Cover passphrase injection via script(1) with env vars
and the SSH_ASKPASS alternative for headless automation.
2026-02-22 02:13:32 +01:00
11 changed files with 168 additions and 9 deletions

30
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,30 @@
name: ci
on:
push:
branches: [master]
pull_request:
branches: [master]
jobs:
lint:
runs-on: linux
container: node:22-alpine
steps:
- run: apk add --no-cache git
- run: |
git clone --depth 1 \
-c "http.extraHeader=Authorization: token ${{ github.token }}" \
"${{ github.server_url }}/${{ github.repository }}.git" .
- run: npx markdownlint-cli2 "**/*.md"
link-check:
runs-on: linux
container: python:3.12-slim
steps:
- run: apt-get update -qq && apt-get install -y -qq git > /dev/null
- run: |
git clone --depth 1 \
-c "http.extraHeader=Authorization: token ${{ github.token }}" \
"${{ github.server_url }}/${{ github.repository }}.git" .
- run: python3 scripts/check-links.py

35
.markdownlint-cli2.yaml Normal file
View File

@@ -0,0 +1,35 @@
config:
# Allow long lines in tables and code blocks
MD013:
line_length: 200
tables: false
code_blocks: false
# Allow duplicate headings across files (common in howtos)
MD024:
siblings_only: true
# Allow trailing punctuation in headings (e.g. "~/.ssh/config")
MD026: false
# Allow inline HTML (rare but sometimes needed)
MD033: false
# Allow bare URLs in markdown
MD034: false
# Allow multiple blank lines
MD012: false
# Allow mixed table column alignment
MD060: false
# Allow lists without surrounding blank lines (tight prose)
MD032: false
globs:
- "**/*.md"
ignores:
- ".git"
- ".venv"

View File

@@ -15,7 +15,7 @@ Scattered notes, bookmarks, and `history | grep` are poor substitutes for well-o
Flat file collection — no build step, no dependencies. Plain Markdown rendered by any viewer (terminal, browser, editor). Flat file collection — no build step, no dependencies. Plain Markdown rendered by any viewer (terminal, browser, editor).
``` ```text
topics/<topic>.md # Individual howto files topics/<topic>.md # Individual howto files
docs/TEMPLATE.md # Template for new topics docs/TEMPLATE.md # Template for new topics
``` ```

View File

@@ -8,7 +8,7 @@ Quick-reference documentation — concise, searchable, copy-pasteable. Each topi
## Structure ## Structure
``` ```text
topics/ topics/
<topic>.md # One file per subject <topic>.md # One file per subject
docs/ docs/

View File

@@ -13,7 +13,7 @@
Each topic file follows `docs/TEMPLATE.md`: Each topic file follows `docs/TEMPLATE.md`:
``` ```text
# <Topic> # <Topic>
## Overview — one-liner description ## Overview — one-liner description
## Common Commands — daily-driver commands ## Common Commands — daily-driver commands

66
scripts/check-links.py Normal file
View File

@@ -0,0 +1,66 @@
#!/usr/bin/env python3
"""Check internal cross-references between topic files.
Scans all .md files for See Also links to other topics (backtick-quoted names)
and verifies that matching files exist under topics/.
Exit codes:
0 — all links valid
1 — broken links found
"""
import re
import sys
from pathlib import Path
TOPICS_DIR = Path("topics")
DOCS_DIR = Path("docs")
SEARCH_DIRS = [Path("."), TOPICS_DIR, DOCS_DIR]
EXCLUDE = {"docs/TEMPLATE.md"}
def find_md_files() -> list[Path]:
files = []
for d in SEARCH_DIRS:
if d.exists():
files.extend(d.glob("*.md"))
return sorted(set(files))
def extract_topic_refs(path: Path) -> list[tuple[int, str]]:
"""Extract backtick-quoted topic references from See Also sections."""
refs = []
in_see_also = False
for lineno, line in enumerate(path.read_text().splitlines(), start=1):
if re.match(r"^##\s+See Also", line, re.IGNORECASE):
in_see_also = True
continue
if in_see_also and re.match(r"^##\s+", line):
break
if in_see_also:
for match in re.finditer(r"`([a-z][a-z0-9-]*)`", line):
refs.append((lineno, match.group(1)))
return refs
def check_links() -> int:
md_files = find_md_files()
topic_names = {p.stem for p in TOPICS_DIR.glob("*.md")} if TOPICS_DIR.exists() else set()
errors = 0
for path in [p for p in md_files if str(p) not in EXCLUDE]:
for lineno, ref in extract_topic_refs(path):
if ref not in topic_names:
print(f" {path}:{lineno} broken ref `{ref}` — no topics/{ref}.md")
errors += 1
if errors:
print(f"\n {errors} broken link(s) found")
return 1
print(f" {len(md_files)} files checked, all internal links valid")
return 0
if __name__ == "__main__":
sys.exit(check_links())

View File

@@ -63,7 +63,7 @@ all:
## Directory Layout ## Directory Layout
``` ```text
inventory/ inventory/
├── hosts.yml # Host definitions ├── hosts.yml # Host definitions
├── group_vars/ ├── group_vars/
@@ -82,7 +82,7 @@ inventory/
### Multiple Environments ### Multiple Environments
``` ```text
inventories/ inventories/
├── staging/ ├── staging/
│ ├── hosts.yml │ ├── hosts.yml

View File

@@ -4,7 +4,7 @@
## Role Directory Structure ## Role Directory Structure
``` ```text
roles/ roles/
└── nginx/ └── nginx/
├── defaults/ ├── defaults/
@@ -164,7 +164,7 @@ nginx_access_log: /var/log/nginx/access.log
## Project Layout with Roles ## Project Layout with Roles
``` ```text
ansible-project/ ansible-project/
├── ansible.cfg ├── ansible.cfg
├── site.yml # Master playbook ├── site.yml # Master playbook

View File

@@ -6,7 +6,7 @@
Ansible merges variables from many sources. **Higher number wins.** Ansible merges variables from many sources. **Higher number wins.**
``` ```text
1. command line values (for constants, not variables) 1. command line values (for constants, not variables)
2. role defaults (roles/x/defaults/main.yml) 2. role defaults (roles/x/defaults/main.yml)
3. inventory file or script group vars 3. inventory file or script group vars

View File

@@ -68,6 +68,34 @@ ssh-add -D
ssh -A user@bastion # remote can use your local keys ssh -A user@bastion # remote can use your local keys
``` ```
### Non-Interactive ssh-add (passphrase from env)
`ssh-add` insists on a terminal for passphrase input. Use `script` to fake a TTY and feed the passphrase from an environment variable:
```bash
# SSH_KEY_PASS must be set (e.g. sourced from a secrets file)
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"
```
Useful in automation (CI, cron, Ansible) where no interactive terminal exists.
```bash
# Full pattern: source secrets, start agent, add key
source ~/.bashrc.secrets # exports SSH_KEY_PASS
eval "$(ssh-agent -s)"
{ sleep 0.1; echo "$SSH_KEY_PASS"; } | script -q /dev/null -c "ssh-add $HOME/.ssh/id_ed25519"
ssh-add -l # verify key loaded
```
Alternative with `SSH_ASKPASS` (avoids `script`):
```bash
export SSH_ASKPASS_REQUIRE=force
export SSH_ASKPASS="$(mktemp)" && printf '#!/bin/sh\necho "$SSH_KEY_PASS"' > "$SSH_ASKPASS" && chmod +x "$SSH_ASKPASS"
ssh-add ~/.ssh/id_ed25519
rm -f "$SSH_ASKPASS"
```
## Config File (~/.ssh/config) ## Config File (~/.ssh/config)
```ssh-config ```ssh-config

View File

@@ -246,7 +246,7 @@ systemctl list-timers
### OnCalendar Syntax ### OnCalendar Syntax
``` ```text
# Format: DayOfWeek Year-Month-Day Hour:Minute:Second # Format: DayOfWeek Year-Month-Day Hour:Minute:Second
*-*-* 02:00:00 # daily at 2am *-*-* 02:00:00 # daily at 2am