Project template: honest CI, gitleaks config, protection bootstrap
This commit is contained in:
@@ -0,0 +1,3 @@
|
|||||||
|
# Copy to .env (gitignored) and fill in. Real secrets belong in Infisical:
|
||||||
|
# https://secrets.levkin.ca -> LevkinOps -> /apps/<this-repo>
|
||||||
|
EXAMPLE_API_KEY=changeme
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
# Homelab CI — Python lane + secret scan.
|
||||||
|
# POLICY: lint and tests are HARD gates. Never add `|| true` to them.
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
python-ci:
|
||||||
|
runs-on: [homelab, self-hosted, linux, python]
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Python tooling
|
||||||
|
run: |
|
||||||
|
apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-pip python3-venv
|
||||||
|
python3 -m pip install --upgrade pip --break-system-packages
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt --break-system-packages; fi
|
||||||
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt --break-system-packages; fi
|
||||||
|
pip install pytest ruff --break-system-packages
|
||||||
|
|
||||||
|
- name: Ruff lint (hard gate)
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Pytest (hard gate)
|
||||||
|
run: pytest -q
|
||||||
|
|
||||||
|
# Advisory-only scanners (allowed to be soft — they're noisy, not gates)
|
||||||
|
- name: Bandit (advisory)
|
||||||
|
run: pip install bandit --break-system-packages && bandit -r . -q || true
|
||||||
|
|
||||||
|
secret-scan:
|
||||||
|
runs-on: [homelab, self-hosted, linux, heavy]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Gitleaks
|
||||||
|
run: |
|
||||||
|
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest \
|
||||||
|
detect --source /repo --config /repo/.gitleaks.toml --no-banner --redact
|
||||||
+54
@@ -0,0 +1,54 @@
|
|||||||
|
# --- OS / editor ---
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
|
||||||
|
# --- Secrets & env (real values never in git) ---
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*password*
|
||||||
|
*credential*
|
||||||
|
!*example*
|
||||||
|
|
||||||
|
# --- Real data (commit *.example.* fixtures instead) ---
|
||||||
|
data/*.json
|
||||||
|
data/*.yml
|
||||||
|
data/*.yaml
|
||||||
|
!data/*.example.*
|
||||||
|
*.local.yml
|
||||||
|
*.local.json
|
||||||
|
|
||||||
|
# --- Python ---
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
.mypy_cache/
|
||||||
|
.ruff_cache/
|
||||||
|
|
||||||
|
# --- Node ---
|
||||||
|
node_modules/
|
||||||
|
dist/
|
||||||
|
coverage/
|
||||||
|
npm-debug.log*
|
||||||
|
|
||||||
|
# --- Outputs / scratch ---
|
||||||
|
out/
|
||||||
|
tmp/
|
||||||
|
.tmp/
|
||||||
|
scratch/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# --- Agent tooling (local, not for the repo) ---
|
||||||
|
.cursor/
|
||||||
|
.claude/
|
||||||
|
.codex/
|
||||||
|
.agents/
|
||||||
|
.opencode/
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
# Repo gitleaks config — same rules as the global fallback
|
||||||
|
# (~/.config/git/gitleaks.toml) so local hook and CI agree.
|
||||||
|
|
||||||
|
title = "repo gitleaks"
|
||||||
|
|
||||||
|
[extend]
|
||||||
|
useDefault = true
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "env-file-committed"
|
||||||
|
description = "Raw .env file (or backup/variant) staged for commit"
|
||||||
|
path = '''(^|/)\.env(\.(backup|bak|old|orig|save|local|prod|production|dev|development|staging))?$'''
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "password-or-credential-file"
|
||||||
|
description = "File name indicates stored password/credential material"
|
||||||
|
path = '''(?i)(^|/)[^/]*(password|passwd|credential)[^/]*\.(txt|json|ya?ml|env|cfg|ini)$'''
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "private-key-file"
|
||||||
|
description = "Private key or keystore file staged for commit"
|
||||||
|
path = '''(^|/)(id_rsa|id_ed25519|id_ecdsa)[^/]*$|\.(pem|p12|pfx|key)$'''
|
||||||
|
|
||||||
|
[[rules]]
|
||||||
|
id = "dotfile-secret-token"
|
||||||
|
description = "Hidden standalone secret/token file"
|
||||||
|
path = '''(^|/)\.[^/]*(secret|token)[^/]*$'''
|
||||||
|
|
||||||
|
[allowlist]
|
||||||
|
description = "Legitimate non-secret files matching the patterns above"
|
||||||
|
paths = [
|
||||||
|
'''(?i)\.(example|sample|template|dist)$''',
|
||||||
|
'''(^|/)\.env\.example[^/]*$''',
|
||||||
|
'''(^|/)[^/]*\.pub$''',
|
||||||
|
'''(^|/)package(-lock)?\.json$''',
|
||||||
|
]
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 Ilia Dobkin
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# project-template
|
||||||
|
|
||||||
|
Clone-and-go skeleton for new projects. Every repo started from this template
|
||||||
|
gets, on day one: honest CI (lint + tests are **hard gates**, never `|| true`),
|
||||||
|
secret scanning, branch protection, LICENSE, and a gitignore that keeps real
|
||||||
|
data out of git.
|
||||||
|
|
||||||
|
Born out of the 2026-07 portfolio hygiene sweep — see
|
||||||
|
`~/.cursor/rules/new-project-standards.mdc` for the standards this enforces.
|
||||||
|
|
||||||
|
## Start a new project
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. Copy the skeleton (don't clone-fork; you want fresh history)
|
||||||
|
cp -R ~/Documents/code/project-template ~/Documents/code/<name>
|
||||||
|
cd ~/Documents/code/<name>
|
||||||
|
rm -rf .git && git init -b main
|
||||||
|
|
||||||
|
# 2. Pick your CI lane and drop the other
|
||||||
|
cp ci-templates/ci-python.yml .gitea/workflows/ci.yml # or ci-node.yml
|
||||||
|
rm -rf ci-templates
|
||||||
|
|
||||||
|
# 3. Fill in README/LICENSE year, write code, commit
|
||||||
|
|
||||||
|
# 4. Create the Gitea repo with protection + settings in one shot
|
||||||
|
./bootstrap.sh <name> python # or: node
|
||||||
|
```
|
||||||
|
|
||||||
|
`bootstrap.sh` creates the (private by default) repo on `git.levkin.ca`,
|
||||||
|
enables delete-branch-after-merge, pushes `main`, and adds branch protection
|
||||||
|
requiring the CI contexts. Make a repo public only after it passes the
|
||||||
|
public-repo checklist below.
|
||||||
|
|
||||||
|
## Rules baked in
|
||||||
|
|
||||||
|
- **No `|| true` on lint or test steps.** If a gate is red, fix it or narrow
|
||||||
|
the config with a comment — never mute the gate.
|
||||||
|
- **Real data never in fixtures.** Commit `*.example.json` / `*.example.yml`
|
||||||
|
with fake values; real files are gitignored (see `.gitignore`).
|
||||||
|
- **No secrets in git, ever.** Secrets live in Infisical
|
||||||
|
(`secrets.levkin.ca`, LevkinOps → `/apps/<repo>`) or local gitignored
|
||||||
|
`.env`. The global pre-commit hook (gitleaks + filename denylist) is the
|
||||||
|
last line of defense, not the first.
|
||||||
|
- **No absolute home paths in code.** Use env vars or `Path.home()` /
|
||||||
|
`process.env.HOME`.
|
||||||
|
- **No LAN IPs / homelab topology in public docs.** Use placeholders; deploy
|
||||||
|
runbooks live in the private ansible repo.
|
||||||
|
|
||||||
|
## Public-repo checklist (before flipping `private: false`)
|
||||||
|
|
||||||
|
- [ ] LICENSE present, README explains what/why/how for an outsider
|
||||||
|
- [ ] `rg "10\.0\.10\."` returns nothing
|
||||||
|
- [ ] `rg "/Users/"` returns nothing
|
||||||
|
- [ ] No real names/addresses/phones except your intentional public persona
|
||||||
|
- [ ] gitleaks full-history scan clean: `gitleaks detect --source .`
|
||||||
|
- [ ] CI green with hard gates
|
||||||
Executable
+52
@@ -0,0 +1,52 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Create the Gitea repo for this project with gates preconfigured:
|
||||||
|
# private repo, delete-branch-after-merge, branch protection on main
|
||||||
|
# requiring the CI contexts, initial push.
|
||||||
|
#
|
||||||
|
# Usage: ./bootstrap.sh <repo-name> <python|node> [--public]
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
NAME="${1:?usage: bootstrap.sh <repo-name> <python|node> [--public]}"
|
||||||
|
LANE="${2:?usage: bootstrap.sh <repo-name> <python|node> [--public]}"
|
||||||
|
PRIVATE=true
|
||||||
|
[[ "${3:-}" == "--public" ]] && PRIVATE=false
|
||||||
|
|
||||||
|
GITEA="https://git.levkin.ca"
|
||||||
|
OWNER="ilia"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$HOME/.config/hermes/gitea.env" # provides GITEA_TOKEN
|
||||||
|
|
||||||
|
api() {
|
||||||
|
local method="$1" path="$2" body="${3:-}"
|
||||||
|
curl -sf -X "$method" -H "Authorization: token ${GITEA_TOKEN}" \
|
||||||
|
-H "Content-Type: application/json" ${body:+-d "$body"} "${GITEA}/api/v1${path}"
|
||||||
|
}
|
||||||
|
|
||||||
|
case "$LANE" in
|
||||||
|
python) CONTEXTS='["CI / python-ci (pull_request)","CI / secret-scan (pull_request)"]' ;;
|
||||||
|
node) CONTEXTS='["CI / node-ci (pull_request)","CI / secret-scan (pull_request)"]' ;;
|
||||||
|
*) echo "lane must be python or node" >&2; exit 1 ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [[ ! -f ".gitea/workflows/ci.yml" ]]; then
|
||||||
|
echo "ERROR: .gitea/workflows/ci.yml missing — copy one from ci-templates/ first." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Creating ${OWNER}/${NAME} (private=${PRIVATE})..."
|
||||||
|
api POST /user/repos "{\"name\":\"${NAME}\",\"private\":${PRIVATE},\"default_branch\":\"main\"}" >/dev/null
|
||||||
|
|
||||||
|
echo "Enabling delete-branch-after-merge..."
|
||||||
|
api PATCH "/repos/${OWNER}/${NAME}" '{"default_delete_branch_after_merge": true}' >/dev/null
|
||||||
|
|
||||||
|
echo "Pushing main..."
|
||||||
|
git remote add origin "${GITEA}/${OWNER}/${NAME}.git" 2>/dev/null || true
|
||||||
|
git push "https://${OWNER}:${GITEA_TOKEN}@git.levkin.ca/${OWNER}/${NAME}.git" main
|
||||||
|
|
||||||
|
echo "Adding branch protection (required contexts: ${CONTEXTS})..."
|
||||||
|
api POST "/repos/${OWNER}/${NAME}/branch_protections" \
|
||||||
|
"{\"branch_name\":\"main\",\"rule_name\":\"main\",\"enable_push\":false,\"enable_status_check\":true,\"status_check_contexts\":${CONTEXTS},\"required_approvals\":0}" >/dev/null
|
||||||
|
|
||||||
|
echo "Done: ${GITEA}/${OWNER}/${NAME}"
|
||||||
|
echo "Work via PRs from now on; merge with:"
|
||||||
|
echo " GITEA_REPO=${NAME} bash ~/Documents/code/hermes/scripts/gitea-merge-when-green.sh <pr>"
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
---
|
||||||
|
# Homelab CI — Node lane + secret scan.
|
||||||
|
# POLICY: lint, typecheck, and tests are HARD gates. Never add `|| true` to them.
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
node-ci:
|
||||||
|
runs-on: [homelab, self-hosted, linux]
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install
|
||||||
|
run: npm ci
|
||||||
|
|
||||||
|
- name: Lint (hard gate)
|
||||||
|
run: npm run lint --if-present
|
||||||
|
|
||||||
|
- name: Typecheck (hard gate)
|
||||||
|
run: npm run typecheck --if-present
|
||||||
|
|
||||||
|
- name: Test (hard gate)
|
||||||
|
run: npm test
|
||||||
|
|
||||||
|
secret-scan:
|
||||||
|
runs-on: [homelab, self-hosted, linux, heavy]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Gitleaks
|
||||||
|
run: |
|
||||||
|
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest \
|
||||||
|
detect --source /repo --config /repo/.gitleaks.toml --no-banner --redact
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
---
|
||||||
|
# Homelab CI — Python lane + secret scan.
|
||||||
|
# POLICY: lint and tests are HARD gates. Never add `|| true` to them.
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
types: [opened, synchronize, reopened]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
python-ci:
|
||||||
|
runs-on: [homelab, self-hosted, linux, python]
|
||||||
|
container:
|
||||||
|
image: node:20-bookworm
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Python tooling
|
||||||
|
run: |
|
||||||
|
apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq python3 python3-pip python3-venv
|
||||||
|
python3 -m pip install --upgrade pip --break-system-packages
|
||||||
|
if [ -f requirements.txt ]; then pip install -r requirements.txt --break-system-packages; fi
|
||||||
|
if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt --break-system-packages; fi
|
||||||
|
pip install pytest ruff --break-system-packages
|
||||||
|
|
||||||
|
- name: Ruff lint (hard gate)
|
||||||
|
run: ruff check .
|
||||||
|
|
||||||
|
- name: Pytest (hard gate)
|
||||||
|
run: pytest -q
|
||||||
|
|
||||||
|
# Advisory-only scanners (allowed to be soft — they're noisy, not gates)
|
||||||
|
- name: Bandit (advisory)
|
||||||
|
run: pip install bandit --break-system-packages && bandit -r . -q || true
|
||||||
|
|
||||||
|
secret-scan:
|
||||||
|
runs-on: [homelab, self-hosted, linux, heavy]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- name: Gitleaks
|
||||||
|
run: |
|
||||||
|
docker run --rm -v "$PWD:/repo" ghcr.io/gitleaks/gitleaks:latest \
|
||||||
|
detect --source /repo --config /repo/.gitleaks.toml --no-banner --redact
|
||||||
Reference in New Issue
Block a user