Compare commits
2 Commits
5aaa290b76
...
a010db3450
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a010db3450 | ||
|
|
7e8661f68b |
35
.gitea/workflows/ci.yml
Normal file
35
.gitea/workflows/ci.yml
Normal file
@@ -0,0 +1,35 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master]
|
||||
pull_request:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: linux
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Markdown lint
|
||||
run: |
|
||||
podman run --rm \
|
||||
-v "${{ github.workspace }}:/work:Z" \
|
||||
docker.io/davidanson/markdownlint-cli2:v0.17.2 \
|
||||
"**/*.md"
|
||||
|
||||
link-check:
|
||||
runs-on: linux
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Check internal links
|
||||
run: |
|
||||
podman run --rm \
|
||||
-v "${{ github.workspace }}:/work:Z" \
|
||||
-w /work \
|
||||
docker.io/library/python:3.12-slim \
|
||||
python3 scripts/check-links.py
|
||||
29
.markdownlint-cli2.yaml
Normal file
29
.markdownlint-cli2.yaml
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
|
||||
globs:
|
||||
- "**/*.md"
|
||||
|
||||
ignores:
|
||||
- ".git"
|
||||
- ".venv"
|
||||
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())
|
||||
@@ -68,6 +68,34 @@ ssh-add -D
|
||||
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)
|
||||
|
||||
```ssh-config
|
||||
|
||||
Reference in New Issue
Block a user