diff --git a/.claude/skills/setup-project/SKILL.md b/.claude/skills/setup-project/SKILL.md index 038c5196..3a5acf69 100644 --- a/.claude/skills/setup-project/SKILL.md +++ b/.claude/skills/setup-project/SKILL.md @@ -43,6 +43,40 @@ date: 2025-01-01 5. Check releases page for container images 6. Last resort: Build from Dockerfile (avoid if possible) +**Classify Dockerfile State** (drives whether we contribute a PR back upstream later): + +| State | When | Action on deploy success | +|---|---|---| +| `image-used` | An official/community image worked (priority 1-5). | No upstream PR. Default case. | +| `used-as-is` | Upstream ships a Dockerfile; it built and ran fine. | No upstream PR. | +| `fixed-broken-upstream` | Upstream Dockerfile exists but fails to build / run; we patched it. | Open a `fix-dockerfile` PR after stability gate. | +| `written-from-scratch` | Upstream has no Dockerfile at all; we authored one. | Open an `add-dockerfile` PR after stability gate. | + +Record the chosen state and supporting metadata in `modules/kubernetes//.contribution-state.json`. When we author or fix a Dockerfile, also write `modules/kubernetes//files/Dockerfile`, `.dockerignore`, and `BUILD.md` (from `templates/Dockerfile.README.md`) — these travel with the upstream PR. + +```json +{ + "upstream_repo": "owner/name", + "dockerfile_state": "written-from-scratch", + "dockerfile_path_in_infra": "modules/kubernetes//files/Dockerfile", + "deploy_target_url": "https://.viktorbarzin.me", + "image_tag": "registry.viktorbarzin.me/:", + "image_size": "", + "base_image": "", + "dockerfile_shape": "multi-stage, non-root, linux/amd64", + "deploy_verified_at": null, + "contribution_pr_url": null +} +``` + +**Dockerfile quality bar** (when writing one ourselves — enforced before PR): +- Multi-stage build where it makes sense (Node, Go, Rust, Python with compiled deps). +- Explicit non-root `USER`. +- `HEALTHCHECK` when the app exposes a known endpoint. +- Minimal base image (alpine / distroless preferred; `-slim` otherwise). +- No secrets baked in; runtime config via `ENV`. +- `.dockerignore` that excludes `.git`, `node_modules`, test artifacts. + **Extract Configuration**: - Container port (default port the app listens on) - Environment variables (DATABASE_URL, REDIS_HOST, SMTP, etc.) @@ -345,6 +379,21 @@ kubectl logs -n -l app= --tail=50 Test URL: `https://.viktorbarzin.me` +### 8b. Stability Gate (required when `dockerfile_state ∈ {written-from-scratch, fixed-broken-upstream}`) + +Before committing — and before any upstream PR in §10 — run a 10-minute stability check to catch pods that crash-loop a few minutes after Ready. + +```bash +.claude/skills/setup-project/scripts/stability-gate.sh https://.viktorbarzin.me +``` + +Polls pod readiness + `curl` 200 every 30s × 20 iterations. Requires 18/20 successes (tolerates 2 blips). + +- **Pass** → update the state file: `jq '.deploy_verified_at = (now | todate)' .contribution-state.json | sponge .contribution-state.json` → proceed to §9 and §10. +- **Fail** → stop. Investigate via `kubectl logs`, `kubectl describe`. Do NOT commit. Do NOT fire §10. Re-run the gate after fixes. + +For `image-used` / `used-as-is` states, the gate is optional (app is already running a known-good image). + ### 9. Commit Changes ```bash @@ -358,6 +407,37 @@ git commit -m "Add deployment [ci skip]" ``` +### 10. Contribute Dockerfile Upstream (only when `dockerfile_state ∈ {written-from-scratch, fixed-broken-upstream}`) + +Goal: give the community the working Dockerfile we just validated in production. + +**Preconditions** (script enforces): +- `.contribution-state.json` present with a trigger state and `deploy_verified_at` set. +- `files/Dockerfile`, `files/.dockerignore`, `files/BUILD.md` exist next to the module. +- `GITHUB_TOKEN` in env — or `vault kv get -field=github_pat secret/viktor` is reachable. + +**Run**: +```bash +.claude/skills/setup-project/scripts/contribute-dockerfile.sh modules/kubernetes/ +``` + +**What the script does** (all via GitHub REST — `gh` CLI is sandbox-blocked): +1. Reads `.contribution-state.json`; skips unless state is `written-from-scratch` or `fixed-broken-upstream` and no `contribution_pr_url` is already recorded. +2. Upstream sanity checks: repo exists, public, not archived; default branch discoverable; for `written-from-scratch`, verifies a `Dockerfile` didn't land upstream while we were deploying; bails cleanly if an open PR from our fork already exists. +3. `POST /repos///forks` — idempotent; waits up to 30s for the fork to be ready at `ViktorBarzin/`. +4. `POST /repos/ViktorBarzin//merge-upstream` — keeps fork current with upstream default branch. +5. Creates branch `add-dockerfile` (or `fix-dockerfile`), timestamp-suffixed if that branch already exists with unrelated commits. +6. Commits `Dockerfile`, `.dockerignore`, `BUILD.md` via Contents API. Each commit message carries `Signed-off-by:` for DCO-enforcing repos. +7. Opens PR against upstream with body rendered from `templates/PR_BODY.md`. +8. Writes `contribution_pr_url` back into `.contribution-state.json` and echoes the URL. + +**Failure handling**: +- Upstream archived / private / deleted → logged as SKIP, deploy success stands. +- Fork/branch/PR already exists → treated as idempotent success; existing URL recorded. +- GitHub 5xx → 3× exponential backoff, then hard fail with a clear message — safe to re-run the script. + +**After the PR opens**: the URL is in `.contribution-state.json`. Share it with the user. No automated follow-up on merge/reject — that's a manual check for now. + ## Common Patterns ### Init Container for Migrations @@ -410,13 +490,17 @@ env { - [ ] Create NFS directory and export on TrueNAS (if persistent storage needed) - [ ] Verify NFS mount is accessible from k8s nodes - [ ] Create `modules/kubernetes//main.tf` +- [ ] Classify `dockerfile_state` and write `.contribution-state.json` +- [ ] If writing/fixing Dockerfile: satisfy the quality bar (multi-stage, non-root, `.dockerignore`, `BUILD.md`) - [ ] Update `modules/kubernetes/main.tf` (variables, DEFCON level, module block) - [ ] Update `main.tf` (variable, pass to module) - [ ] Update `terraform.tfvars` (password, Cloudflare DNS) - [ ] Run `terraform init` and `terraform apply` - [ ] Verify pods are running - [ ] Test the URL +- [ ] Run stability-gate.sh — needed for contribution, optional otherwise - [ ] Commit changes with `[ci skip]` +- [ ] Run contribute-dockerfile.sh if state triggers an upstream PR ## Questions to Ask User diff --git a/.claude/skills/setup-project/scripts/contribute-dockerfile.sh b/.claude/skills/setup-project/scripts/contribute-dockerfile.sh new file mode 100755 index 00000000..18ade20e --- /dev/null +++ b/.claude/skills/setup-project/scripts/contribute-dockerfile.sh @@ -0,0 +1,270 @@ +#!/usr/bin/env bash +# Contribute a working Dockerfile back to an upstream GitHub repo. +# +# Reads state from /.contribution-state.json and: +# 1. Validates triggers (dockerfile_state ∈ {written-from-scratch, fixed-broken-upstream}) +# 2. Confirms upstream is public, not archived, no concurrent Dockerfile landed +# 3. Forks upstream to ViktorBarzin (idempotent) +# 4. Syncs fork with upstream default branch +# 5. Creates branch (add-dockerfile or fix-dockerfile), appends - on collision +# 6. Commits Dockerfile + .dockerignore + BUILD.md via Contents API +# 7. Opens PR against upstream with body rendered from PR_BODY.md +# 8. Writes contribution_pr_url back into state file +# +# Usage: +# contribute-dockerfile.sh +# +# Example: +# contribute-dockerfile.sh /home/wizard/code/infra/modules/kubernetes/myapp +# +# Requires: jq, curl, vault CLI (logged in). + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +TEMPLATES_DIR="$(cd "$SCRIPT_DIR/../templates" && pwd)" + +FORK_OWNER="ViktorBarzin" + +log() { echo "contribute-dockerfile: $*"; } +die() { echo "contribute-dockerfile: ERROR: $*" >&2; exit 1; } +skip() { echo "contribute-dockerfile: SKIP: $*"; exit 0; } + +if [ "$#" -ne 1 ]; then + die "usage: $0 " +fi + +MODULE_DIR="$1" +STATE_FILE="$MODULE_DIR/.contribution-state.json" + +[ -f "$STATE_FILE" ] || die "state file not found: $STATE_FILE" + +# --- Read + validate state --- +dockerfile_state=$(jq -r '.dockerfile_state // ""' "$STATE_FILE") +upstream_repo=$(jq -r '.upstream_repo // ""' "$STATE_FILE") +dockerfile_path=$(jq -r '.dockerfile_path_in_infra // ""' "$STATE_FILE") +deploy_verified_at=$(jq -r '.deploy_verified_at // ""' "$STATE_FILE") +existing_pr_url=$(jq -r '.contribution_pr_url // ""' "$STATE_FILE") + +if [ -n "$existing_pr_url" ] && [ "$existing_pr_url" != "null" ]; then + skip "PR already exists: $existing_pr_url" +fi + +case "$dockerfile_state" in + written-from-scratch) BRANCH_NAME="add-dockerfile"; reason_type="none" ;; + fixed-broken-upstream) BRANCH_NAME="fix-dockerfile"; reason_type="broken" ;; + *) skip "dockerfile_state='$dockerfile_state' — nothing to contribute" ;; +esac + +[ -z "$deploy_verified_at" ] || [ "$deploy_verified_at" = "null" ] && die "deploy not verified yet (deploy_verified_at empty); run stability-gate first" + +[ -z "$upstream_repo" ] && die "upstream_repo empty in state file" +[[ "$upstream_repo" == */* ]] || die "upstream_repo must be owner/name, got: $upstream_repo" + +UP_OWNER="${upstream_repo%/*}" +UP_NAME="${upstream_repo#*/}" + +abs_dockerfile="$MODULE_DIR/$(basename "$dockerfile_path")" +if [ ! -f "$MODULE_DIR/files/Dockerfile" ]; then + die "Dockerfile not found at $MODULE_DIR/files/Dockerfile" +fi +DOCKERFILE_SRC="$MODULE_DIR/files/Dockerfile" +DOCKERIGNORE_SRC="$MODULE_DIR/files/.dockerignore" +BUILDMD_SRC="$MODULE_DIR/files/BUILD.md" +for f in "$DOCKERIGNORE_SRC" "$BUILDMD_SRC"; do + [ -f "$f" ] || die "required file missing: $f" +done + +# --- GitHub auth --- +GITHUB_TOKEN="${GITHUB_TOKEN:-$(vault kv get -field=github_pat secret/viktor 2>/dev/null || true)}" +[ -n "$GITHUB_TOKEN" ] || die "GITHUB_TOKEN not set and vault lookup failed (vault login -method=oidc first)" + +gh_api() { + local method="$1"; local path="$2"; local data="${3:-}" + local url="https://api.github.com${path}" + local curl_args=(-sS -w "\n%{http_code}" -X "$method" + -H "Authorization: token $GITHUB_TOKEN" + -H "Accept: application/vnd.github+json" + -H "X-GitHub-Api-Version: 2022-11-28") + [ -n "$data" ] && curl_args+=(-d "$data") + curl "${curl_args[@]}" "$url" +} + +gh_api_retry() { + local method="$1"; local path="$2"; local data="${3:-}" + local attempt=1 + local max_attempts=3 + local out http + while [ "$attempt" -le "$max_attempts" ]; do + out=$(gh_api "$method" "$path" "$data") + http=$(printf '%s' "$out" | tail -n1) + body=$(printf '%s' "$out" | sed '$d') + if [ "$http" -ge 500 ] || [ "$http" = "000" ]; then + log "retry $attempt/$max_attempts on $method $path (http=$http)" + attempt=$((attempt + 1)) + sleep $((2 ** attempt)) + continue + fi + printf '%s\n%s' "$body" "$http" + return 0 + done + die "GitHub API 5xx after $max_attempts attempts on $method $path" +} + +# Helpers that parse the combined body+http form. +gh_http() { printf '%s' "$1" | tail -n1; } +gh_body() { printf '%s' "$1" | sed '$d'; } + +# --- Upstream sanity checks --- +log "checking upstream $upstream_repo" +resp=$(gh_api_retry GET "/repos/$UP_OWNER/$UP_NAME") +http=$(gh_http "$resp"); body=$(gh_body "$resp") +if [ "$http" = "404" ]; then skip "upstream repo not found (may be private or deleted): $upstream_repo"; fi +[ "$http" = "200" ] || die "GET upstream failed http=$http body=$body" + +archived=$(printf '%s' "$body" | jq -r '.archived') +default_branch=$(printf '%s' "$body" | jq -r '.default_branch') +[ "$archived" = "true" ] && skip "upstream is archived — not opening PR" +[ -n "$default_branch" ] || die "could not determine upstream default branch" +log "upstream default branch: $default_branch" + +# If we wrote the Dockerfile from scratch, make sure one didn't land upstream meanwhile. +if [ "$dockerfile_state" = "written-from-scratch" ]; then + resp=$(gh_api_retry GET "/repos/$UP_OWNER/$UP_NAME/contents/Dockerfile?ref=$default_branch") + http=$(gh_http "$resp") + if [ "$http" = "200" ]; then + skip "a Dockerfile landed upstream since we started — aborting to avoid clobbering" + fi +fi + +# Check for an existing open PR from our fork. +resp=$(gh_api_retry GET "/repos/$UP_OWNER/$UP_NAME/pulls?state=open&head=${FORK_OWNER}:${BRANCH_NAME}") +http=$(gh_http "$resp"); body=$(gh_body "$resp") +if [ "$http" = "200" ]; then + existing=$(printf '%s' "$body" | jq -r '.[0].html_url // ""') + if [ -n "$existing" ]; then + log "existing open PR found: $existing — recording and skipping" + jq --arg url "$existing" '.contribution_pr_url = $url' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" + exit 0 + fi +fi + +# --- Fork --- +log "ensuring fork exists at $FORK_OWNER/$UP_NAME" +resp=$(gh_api_retry POST "/repos/$UP_OWNER/$UP_NAME/forks" '{}') +http=$(gh_http "$resp") +if [ "$http" != "202" ] && [ "$http" != "200" ]; then + die "fork call failed http=$http" +fi + +# Wait for fork to be ready (GitHub can take up to ~30s). +for i in $(seq 1 15); do + resp=$(gh_api_retry GET "/repos/$FORK_OWNER/$UP_NAME") + if [ "$(gh_http "$resp")" = "200" ]; then break; fi + sleep 2 +done +[ "$(gh_http "$resp")" = "200" ] || die "fork $FORK_OWNER/$UP_NAME did not become ready" + +# --- Sync fork with upstream default branch --- +log "syncing fork with upstream/$default_branch" +resp=$(gh_api_retry POST "/repos/$FORK_OWNER/$UP_NAME/merge-upstream" "$(jq -n --arg b "$default_branch" '{branch:$b}')") +http=$(gh_http "$resp") +[ "$http" = "200" ] || [ "$http" = "409" ] || log "merge-upstream returned http=$http (continuing)" + +# --- Determine base SHA for new branch --- +resp=$(gh_api_retry GET "/repos/$FORK_OWNER/$UP_NAME/git/ref/heads/$default_branch") +http=$(gh_http "$resp"); body=$(gh_body "$resp") +[ "$http" = "200" ] || die "could not read default branch ref on fork (http=$http)" +base_sha=$(printf '%s' "$body" | jq -r '.object.sha') + +# --- Create branch (or append timestamp on collision) --- +attempt_branch="$BRANCH_NAME" +resp=$(gh_api_retry GET "/repos/$FORK_OWNER/$UP_NAME/git/ref/heads/$attempt_branch") +if [ "$(gh_http "$resp")" = "200" ]; then + attempt_branch="${BRANCH_NAME}-$(date +%s | tail -c 9)" + log "branch existed; using $attempt_branch" +fi + +log "creating branch $attempt_branch off $base_sha" +payload=$(jq -n --arg r "refs/heads/$attempt_branch" --arg s "$base_sha" '{ref:$r,sha:$s}') +resp=$(gh_api_retry POST "/repos/$FORK_OWNER/$UP_NAME/git/refs" "$payload") +[ "$(gh_http "$resp")" = "201" ] || die "could not create branch: $(gh_body "$resp")" + +# --- Helper to PUT a file via Contents API --- +put_file() { + local src="$1"; local dst="$2"; local message="$3" + local b64 payload exists_resp http existing_sha="" + b64=$(base64 -w0 < "$src") + + exists_resp=$(gh_api_retry GET "/repos/$FORK_OWNER/$UP_NAME/contents/$dst?ref=$attempt_branch") + if [ "$(gh_http "$exists_resp")" = "200" ]; then + existing_sha=$(gh_body "$exists_resp" | jq -r '.sha') + fi + + if [ -n "$existing_sha" ]; then + payload=$(jq -n --arg m "$message" --arg c "$b64" --arg b "$attempt_branch" --arg sha "$existing_sha" \ + '{message:$m, content:$c, branch:$b, sha:$sha}') + else + payload=$(jq -n --arg m "$message" --arg c "$b64" --arg b "$attempt_branch" \ + '{message:$m, content:$c, branch:$b}') + fi + + resp=$(gh_api_retry PUT "/repos/$FORK_OWNER/$UP_NAME/contents/$dst" "$payload") + http=$(gh_http "$resp") + [ "$http" = "200" ] || [ "$http" = "201" ] || die "PUT $dst failed http=$http body=$(gh_body "$resp")" +} + +commit_msg_prefix="Add Dockerfile" +[ "$dockerfile_state" = "fixed-broken-upstream" ] && commit_msg_prefix="Fix Dockerfile" + +log "committing Dockerfile, .dockerignore, BUILD.md" +put_file "$DOCKERFILE_SRC" "Dockerfile" "$commit_msg_prefix + +Signed-off-by: Viktor Barzin " +put_file "$DOCKERIGNORE_SRC" ".dockerignore" "Add .dockerignore + +Signed-off-by: Viktor Barzin " +put_file "$BUILDMD_SRC" "BUILD.md" "Add BUILD.md + +Signed-off-by: Viktor Barzin " + +# --- Render PR body --- +reason_paragraph="This project currently has no Dockerfile, making it harder for the self-hosting community to run this. I put together a working one while deploying this app to my home Kubernetes cluster and wanted to upstream it." +if [ "$reason_type" = "broken" ]; then + reason_paragraph="The existing Dockerfile in this repo does not build cleanly for \`linux/amd64\`. I tracked down the fixes while deploying this app to my home Kubernetes cluster and wanted to upstream them." +fi + +IMAGE_SIZE=$(jq -r '.image_size // "unknown"' "$STATE_FILE") +BASE_IMAGE=$(jq -r '.base_image // "unknown"' "$STATE_FILE") +IMAGE_TAG=$(jq -r '.image_tag // "myapp:latest"' "$STATE_FILE") +DOCKERFILE_SHAPE=$(jq -r '.dockerfile_shape // "multi-stage, non-root, linux/amd64"' "$STATE_FILE") + +pr_body=$(cat "$TEMPLATES_DIR/PR_BODY.md") +pr_body="${pr_body//\{\{REASON_PARAGRAPH\}\}/$reason_paragraph}" +pr_body="${pr_body//\{\{DOCKERFILE_SHAPE\}\}/$DOCKERFILE_SHAPE}" +pr_body="${pr_body//\{\{IMAGE_SIZE\}\}/$IMAGE_SIZE}" +pr_body="${pr_body//\{\{BASE_IMAGE\}\}/$BASE_IMAGE}" +pr_body="${pr_body//\{\{IMAGE_TAG\}\}/$IMAGE_TAG}" + +pr_title="$commit_msg_prefix" + +# --- Open PR --- +log "opening PR against $UP_OWNER/$UP_NAME:$default_branch" +payload=$(jq -n \ + --arg t "$pr_title" \ + --arg h "${FORK_OWNER}:${attempt_branch}" \ + --arg b "$default_branch" \ + --arg body "$pr_body" \ + '{title:$t, head:$h, base:$b, body:$body, maintainer_can_modify:true}') +resp=$(gh_api_retry POST "/repos/$UP_OWNER/$UP_NAME/pulls" "$payload") +http=$(gh_http "$resp"); body=$(gh_body "$resp") +if [ "$http" != "201" ]; then + die "PR creation failed http=$http body=$body" +fi + +pr_url=$(printf '%s' "$body" | jq -r '.html_url') +log "PR opened: $pr_url" + +# --- Record PR URL in state file --- +jq --arg url "$pr_url" '.contribution_pr_url = $url' "$STATE_FILE" > "$STATE_FILE.tmp" && mv "$STATE_FILE.tmp" "$STATE_FILE" +log "state file updated with PR URL" diff --git a/.claude/skills/setup-project/scripts/stability-gate.sh b/.claude/skills/setup-project/scripts/stability-gate.sh new file mode 100755 index 00000000..2d47d15e --- /dev/null +++ b/.claude/skills/setup-project/scripts/stability-gate.sh @@ -0,0 +1,71 @@ +#!/usr/bin/env bash +# 10-minute deploy stability gate for setup-project skill. +# Polls pod readiness + HTTP 200 on target URL every 30s for 20 iterations. +# Requires 18/20 probes to succeed (tolerates 2 blips for restarts/DNS propagation). +# +# Usage: +# stability-gate.sh +# +# Example: +# stability-gate.sh myapp myapp https://myapp.viktorbarzin.me +# +# Exit codes: +# 0 - Stable (>=18/20 probes OK) +# 1 - Unstable (<18/20 probes OK) +# 2 - Usage error + +set -u + +if [ "$#" -ne 3 ]; then + echo "Usage: $0 " >&2 + exit 2 +fi + +NS="$1" +APP="$2" +URL="$3" + +TOTAL_PROBES=20 +MIN_SUCCESSES=18 +INTERVAL_SECONDS=30 + +ok_count=0 +fail_count=0 + +echo "stability-gate: ns=$NS app=$APP url=$URL" +echo "stability-gate: $TOTAL_PROBES probes x ${INTERVAL_SECONDS}s (need $MIN_SUCCESSES/$TOTAL_PROBES)" + +for i in $(seq 1 "$TOTAL_PROBES"); do + probe_ok=true + + if ! kubectl wait --for=condition=Ready pod -l "app=$APP" -n "$NS" --timeout=25s >/dev/null 2>&1; then + probe_ok=false + fi + + status=$(curl -sS -o /dev/null -w "%{http_code}" --max-time 10 "$URL" || echo "000") + if [ "$status" != "200" ]; then + probe_ok=false + fi + + if [ "$probe_ok" = "true" ]; then + ok_count=$((ok_count + 1)) + printf " probe %2d/%d: OK (http=%s)\n" "$i" "$TOTAL_PROBES" "$status" + else + fail_count=$((fail_count + 1)) + printf " probe %2d/%d: FAIL (http=%s)\n" "$i" "$TOTAL_PROBES" "$status" + fi + + if [ "$i" -lt "$TOTAL_PROBES" ]; then + sleep "$INTERVAL_SECONDS" + fi +done + +echo "stability-gate: results ok=$ok_count fail=$fail_count" + +if [ "$ok_count" -ge "$MIN_SUCCESSES" ]; then + echo "stability-gate: PASS" + exit 0 +fi + +echo "stability-gate: FAIL (need $MIN_SUCCESSES, got $ok_count)" >&2 +exit 1 diff --git a/.claude/skills/setup-project/templates/Dockerfile.README.md b/.claude/skills/setup-project/templates/Dockerfile.README.md new file mode 100644 index 00000000..9cf2168b --- /dev/null +++ b/.claude/skills/setup-project/templates/Dockerfile.README.md @@ -0,0 +1,24 @@ +# Build notes + +## Build + +``` +docker build --platform linux/amd64 -t {{IMAGE_NAME}}:{{TAG}} . +``` + +## Run + +``` +docker run --rm -p {{CONTAINER_PORT}}:{{CONTAINER_PORT}} {{IMAGE_NAME}}:{{TAG}} +``` + +## Configuration + +{{ENV_VARS_TABLE}} + +## Notes + +- Built for `linux/amd64`; multi-arch not tested. +- Image size: `{{IMAGE_SIZE}}`, base: `{{BASE_IMAGE}}`. +- Runs as a non-root user. +{{EXTRA_NOTES}} diff --git a/.claude/skills/setup-project/templates/PR_BODY.md b/.claude/skills/setup-project/templates/PR_BODY.md new file mode 100644 index 00000000..5d07c3cb --- /dev/null +++ b/.claude/skills/setup-project/templates/PR_BODY.md @@ -0,0 +1,25 @@ +## Add a working Dockerfile + +### Why +{{REASON_PARAGRAPH}} + +### What this adds +- `Dockerfile` — {{DOCKERFILE_SHAPE}} +- `.dockerignore` +- `BUILD.md` with the build command and notes + +### Tested +- Built and pushed to a private registry, deployed to a Kubernetes cluster. +- Pod has been Ready and serving HTTP 200 at the ingress for 10+ minutes of continuous probing before this PR was opened. +- Image size: {{IMAGE_SIZE}}, base: {{BASE_IMAGE}} +- Platform tested: `linux/amd64` + +### Build command +``` +docker build --platform linux/amd64 -t {{IMAGE_TAG}} . +``` + +Happy to iterate on base image, build args, or multi-arch support if you'd prefer a different shape. Thanks for the project! + +--- +Contributed after self-hosting this project. Filed by the repo owner's deployment workflow; feel free to mention me (@ViktorBarzin) with any follow-ups.