Compare commits
8 Commits
5aaa290b76
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f533aad0fa | ||
|
|
fa82dd816d | ||
|
|
0a574734f4 | ||
|
|
53aa4ef1fc | ||
|
|
1a1c20c734 | ||
|
|
9e652d76e6 | ||
|
|
a010db3450 | ||
|
|
7e8661f68b |
30
.gitea/workflows/ci.yml
Normal file
30
.gitea/workflows/ci.yml
Normal 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
35
.markdownlint-cli2.yaml
Normal 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"
|
||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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/
|
||||||
|
|||||||
@@ -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
66
scripts/check-links.py
Normal 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())
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user