diff --git a/AGENTS.md b/AGENTS.md index c5af92a..82d8c2a 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,3 +6,4 @@ - After any meaningful change set: **commit and push** to `main` with a clear, complete-sentence message (what changed and why). - Never commit secrets: `.env`, `wireguard/wg0.conf`, `config/`, GGUF models, `ansible/.secrets/`. - Prefer SSH push (`GIT_SSH_COMMAND` with `~/.ssh/id_ed25519` if needed). +- Server checkout: `/opt/stack` is the same repo (`git pull` on ser5). Ansible uses `stack_deploy_method: git`. diff --git a/ansible/group_vars/all.yml b/ansible/group_vars/all.yml index e18c735..6f1b4e8 100644 --- a/ansible/group_vars/all.yml +++ b/ansible/group_vars/all.yml @@ -195,9 +195,17 @@ forgejo_root_url: "http://{{ forgejo_domain }}:{{ forgejo_http_port }}/" # --------------------------------------------------------------------------- # Stack deployment # --------------------------------------------------------------------------- -# Sync this repo (parent of ansible/) to stack_root on the server. -# Set false if you manage /opt/stack yourself (git clone, etc.). -deploy_stack_files: true +# Deploy stack *code* to the server. +# git — /opt/stack is a clone of Forgejo (preferred; use `git pull` on ser5) +# rsync — push from the control machine (laptop) tree +stack_deploy_method: git +deploy_stack_files: true # master switch for code deploy tasks + +# Forgejo remote used on the server (same host → prefer loopback SSH port) +stack_git_repo: "ssh://git@127.0.0.1:2222/tim/stack.git" +stack_git_branch: main +# Also accept the LAN URL if someone clones from another machine +stack_git_repo_lan: "ssh://git@192.168.8.123:2222/tim/stack.git" # Paths on the control machine (the machine running ansible-playbook) # Default: the arr-stack repo that contains this ansible/ directory diff --git a/ansible/roles/stack/tasks/main.yml b/ansible/roles/stack/tasks/main.yml index bd02072..21b9e5e 100644 --- a/ansible/roles/stack/tasks/main.yml +++ b/ansible/roles/stack/tasks/main.yml @@ -47,14 +47,142 @@ loop: "{{ media_subdirs }}" tags: [stack, storage] -- name: Deploy stack files from control machine +# --------------------------------------------------------------------------- +# Code deploy: git (preferred) or rsync +# Local-only paths stay untracked: config/, .env, models/*.gguf, wireguard/wg0.conf +# --------------------------------------------------------------------------- + +- name: Ensure git is installed on stack host + ansible.builtin.apt: + name: git + state: present + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Ensure stack owner has an SSH key for Forgejo git + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.user: + name: "{{ stack_owner }}" + generate_ssh_key: true + ssh_key_type: ed25519 + ssh_key_file: "/home/{{ stack_owner }}/.ssh/id_ed25519" + ssh_key_comment: "{{ stack_owner }}@{{ inventory_hostname }}-forgejo" + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Read stack owner public key + ansible.builtin.slurp: + src: "/home/{{ stack_owner }}/.ssh/id_ed25519.pub" + register: stack_owner_pubkey + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Ensure Forgejo knows the stack host deploy key (idempotent via API best-effort) + ansible.builtin.shell: | + set -e + PUB='{{ stack_owner_pubkey.content | b64decode | trim }}' + # Create a short-lived admin token, add key if fingerprint not present, drop token + TOKEN=$(docker exec -u 1000 forgejo gitea admin user generate-access-token \ + --username "{{ stack_owner }}" \ + --token-name "ansible-deploy-$(date +%s)" \ + --scopes all 2>/dev/null | awk '/Access token was successfully created:/{print $NF}') + if [ -z "$TOKEN" ]; then + echo "Could not create Forgejo token (is forgejo up?). Continuing — key may already exist." + exit 0 + fi + # List keys; add if title missing + if curl -fsS -H "Authorization: token $TOKEN" http://127.0.0.1:3002/api/v1/user/keys \ + | grep -q "ansible-ser5\|{{ inventory_hostname }}-forgejo"; then + echo "SSH key already registered" + else + curl -fsS -X POST http://127.0.0.1:3002/api/v1/user/keys \ + -H "Authorization: token $TOKEN" \ + -H "Content-Type: application/json" \ + -d "{\"title\":\"ansible-{{ inventory_hostname }}\",\"key\":\"$PUB\"}" || true + fi + args: + executable: /bin/bash + when: + - deploy_stack_files | bool + - stack_deploy_method == "git" + changed_when: false + failed_when: false + tags: [stack, deploy, git] + +- name: Initialize git repo in stack root if missing + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.command: git init -b {{ stack_git_branch }} + args: + chdir: "{{ stack_root }}" + creates: "{{ stack_root }}/.git" + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Configure origin remote + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.shell: | + set -e + cd "{{ stack_root }}" + if git remote get-url origin >/dev/null 2>&1; then + git remote set-url origin "{{ stack_git_repo }}" + else + git remote add origin "{{ stack_git_repo }}" + fi + args: + executable: /bin/bash + when: deploy_stack_files | bool and stack_deploy_method == "git" + changed_when: true + tags: [stack, deploy, git] + +- name: Mark stack root as safe.directory for git + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.command: git config --global --add safe.directory {{ stack_root }} + changed_when: false + failed_when: false + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Fetch and hard-reset tracked files to origin (keeps untracked secrets) + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.shell: | + set -euo pipefail + cd "{{ stack_root }}" + export GIT_SSH_COMMAND="ssh -i /home/{{ stack_owner }}/.ssh/id_ed25519 -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" + git fetch origin "{{ stack_git_branch }}" + # Preserve local-only paths; only reset tracked files + git checkout -B "{{ stack_git_branch }}" "origin/{{ stack_git_branch }}" + args: + executable: /bin/bash + when: deploy_stack_files | bool and stack_deploy_method == "git" + register: stack_git_pull + tags: [stack, deploy, git] + +- name: Show git HEAD after pull + become: true + become_user: "{{ stack_owner }}" + ansible.builtin.command: git -C {{ stack_root }} log -1 --oneline + register: stack_git_head + changed_when: false + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Report stack git revision + ansible.builtin.debug: + msg: "stack @ {{ stack_git_head.stdout | default('n/a') }}" + when: deploy_stack_files | bool and stack_deploy_method == "git" + tags: [stack, deploy, git] + +- name: Deploy stack files from control machine (rsync fallback) ansible.posix.synchronize: src: "{{ stack_src }}/" dest: "{{ stack_root }}/" delete: false recursive: true - # Non-root rsync cannot chown/chgrp files Docker created as root - # (e.g. searxng). Content sync only; ownership fixed in next task. archive: true owner: false group: false @@ -72,11 +200,11 @@ - "--no-owner" - "--no-group" - "--chmod=Du=rwx,Dgo=rx,Fu=rw,Fgo=r" - when: deploy_stack_files | bool + when: deploy_stack_files | bool and stack_deploy_method == "rsync" become: false - tags: [stack, deploy] + tags: [stack, deploy, rsync] -- name: Fix ownership after rsync +- name: Fix ownership of stack tree ansible.builtin.file: path: "{{ stack_root }}" state: directory @@ -84,7 +212,6 @@ group: "{{ stack_group }}" recurse: true when: deploy_stack_files | bool - # become: true from play — can reset root-owned files from containers tags: [stack, deploy] - name: Deploy production .env for this host diff --git a/quickstart.md b/quickstart.md index ec5cc07..a3dcd71 100644 --- a/quickstart.md +++ b/quickstart.md @@ -18,6 +18,19 @@ ansible-playbook site.yml -K # -K only if sudo still asks once; playboo What it does: OS updates, tools (incl. Microsoft `edit`), Docker, AMD/Vulkan, UFW (LAN only), `/opt/stack` + `/storage`, `.env`, `arr-stack.service`, and **tim** as local admin (passwordless sudo, docker/video/render/…, owns stack + storage, permissive GPU nodes). +**Code on the server:** `/opt/stack` is a **git clone** of Forgejo `tim/stack` (`stack_deploy_method: git`). +Day-to-day updates on ser5: + +```bash +ssh tim@192.168.8.123 +cd /opt/stack +git pull +docker compose up -d # if compose/images changed +``` + +Ansible can also refresh code: `ansible-playbook site.yml -K --tags stack,deploy` (runs `git fetch` + checkout). +Local-only (not in git): `.env`, `config/`, `models/**/*.gguf`, `wireguard/wg0.conf`. + **Before first full stack start** (on ser5): ```bash