From 8b6d4e8ea6900fbad3abb792283e147b2208750f Mon Sep 17 00:00:00 2001 From: Hardik Date: Thu, 11 Jun 2026 16:39:43 +0530 Subject: [PATCH] =?UTF-8?q?feat(automation):=20issue-to-deploy=20pipeline?= =?UTF-8?q?=20=E2=80=94=20Report=20Issue=20button,=20Claude=20watcher,=20t?= =?UTF-8?q?ag-triggered=20deploy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Report Issue button in portal header files a Forgejo issue (portal + claude-queue labels) - Windows scheduled watcher runs headless Claude Code on queued issues and opens a PR - .forgejo/workflows/deploy.yml deploys v* release tags via the pms1 host runner (pm2 restart ppms) Co-Authored-By: Claude Fable 5 --- .forgejo/workflows/deploy.yml | 42 ++++ .gitignore | 5 + App/.env.example | 6 + App/components/layout/header.tsx | 2 + App/components/layout/report-issue-actions.ts | 52 +++++ App/components/layout/report-issue-button.tsx | 130 ++++++++++++ App/lib/forgejo.ts | 56 +++++ automation/README.md | 69 ++++++ automation/claude-issue-watcher.ps1 | 197 ++++++++++++++++++ automation/register-watcher-task.ps1 | 24 +++ automation/watcher.config.example.json | 11 + 11 files changed, 594 insertions(+) create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 App/components/layout/report-issue-actions.ts create mode 100644 App/components/layout/report-issue-button.tsx create mode 100644 App/lib/forgejo.ts create mode 100644 automation/README.md create mode 100644 automation/claude-issue-watcher.ps1 create mode 100644 automation/register-watcher-task.ps1 create mode 100644 automation/watcher.config.example.json diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..1241ab7 --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,42 @@ +name: Deploy release to production + +# Pushing a release tag (vX.Y.Z) deploys that tag to the portal at +# pms.pelagiamarine.com. Runs on the pms1 host runner (label: host), +# which executes as shad0w with direct access to the pm2-managed app. + +on: + push: + tags: + - "v*" + +jobs: + deploy: + runs-on: host + steps: + - name: Deploy tag to ~/pms and restart ppms + run: | + set -euo pipefail + export NVM_DIR="$HOME/.nvm" + . "$NVM_DIR/nvm.sh" + + TAG="${GITHUB_REF_NAME}" + echo "=== Deploying $TAG ===" + + cd "$HOME/pms" + git fetch origin --tags --force + git checkout -f "refs/tags/$TAG" + + cd App + pnpm install --frozen-lockfile + pnpm build # includes prisma generate + pnpm db:migrate:deploy + + pm2 restart ppms --update-env + echo "=== Deployed $TAG ===" + + - name: Verify portal responds + run: | + sleep 5 + code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:3000/login) + echo "Portal /login returned HTTP $code" + test "$code" = "200" diff --git a/.gitignore b/.gitignore index 341c6be..bd4dceb 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,11 @@ App/coverage/ .codex/ .antigravity/ +# Claude issue watcher (real token + logs stay local) +automation/watcher.config.json +automation/logs/ +automation/.watcher.lock + # OS .DS_Store Thumbs.db diff --git a/App/.env.example b/App/.env.example index 4d7757b..3001b4b 100644 --- a/App/.env.example +++ b/App/.env.example @@ -48,3 +48,9 @@ EMAIL_FROM_NAME="Pelagia Portal" # Development default (localhost:3002) is used if this is unset. # Start the service with: cd GstService && npm run dev GST_SERVICE_URL=http://localhost:3003 + +# ── Forgejo issue reporting (Report Issue button) ───────────── +# Token needs write:issue scope on the repo below. +FORGEJO_URL=https://git.pelagiamarine.com +FORGEJO_REPO=shad0w/pelagia-portal +FORGEJO_TOKEN= diff --git a/App/components/layout/header.tsx b/App/components/layout/header.tsx index 0189acc..c70d7f8 100644 --- a/App/components/layout/header.tsx +++ b/App/components/layout/header.tsx @@ -5,6 +5,7 @@ import { LogOut } from "lucide-react"; import type { Role } from "@prisma/client"; import { CartIcon } from "./cart-icon"; import { NotificationBell } from "./notification-bell"; +import { ReportIssueButton } from "./report-issue-button"; const ROLE_LABELS: Record = { TECHNICAL: "Technical", @@ -39,6 +40,7 @@ export function Header({ user, initialUnreadCount, initialNotifications }: Heade
{CART_ROLES.includes(user.role) && } + { + const session = await auth(); + if (!session?.user) return { error: "Unauthorized" }; + + const parsed = reportSchema.safeParse(Object.fromEntries(formData)); + if (!parsed.success) return { error: parsed.error.errors[0].message }; + const { title, description, priority, page } = parsed.data; + + const body = [ + "### Raised by", + `${session.user.name} (${session.user.email}, ${session.user.role}) — via portal Report Issue button`, + "", + "### Description", + description, + "", + "### Priority", + priority, + "", + "### Context", + `- Page: \`${page || "unknown"}\``, + `- Reported at: ${new Date().toISOString()}`, + ].join("\n"); + + try { + const issue = await createForgejoIssue({ + title: `[Issue]: ${title}`, + body, + labels: ["portal", "claude-queue"], + }); + return { ok: true, issueNumber: issue.number, issueUrl: issue.html_url }; + } catch (err) { + console.error("reportIssue failed:", err); + return { error: "Could not file the issue. Please try again or contact the administrator." }; + } +} diff --git a/App/components/layout/report-issue-button.tsx b/App/components/layout/report-issue-button.tsx new file mode 100644 index 0000000..bf61793 --- /dev/null +++ b/App/components/layout/report-issue-button.tsx @@ -0,0 +1,130 @@ +"use client"; + +import { useState, useTransition } from "react"; +import { usePathname } from "next/navigation"; +import { Bug } from "lucide-react"; +import { AdminDialog } from "@/components/ui/admin-dialog"; +import { reportIssue } from "./report-issue-actions"; + +const PRIORITIES = [ + "P0 — Critical (broken / blocking)", + "P1 — High", + "P2 — Medium", + "P3 — Low", +] as const; + +export function ReportIssueButton() { + const pathname = usePathname(); + const [open, setOpen] = useState(false); + const [pending, startTransition] = useTransition(); + const [error, setError] = useState(null); + const [filedIssue, setFiledIssue] = useState<{ number: number; url: string } | null>(null); + + function close() { + setOpen(false); + setError(null); + setFiledIssue(null); + } + + function onSubmit(e: React.FormEvent) { + e.preventDefault(); + setError(null); + const formData = new FormData(e.currentTarget); + formData.set("page", pathname); + startTransition(async () => { + const result = await reportIssue(formData); + if ("error" in result) { + setError(result.error); + } else { + setFiledIssue({ number: result.issueNumber, url: result.issueUrl }); + } + }); + } + + return ( + <> + + + + {filedIssue ? ( +
+

+ Thanks — issue #{filedIssue.number} has been + filed and queued for an automated fix. You'll see the change in an upcoming + release. +

+ +
+ ) : ( +
+
+ + +
+
+ +