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