diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..93a022d --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,48 @@ +name: Deploy FileDrop + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Build + run: bun run build + + - name: Deploy to VPS + shell: bash + env: + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + VPS_SSH_KEY_VALUE: ${{ secrets.VPS_SSH_KEY }} + PRODUCTION_ENV: ${{ secrets.PRODUCTION_ENV }} + ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} + run: | + set -eu + + key_file="${RUNNER_TEMP:-/tmp}/filedrop_deploy_key" + printf '%s\n' "$VPS_SSH_KEY_VALUE" > "$key_file" + chmod 600 "$key_file" + + printf '%s\n' "$PRODUCTION_ENV" > .env + chmod 600 .env + + VPS_SSH_KEY="$key_file" ./deploy.sh --no-build diff --git a/deploy.sh b/deploy.sh index 6538e6f..a3b96f7 100755 --- a/deploy.sh +++ b/deploy.sh @@ -6,9 +6,9 @@ # then rebuild the Docker image and restart the container on the VPS. # # Prerequisites: -# - GitLab CLI (glab) with active session, OR the env vars below # - SSH access to the VPS # - Docker + docker compose on the VPS +# - For CI: Gitea Actions secrets injected as environment variables # # Usage: # ./deploy.sh # build + deploy @@ -16,10 +16,15 @@ # ./deploy.sh --help # show this message # ./deploy.sh --check # dry-run: show vars and exit # -# Required env (or auto-fetched from GitLab CI vars via glab): +# Required env in CI: # VPS_HOST — VPS IP/hostname -# VPS_USER — SSH user (default: root) -# VPS_SSH_KEY — path/contents of SSH private key +# VPS_USER — SSH user +# VPS_SSH_KEY — path to SSH private key file +# +# Local defaults: +# VPS_HOST — 45.127.35.244 +# VPS_USER — root +# VPS_SSH_KEY — ~/.ssh/id_ed25519 # # Optional: # DEPLOY_DIR — deploy dir on VPS (default: /opt/filedrop) @@ -30,7 +35,6 @@ set -eu # ── Config ──────────────────────────────────────────────────────────────────── APP_NAME="filedrop" -GITLAB_PROJECT="superaseph%2FTeleUploader" DEPLOY_DIR="${DEPLOY_DIR:-/opt/${APP_NAME}}" COMPOSE_FILE="docker-compose.yml" DOCKER_IMAGE="ghcr.io/mytheclipse/${APP_NAME}" @@ -50,30 +54,12 @@ for arg in "$@"; do esac done -# ── Auto-fetch credentials from GitLab CI vars ───────────────────────────── -fetch_ci_var() { - glab api "projects/${GITLAB_PROJECT}/variables/$1" 2>/dev/null \ - | python3 -c "import json,sys; print(json.load(sys.stdin).get('value',''))" 2>/dev/null || true -} - # ── Default credentials ───────────────────────────────────────────────────── -# Hardcoded defaults for this project. Env vars take precedence. +# Local defaults for this project. CI-provided environment variables take precedence. : "${VPS_HOST:=45.127.35.244}" : "${VPS_USER:=root}" : "${VPS_SSH_KEY:=${HOME}/.ssh/id_ed25519}" -# Fallback: fetch from GitLab CI vars if defaults are empty (for CI runs) -if [ -z "${VPS_HOST:-}" ]; then VPS_HOST=$(fetch_ci_var VPS_HOST); fi -if [ -z "${VPS_USER:-}" ]; then VPS_USER=$(fetch_ci_var VPS_USERNAME); fi -if [ -z "${VPS_SSH_KEY:-}" ]; then - KEY=$(fetch_ci_var VPS_SSH_KEY) - if [ -n "$KEY" ]; then - VPS_SSH_KEY=$(mktemp) - echo "$KEY" > "$VPS_SSH_KEY" - chmod 600 "$VPS_SSH_KEY" - fi -fi - # ── Check mode ──────────────────────────────────────────────────────────────── if $DO_CHECK; then echo "=== Config ===" @@ -93,6 +79,11 @@ if $DO_CHECK; then exit 0 fi +# ── Helpers ─────────────────────────────────────────────────────────────────── +log() { echo "→ $*"; } +ok() { echo "✓ $*"; } +die() { echo "✗ $*"; exit 1; } + # ── Validate ────────────────────────────────────────────────────────────────── # (Defaults are set above — this fails only if something went wrong) : "${VPS_HOST:?VPS_HOST resolved to empty}" @@ -103,11 +94,7 @@ fi SSH_DEST="${VPS_USER}@${VPS_HOST}" SSH_OPTS="-i $VPS_SSH_KEY -o StrictHostKeyChecking=accept-new" -# ── Helpers ─────────────────────────────────────────────────────────────────── vps() { ssh $SSH_OPTS "$SSH_DEST" "$@"; } -log() { echo "→ $*"; } -ok() { echo "✓ $*"; } -die() { echo "✗ $*"; exit 1; } # ── 1. Test SSH connection ──────────────────────────────────────────────────── log "Testing SSH connection to ${VPS_USER}@${VPS_HOST}..." diff --git a/docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md b/docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md new file mode 100644 index 0000000..b1285ee --- /dev/null +++ b/docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md @@ -0,0 +1,709 @@ +# Gitea CI/CD Migration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Move `origin` from GitLab to `git.imrnes.team:MythEclipse/TeleUploader` and add Gitea Actions deployment on push to `main` using the existing VPS deploy path. + +**Architecture:** Keep `deploy.sh` as the single deployment entrypoint and make it provider-neutral. Add a thin `.gitea/workflows/deploy.yml` wrapper that performs the CI gate (`bun install`, `bun run lint`, `bun run build`), reconstructs runtime secrets into files, then calls `./deploy.sh --no-build`. + +**Tech Stack:** Bun, Bun test, Biome, Bash, Docker Compose on VPS, Gitea Actions, `tea` CLI, SSH. + +## Global Constraints + +- Default to Bun commands: use `bun install`, `bun run lint`, `bun run build`, and `bun test`. +- Do not use Node.js, npm, yarn, pnpm, vite, webpack, or Express for this work. +- Deployment target repository is `MythEclipse/TeleUploader` on `git.imrnes.team`. +- Target SSH remote is `git@git.imrnes.team:MythEclipse/TeleUploader.git`. +- Gitea Actions deployment trigger is push to `main`. +- Automatic deploy gate is lint + build only; do not add full test suite execution to the deploy workflow. +- Preserve the existing VPS deployment mechanism: SSH tar pipe, remote Docker Compose build, and container restart. +- Do not commit unless the user explicitly authorizes commits during execution. +- Do not push unless the user explicitly authorizes pushing during execution. +- Never commit `.env`; reconstruct it in Gitea Actions from a repository secret named `PRODUCTION_ENV`. + +--- + +## File Structure + +- Modify: `deploy.sh` + - Responsibility: local and CI deployment entrypoint. It must no longer know about GitLab or `glab`; it should read deployment inputs from environment variables or local defaults. +- Create: `.gitea/workflows/deploy.yml` + - Responsibility: Gitea Actions CI/CD wrapper. It runs lint/build, writes CI secrets into temporary files, and calls `deploy.sh --no-build`. +- Create/modify: `test/deploy-config.test.ts` + - Responsibility: fast static and dry-run coverage for deployment configuration. It prevents regressions to GitLab-specific deploy logic and verifies the workflow invokes the expected deploy path. +- External state: Gitea repository `MythEclipse/TeleUploader` + - Responsibility: new git origin and Gitea Actions secret storage. Manage with `tea` only after code/config changes are reviewed. + +--- + +### Task 1: Make `deploy.sh` Provider-Neutral + +**Files:** +- Create: `test/deploy-config.test.ts` +- Modify: `deploy.sh` + +**Interfaces:** +- Consumes: existing `deploy.sh --check`, `deploy.sh --no-build`, and Bun scripts from `package.json`. +- Produces: provider-neutral `deploy.sh` that accepts these environment variables: + - `VPS_HOST: string` + - `VPS_USER: string` + - `VPS_SSH_KEY: string` path to an SSH private key file + - optional `DEPLOY_DIR: string` + - optional `ADMIN_PASSWORD: string` + +- [ ] **Step 1: Write the failing deploy configuration tests** + +Create `test/deploy-config.test.ts` with this exact content: + +```ts +import { expect, test } from "bun:test"; + +const repoRoot = new URL("../", import.meta.url); +const deployScript = Bun.file(new URL("../deploy.sh", import.meta.url)); + +test("deploy script is provider-neutral", async () => { + const text = await deployScript.text(); + + expect(text).not.toContain("GITLAB_PROJECT"); + expect(text).not.toContain("fetch_ci_var"); + expect(text).not.toContain("glab"); + expect(text).not.toContain("GitLab CI"); + expect(text).toContain("Gitea Actions secrets"); +}); + +test("deploy check mode does not require an SSH key file", async () => { + const proc = Bun.spawn(["bash", "deploy.sh", "--check"], { + cwd: repoRoot.pathname, + env: { + ...Bun.env, + VPS_HOST: "203.0.113.10", + VPS_USER: "deploy", + VPS_SSH_KEY: "/tmp/nonexistent-teleuploader-key", + }, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stdout).toContain("App name:"); + expect(stdout).toContain("VPS_HOST:"); + expect(stdout).toContain("VPS_USER:"); + expect(stdout).toContain("VPS_SSH_KEY:"); + expect(stderr).toBe(""); +}); +``` + +- [ ] **Step 2: Run the new test and verify the expected failure** + +Run: + +```bash +bun test test/deploy-config.test.ts +``` + +Expected result: FAIL in `deploy script is provider-neutral` because the current `deploy.sh` still contains at least `GITLAB_PROJECT`, `fetch_ci_var`, `glab`, or `GitLab CI`. + +- [ ] **Step 3: Update the `deploy.sh` header comments** + +In `deploy.sh`, replace the prerequisite and environment comment block near the top with this exact text: + +```bash +# Prerequisites: +# - SSH access to the VPS +# - Docker + docker compose on the VPS +# - For CI: Gitea Actions secrets injected as environment variables +# +# Usage: +# ./deploy.sh # build + deploy +# ./deploy.sh --no-build # skip build, just deploy dist +# ./deploy.sh --help # show this message +# ./deploy.sh --check # dry-run: show vars and exit +# +# Required env in CI: +# VPS_HOST — VPS IP/hostname +# VPS_USER — SSH user +# VPS_SSH_KEY — path to SSH private key file +# +# Local defaults: +# VPS_HOST — 45.127.35.244 +# VPS_USER — root +# VPS_SSH_KEY — ~/.ssh/id_ed25519 +# +# Optional: +# DEPLOY_DIR — deploy dir on VPS (default: /opt/filedrop) +# ADMIN_PASSWORD — verify health after deploy (optional) +``` + +- [ ] **Step 4: Remove GitLab-specific configuration** + +In `deploy.sh`, replace this config block: + +```bash +APP_NAME="filedrop" +GITLAB_PROJECT="superaseph%2FTeleUploader" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/${APP_NAME}}" +COMPOSE_FILE="docker-compose.yml" +DOCKER_IMAGE="ghcr.io/mytheclipse/${APP_NAME}" +``` + +with this exact block: + +```bash +APP_NAME="filedrop" +DEPLOY_DIR="${DEPLOY_DIR:-/opt/${APP_NAME}}" +COMPOSE_FILE="docker-compose.yml" +DOCKER_IMAGE="ghcr.io/mytheclipse/${APP_NAME}" +``` + +- [ ] **Step 5: Remove GitLab CI variable fetching** + +Delete this entire block from `deploy.sh`: + +```bash +# ── Auto-fetch credentials from GitLab CI vars ───────────────────────────── +fetch_ci_var() { + glab api "projects/${GITLAB_PROJECT}/variables/$1" 2>/dev/null \ + | python3 -c "import json,sys; print(json.load(sys.stdin).get('value',''))" 2>/dev/null || true +} +``` + +Then replace the default credential block with this exact text: + +```bash +# ── Default credentials ───────────────────────────────────────────────────── +# Local defaults for this project. CI-provided environment variables take precedence. +: "${VPS_HOST:=45.127.35.244}" +: "${VPS_USER:=root}" +: "${VPS_SSH_KEY:=${HOME}/.ssh/id_ed25519}" +``` + +Delete this entire fallback block: + +```bash +# Fallback: fetch from GitLab CI vars if defaults are empty (for CI runs) +if [ -z "${VPS_HOST:-}" ]; then VPS_HOST=$(fetch_ci_var VPS_HOST); fi +if [ -z "${VPS_USER:-}" ]; then VPS_USER=$(fetch_ci_var VPS_USERNAME); fi +if [ -z "${VPS_SSH_KEY:-}" ]; then + KEY=$(fetch_ci_var VPS_SSH_KEY) + if [ -n "$KEY" ]; then + VPS_SSH_KEY=$(mktemp) + echo "$KEY" > "$VPS_SSH_KEY" + chmod 600 "$VPS_SSH_KEY" + fi +fi +``` + +- [ ] **Step 6: Move helper functions before validation** + +In `deploy.sh`, ensure this helper block appears before the validation block that calls `die`: + +```bash +# ── Helpers ─────────────────────────────────────────────────────────────────── +log() { echo "→ $*"; } +ok() { echo "✓ $*"; } +die() { echo "✗ $*"; exit 1; } +``` + +After validation, keep these lines together: + +```bash +SSH_DEST="${VPS_USER}@${VPS_HOST}" +SSH_OPTS="-i $VPS_SSH_KEY -o StrictHostKeyChecking=accept-new" + +vps() { ssh $SSH_OPTS "$SSH_DEST" "$@"; } +``` + +The validation block must remain: + +```bash +# ── Validate ────────────────────────────────────────────────────────────────── +# (Defaults are set above — this fails only if something went wrong) +: "${VPS_HOST:?VPS_HOST resolved to empty}" +: "${VPS_USER:?VPS_USER resolved to empty}" +: "${VPS_SSH_KEY:?VPS_SSH_KEY resolved to empty}" +[ -f "$VPS_SSH_KEY" ] || die "SSH key not found at $VPS_SSH_KEY" +``` + +- [ ] **Step 7: Run the focused deploy config test** + +Run: + +```bash +bun test test/deploy-config.test.ts +``` + +Expected result: PASS, 2 tests passing. + +- [ ] **Step 8: Run deploy check mode manually** + +Run: + +```bash +./deploy.sh --check +``` + +Expected result: exits 0 and prints `=== Config ===`, `=== Credentials ===`, and `=== Files to deploy ===`. It may mark files as missing only if local build artifacts are not present yet. + +- [ ] **Step 9: Review diff** + +Run: + +```bash +git diff -- deploy.sh test/deploy-config.test.ts +``` + +Expected result: diff only removes GitLab/glab awareness from `deploy.sh` and adds the new deploy config tests. + +- [ ] **Step 10: Commit only if authorized** + +Ask the user: `May I commit Task 1 changes?` + +If the user says yes, run: + +```bash +git add deploy.sh test/deploy-config.test.ts +git commit -m "chore: make deploy script provider-neutral" +``` + +If the user says no, do not commit; leave the changes in the working tree and continue only if the user wants inline uncommitted execution. + +--- + +### Task 2: Add Gitea Actions Deploy Workflow + +**Files:** +- Modify: `test/deploy-config.test.ts` +- Create: `.gitea/workflows/deploy.yml` + +**Interfaces:** +- Consumes: provider-neutral `deploy.sh --no-build` from Task 1. +- Produces: workflow `.gitea/workflows/deploy.yml` that requires these repository secrets: + - `VPS_HOST` + - `VPS_USER` + - `VPS_SSH_KEY` + - `PRODUCTION_ENV` + - optional `ADMIN_PASSWORD` + +- [ ] **Step 1: Extend deploy config tests for the Gitea workflow** + +Append this exact code to `test/deploy-config.test.ts`: + +```ts +const workflowFile = Bun.file(new URL("../.gitea/workflows/deploy.yml", import.meta.url)); + +test("gitea workflow deploys pushes to main through deploy script", async () => { + const text = await workflowFile.text(); + + expect(text).toContain("name: Deploy FileDrop"); + expect(text).toContain("branches:\n - main"); + expect(text).toContain("uses: actions/checkout@v4"); + expect(text).toContain("uses: oven-sh/setup-bun@v2"); + expect(text).toContain("bun install --frozen-lockfile"); + expect(text).toContain("bun run lint"); + expect(text).toContain("bun run build"); + expect(text).toContain("secrets.VPS_HOST"); + expect(text).toContain("secrets.VPS_USER"); + expect(text).toContain("secrets.VPS_SSH_KEY"); + expect(text).toContain("secrets.PRODUCTION_ENV"); + expect(text).toContain("./deploy.sh --no-build"); +}); +``` + +- [ ] **Step 2: Run the test and verify the expected failure** + +Run: + +```bash +bun test test/deploy-config.test.ts +``` + +Expected result: FAIL because `.gitea/workflows/deploy.yml` does not exist yet. + +- [ ] **Step 3: Create the Gitea Actions workflow** + +Create `.gitea/workflows/deploy.yml` with this exact content: + +```yaml +name: Deploy FileDrop + +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v4 + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Lint + run: bun run lint + + - name: Build + run: bun run build + + - name: Deploy to VPS + shell: bash + env: + VPS_HOST: ${{ secrets.VPS_HOST }} + VPS_USER: ${{ secrets.VPS_USER }} + VPS_SSH_KEY_VALUE: ${{ secrets.VPS_SSH_KEY }} + PRODUCTION_ENV: ${{ secrets.PRODUCTION_ENV }} + ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }} + run: | + set -eu + + key_file="${RUNNER_TEMP:-/tmp}/filedrop_deploy_key" + printf '%s\n' "$VPS_SSH_KEY_VALUE" > "$key_file" + chmod 600 "$key_file" + + printf '%s\n' "$PRODUCTION_ENV" > .env + chmod 600 .env + + VPS_SSH_KEY="$key_file" ./deploy.sh --no-build +``` + +- [ ] **Step 4: Run the focused deploy config test** + +Run: + +```bash +bun test test/deploy-config.test.ts +``` + +Expected result: PASS, 3 tests passing. + +- [ ] **Step 5: Run project lint** + +Run: + +```bash +bun run lint +``` + +Expected result: PASS with Biome reporting no errors. + +- [ ] **Step 6: Run project build** + +Run: + +```bash +bun run build +``` + +Expected result: PASS and creates/updates `dist/index.js` and `dist/migrate.js`. + +- [ ] **Step 7: Review diff** + +Run: + +```bash +git diff -- .gitea/workflows/deploy.yml test/deploy-config.test.ts +``` + +Expected result: diff adds the workflow and the workflow regression test only. + +- [ ] **Step 8: Commit only if authorized** + +Ask the user: `May I commit Task 2 changes?` + +If the user says yes, run: + +```bash +git add .gitea/workflows/deploy.yml test/deploy-config.test.ts +git commit -m "ci: deploy from gitea actions" +``` + +If the user says no, do not commit; keep the changes in the working tree. + +--- + +### Task 3: Create/Verify Gitea Repository and Configure Secrets + +**Files:** +- No repository file changes expected. + +**Interfaces:** +- Consumes: `tea` CLI login for `git.imrnes.team`, local `.env`, local SSH key, and Gitea repository `MythEclipse/TeleUploader`. +- Produces: Gitea repository with deploy secrets ready for Actions: + - `VPS_HOST` + - `VPS_USER` + - `VPS_SSH_KEY` + - `PRODUCTION_ENV` + - optional `ADMIN_PASSWORD` + +- [ ] **Step 1: Verify `tea` identity** + +Run: + +```bash +tea whoami +``` + +Expected result: output shows the `MythEclipse` account on `git.imrnes.team`. + +If the command says no login is available or uses the wrong instance, stop and ask the user to run: + +```bash +! tea login add +``` + +- [ ] **Step 2: Check whether the target repo already exists** + +Run: + +```bash +tea repos MythEclipse/TeleUploader +``` + +Expected result if the repo exists: repository details for `MythEclipse/TeleUploader`. + +Expected result if the repo does not exist: a not-found error. Continue to Step 3 only in that case. + +- [ ] **Step 3: Create the Gitea repository when missing** + +If Step 2 reported that the repo does not exist, run: + +```bash +tea repos create --owner MythEclipse --name TeleUploader --private --description "FileDrop Telegram-backed S3-compatible uploader" +``` + +Expected result: repository `MythEclipse/TeleUploader` is created. + +Then verify it: + +```bash +tea repos MythEclipse/TeleUploader +``` + +Expected result: repository details for `MythEclipse/TeleUploader`. + +- [ ] **Step 4: Verify local production `.env` exists and is untracked** + +Run: + +```bash +test -f .env && echo ".env exists" +git ls-files -- .env +``` + +Expected result: first command prints `.env exists`; second command prints nothing. + +If `.env` is missing, stop and ask the user to provide the production environment file before configuring `PRODUCTION_ENV`. + +If `git ls-files -- .env` prints `.env`, stop because secrets would be tracked; do not continue until the user decides how to remove it safely. + +- [ ] **Step 5: Configure required Gitea Actions secrets** + +Run these commands: + +```bash +tea actions secrets create --repo MythEclipse/TeleUploader VPS_HOST "45.127.35.244" +tea actions secrets create --repo MythEclipse/TeleUploader VPS_USER "root" +tea actions secrets create --repo MythEclipse/TeleUploader --file "${HOME}/.ssh/id_ed25519" VPS_SSH_KEY +tea actions secrets create --repo MythEclipse/TeleUploader --file .env PRODUCTION_ENV +``` + +Expected result: each command succeeds. + +If `${HOME}/.ssh/id_ed25519` is not the desired deploy key, stop and ask the user for the correct key path before running the `VPS_SSH_KEY` command. + +- [ ] **Step 6: Configure optional admin health-check secret when available** + +If the local `.env` contains `ADMIN_PASSWORD=`, extract and configure it with: + +```bash +admin_password=$(grep '^ADMIN_PASSWORD=' .env | sed 's/^ADMIN_PASSWORD=//') +if [ -n "$admin_password" ]; then + tea actions secrets create --repo MythEclipse/TeleUploader ADMIN_PASSWORD "$admin_password" +fi +``` + +Expected result: if `ADMIN_PASSWORD` is present and non-empty, the secret is created. If it is absent or empty, no command is run and the workflow still works because `deploy.sh` treats `ADMIN_PASSWORD` as optional. + +- [ ] **Step 7: Verify secret names** + +Run: + +```bash +tea actions secrets list --repo MythEclipse/TeleUploader +``` + +Expected result: list includes `VPS_HOST`, `VPS_USER`, `VPS_SSH_KEY`, and `PRODUCTION_ENV`. It may also include `ADMIN_PASSWORD`. + +--- + +### Task 4: Switch Remote, Verify Locally, and Push After Approval + +**Files:** +- External git remote configuration only. +- No additional repository file changes expected. + +**Interfaces:** +- Consumes: completed Task 1 and Task 2 changes, configured Gitea repository from Task 3. +- Produces: local `origin` set to `git@git.imrnes.team:MythEclipse/TeleUploader.git`, and after explicit approval, `main` pushed to Gitea. + +- [ ] **Step 1: Show current status before mutating the remote** + +Run: + +```bash +git status --short +git remote -v +``` + +Expected result before remote switch: `origin` still points to `git@gitlab.com:superaseph/TeleUploader.git` unless a prior task already changed it. + +- [ ] **Step 2: Set `origin` to Gitea** + +Run: + +```bash +git remote set-url origin git@git.imrnes.team:MythEclipse/TeleUploader.git +``` + +- [ ] **Step 3: Verify remote URL** + +Run: + +```bash +git remote -v +``` + +Expected result: + +```text +origin git@git.imrnes.team:MythEclipse/TeleUploader.git (fetch) +origin git@git.imrnes.team:MythEclipse/TeleUploader.git (push) +``` + +- [ ] **Step 4: Run focused deploy config tests** + +Run: + +```bash +bun test test/deploy-config.test.ts +``` + +Expected result: PASS, 3 tests passing. + +- [ ] **Step 5: Run lint** + +Run: + +```bash +bun run lint +``` + +Expected result: PASS with no Biome errors. + +- [ ] **Step 6: Run build** + +Run: + +```bash +bun run build +``` + +Expected result: PASS and `dist/index.js` plus `dist/migrate.js` exist. + +- [ ] **Step 7: Run deploy dry-run check** + +Run: + +```bash +./deploy.sh --check +``` + +Expected result: exits 0 and prints deploy config, credentials, and deploy file presence. + +- [ ] **Step 8: Review final diff and status** + +Run: + +```bash +git status --short +git diff -- deploy.sh test/deploy-config.test.ts .gitea/workflows/deploy.yml docs/superpowers/specs/2026-07-07-gitea-cicd-migration-design.md docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md +``` + +Expected result: only intended deployment config, workflow, spec, and plan changes appear. `.env` must not appear in `git status --short` as a tracked change. + +- [ ] **Step 9: Commit remaining changes only if authorized** + +If Task 1 and Task 2 were not committed earlier, ask the user: `May I commit the Gitea CI/CD migration changes?` + +If the user says yes, run: + +```bash +git add deploy.sh test/deploy-config.test.ts .gitea/workflows/deploy.yml docs/superpowers/specs/2026-07-07-gitea-cicd-migration-design.md docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md +git commit -m "ci: migrate deployment to gitea actions" +``` + +If there is nothing to commit because earlier task commits already captured the changes, run: + +```bash +git status --short +``` + +Expected result: clean working tree. + +- [ ] **Step 10: Ask for explicit push approval** + +Ask the user exactly: + +```text +Ready to push main to git@git.imrnes.team:MythEclipse/TeleUploader.git. Should I push now? +``` + +Do not push until the user answers yes. + +- [ ] **Step 11: Push `main` to Gitea after approval** + +After explicit approval, run: + +```bash +git push -u origin main +``` + +Expected result: push succeeds and branch `main` tracks `origin/main` on Gitea. + +- [ ] **Step 12: Verify repository and workflow visibility** + +Run: + +```bash +tea repos MythEclipse/TeleUploader +tea actions workflows list --repo MythEclipse/TeleUploader +tea actions runs list --repo MythEclipse/TeleUploader +``` + +Expected result: + +- Repository details are visible. +- Workflow list includes `Deploy FileDrop`. +- Runs list shows the push-triggered workflow run, or an empty/pending list if the Gitea runner has not picked it up yet. + +If workflow runs are absent because no runner is configured, report that the repo migration and workflow upload are complete but deployment requires a registered Gitea runner. + +--- + +## Self-Review + +- Spec coverage: repository creation, remote switch, provider-neutral `deploy.sh`, Gitea workflow, secrets, lint/build verification, and push approval are all mapped to tasks. +- Placeholder scan: no TBD/TODO/fill-in placeholders remain; conditional branches have exact commands and stop conditions. +- Type/command consistency: `VPS_HOST`, `VPS_USER`, `VPS_SSH_KEY`, `PRODUCTION_ENV`, and `ADMIN_PASSWORD` names are consistent across tests, workflow, `deploy.sh`, and `tea` commands. diff --git a/docs/superpowers/specs/2026-07-07-gitea-cicd-migration-design.md b/docs/superpowers/specs/2026-07-07-gitea-cicd-migration-design.md new file mode 100644 index 0000000..71fd168 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-gitea-cicd-migration-design.md @@ -0,0 +1,173 @@ +# Gitea Remote and CI/CD Migration Design + +## Goal + +Move the repository origin from GitLab to a new Gitea repository and add a Gitea Actions deployment flow that behaves like a GitHub Actions CI/CD pipeline. + +Target repository: + +- Gitea instance: `git.imrnes.team` +- Owner: `MythEclipse` +- Repository: `TeleUploader` +- SSH remote: `git@git.imrnes.team:MythEclipse/TeleUploader.git` + +Deployment should run automatically on pushes to `main`. + +## Current State + +- Current `origin` points to GitLab: `git@gitlab.com:superaseph/TeleUploader.git`. +- There is no existing `.github/workflows` or `.gitea/workflows` directory. +- `deploy.sh` already implements the production deploy path: + 1. Install dependencies. + 2. Format/lint/build with Bun. + 3. Ship the deploy context to the VPS with an SSH tar pipe. + 4. Build and restart the Docker Compose service on the VPS. + 5. Print container status/logs. +- `deploy.sh` still contains GitLab-specific CI variable lookup through `glab`. + +## Recommended Approach + +Use `deploy.sh` as the single deploy entrypoint and add a thin Gitea Actions wrapper around it. + +This keeps local and CI deployment behavior consistent while moving secret storage and automation from GitLab to Gitea. + +## Scope + +In scope: + +1. Create the Gitea repository with `tea` if it does not already exist. +2. Change `origin` to the new Gitea SSH URL. +3. Add `.gitea/workflows/deploy.yml`. +4. Make `deploy.sh` provider-neutral by removing GitLab/glab dependency. +5. Configure required Gitea Actions secrets/variables where supported by `tea`. +6. Push `main` to Gitea after user approval. + +Out of scope: + +- Rewriting the deploy flow into YAML-only SSH commands. +- Changing VPS Docker Compose architecture. +- Changing app runtime configuration or production domains. +- Adding a new Gitea runner. The design assumes a working runner exists for the Gitea instance. + +## Workflow Behavior + +The Gitea workflow should run on pushes to `main`: + +```yaml +on: + push: + branches: + - main +``` + +The workflow job should: + +1. Check out the repository. +2. Set up Bun in the runner environment. +3. Run `bun install`. +4. Run `bun run lint`. +5. Run `bun run build`. +6. Write the SSH private key from a Gitea Actions secret to a temporary file. +7. Export deploy environment variables. +8. Run `./deploy.sh --no-build`. + +`--no-build` avoids duplicate build work because the workflow already runs lint/build before deployment. + +## CI Gate + +The deployment gate is `lint + build` only. + +Tests are intentionally excluded from the automatic deploy gate because this repository has tests that can rely on network, database, or mock isolation constraints. Specific tests can still be run manually when needed. + +## Required Gitea Actions Configuration + +Required secret: + +- `VPS_SSH_KEY`: private SSH key used by the workflow to connect to the VPS. + +Required variable or secret: + +- `VPS_HOST`: VPS hostname or IP. +- `VPS_USER`: SSH username, expected to be `root` unless changed. + +The workflow can read `VPS_HOST` and `VPS_USER` from variables if Gitea Actions variables are enabled. If not, store them as secrets as well. + +## `deploy.sh` Changes + +`deploy.sh` should no longer require GitLab or `glab`. + +Changes: + +1. Remove `GITLAB_PROJECT`. +2. Remove `fetch_ci_var()`. +3. Update comments from GitLab CI vars to Gitea Actions secrets/variables or direct environment variables. +4. Keep local defaults: + - `VPS_HOST=45.127.35.244` + - `VPS_USER=root` + - `VPS_SSH_KEY=$HOME/.ssh/id_ed25519` +5. Preserve existing flags: + - `--no-build` + - `--check` + - `--help` +6. Preserve the existing deploy mechanism and health/status reporting. + +This keeps local `./deploy.sh` usage working while allowing Gitea Actions to inject CI credentials. + +## Repository Setup Commands + +Expected implementation commands after approval: + +```sh +tea repos create --owner MythEclipse --name TeleUploader --private +``` + +If the repository already exists, skip creation and use the existing remote. + +Then: + +```sh +git remote set-url origin git@git.imrnes.team:MythEclipse/TeleUploader.git +git push -u origin main +``` + +Before pushing, verify the local diff and ask for explicit approval if there are uncommitted changes that should be included. + +## Secret Setup Commands + +Use `tea` where possible: + +```sh +tea actions secrets create VPS_SSH_KEY +tea actions variables set VPS_HOST 45.127.35.244 +tea actions variables set VPS_USER root +``` + +If `tea actions variables` is unsupported by the instance, use secrets for all three values. + +Secrets that require interactive input may need the user to run the command with `!` in Claude Code so the prompt is visible in this session. + +## Error Handling + +- If `tea repos create` reports the repo already exists, continue by verifying repository details and setting the remote. +- If `tea` is not authenticated correctly, stop and ask the user to run `! tea login add`. +- If Gitea Actions variables are unavailable, fall back to repository secrets. +- If no Gitea runner is available, repository migration can still complete, but deployment will not run until a runner is registered. +- If `git push` fails due to SSH permissions, stop and report the exact failure. + +## Verification + +Before considering the migration ready: + +1. Run `./deploy.sh --check` locally to confirm deployment inputs resolve. +2. Run `bun run lint`. +3. Run `bun run build`. +4. Verify `git remote -v` points to `git@git.imrnes.team:MythEclipse/TeleUploader.git`. +5. Verify `.gitea/workflows/deploy.yml` exists. +6. After push, verify Gitea can see the repository and Actions workflow. + +## Open Operational Assumptions + +- Gitea Actions is enabled on `git.imrnes.team`. +- A runner capable of running Bun, SSH, tar, and shell commands is available. +- The workflow runner can reach the VPS over SSH. +- The deployment SSH key is authorized on the VPS. diff --git a/test/deploy-config.test.ts b/test/deploy-config.test.ts new file mode 100644 index 0000000..4b64bcb --- /dev/null +++ b/test/deploy-config.test.ts @@ -0,0 +1,58 @@ +import { expect, test } from 'bun:test'; + +const repoRoot = new URL('../', import.meta.url); +const deployScript = Bun.file(new URL('../deploy.sh', import.meta.url)); + +test('deploy script is provider-neutral', async () => { + const text = await deployScript.text(); + + expect(text).not.toContain('GITLAB_PROJECT'); + expect(text).not.toContain('fetch_ci_var'); + expect(text).not.toContain('glab'); + expect(text).not.toContain('GitLab CI'); + expect(text).toContain('Gitea Actions secrets'); +}); + +test('deploy check mode does not require an SSH key file', async () => { + const proc = Bun.spawn(['bash', 'deploy.sh', '--check'], { + cwd: repoRoot.pathname, + env: { + ...Bun.env, + VPS_HOST: '203.0.113.10', + VPS_USER: 'deploy', + VPS_SSH_KEY: '/tmp/nonexistent-teleuploader-key', + }, + }); + + const [stdout, stderr, exitCode] = await Promise.all([ + new Response(proc.stdout).text(), + new Response(proc.stderr).text(), + proc.exited, + ]); + + expect(exitCode).toBe(0); + expect(stdout).toContain('App name:'); + expect(stdout).toContain('VPS_HOST:'); + expect(stdout).toContain('VPS_USER:'); + expect(stdout).toContain('VPS_SSH_KEY:'); + expect(stderr).toBe(''); +}); + +const workflowFile = Bun.file(new URL('../.gitea/workflows/deploy.yml', import.meta.url)); + +test('gitea workflow deploys pushes to main through deploy script', async () => { + const text = await workflowFile.text(); + + expect(text).toContain('name: Deploy FileDrop'); + expect(text).toContain('branches:\n - main'); + expect(text).toContain('uses: actions/checkout@v4'); + expect(text).toContain('uses: oven-sh/setup-bun@v2'); + expect(text).toContain('bun install --frozen-lockfile'); + expect(text).toContain('bun run lint'); + expect(text).toContain('bun run build'); + expect(text).toContain('secrets.VPS_HOST'); + expect(text).toContain('secrets.VPS_USER'); + expect(text).toContain('secrets.VPS_SSH_KEY'); + expect(text).toContain('secrets.PRODUCTION_ENV'); + expect(text).toContain('./deploy.sh --no-build'); +});