Files
TeleUploader/docs/superpowers/plans/2026-07-07-gitea-cicd-migration.md
T
asepharyana e8226bba64
Deploy FileDrop / deploy (push) Successful in 3m21s
ci: migrate deployment to gitea actions
2026-07-07 20:39:41 +07:00

21 KiB

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:

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:

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:

# 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:

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:

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:

# ── 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:

# ── 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:

# 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:

# ── Helpers ───────────────────────────────────────────────────────────────────
log()  { echo "→ $*"; }
ok()   { echo "✓ $*"; }
die()  { echo "✗ $*"; exit 1; }

After validation, keep these lines together:

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:

# ── 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:

bun test test/deploy-config.test.ts

Expected result: PASS, 2 tests passing.

  • Step 8: Run deploy check mode manually

Run:

./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:

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:

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:

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:

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:

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:

bun test test/deploy-config.test.ts

Expected result: PASS, 3 tests passing.

  • Step 5: Run project lint

Run:

bun run lint

Expected result: PASS with Biome reporting no errors.

  • Step 6: Run project build

Run:

bun run build

Expected result: PASS and creates/updates dist/index.js and dist/migrate.js.

  • Step 7: Review diff

Run:

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:

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:

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:

! tea login add
  • Step 2: Check whether the target repo already exists

Run:

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:

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:

tea repos MythEclipse/TeleUploader

Expected result: repository details for MythEclipse/TeleUploader.

  • Step 4: Verify local production .env exists and is untracked

Run:

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:

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:

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:

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:

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:

git remote set-url origin git@git.imrnes.team:MythEclipse/TeleUploader.git
  • Step 3: Verify remote URL

Run:

git remote -v

Expected result:

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:

bun test test/deploy-config.test.ts

Expected result: PASS, 3 tests passing.

  • Step 5: Run lint

Run:

bun run lint

Expected result: PASS with no Biome errors.

  • Step 6: Run build

Run:

bun run build

Expected result: PASS and dist/index.js plus dist/migrate.js exist.

  • Step 7: Run deploy dry-run check

Run:

./deploy.sh --check

Expected result: exits 0 and prints deploy config, credentials, and deploy file presence.

  • Step 8: Review final diff and status

Run:

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:

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:

git status --short

Expected result: clean working tree.

  • Step 10: Ask for explicit push approval

Ask the user exactly:

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:

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:

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.