From 297f5b7f8ac6eeef96db2ba1c0f7af13ef81c03b Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Thu, 12 Mar 2026 17:34:11 +1100 Subject: [PATCH 01/12] Cleanup unused images --- agent/internal/build/build.go | 2 +- agent/internal/container/runtime_darwin.go | 2 +- agent/internal/container/runtime_linux.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/agent/internal/build/build.go b/agent/internal/build/build.go index ff64287..95def39 100644 --- a/agent/internal/build/build.go +++ b/agent/internal/build/build.go @@ -409,7 +409,7 @@ func (b *Builder) Cleanup() error { } } - log.Printf("[build:cleanup] pruning dangling images") + log.Printf("[build:cleanup] pruning unused images") container.ImagePrune() return nil diff --git a/agent/internal/container/runtime_darwin.go b/agent/internal/container/runtime_darwin.go index e7f8168..59d13bc 100644 --- a/agent/internal/container/runtime_darwin.go +++ b/agent/internal/container/runtime_darwin.go @@ -494,7 +494,7 @@ func writeDockerConfig(registryURL, username, password string) error { } func ImagePrune() { - exec.Command("docker", "image", "prune", "-f").Run() + exec.Command("docker", "image", "prune", "-a", "-f").Run() } type dockerContainer struct { diff --git a/agent/internal/container/runtime_linux.go b/agent/internal/container/runtime_linux.go index 81069a5..3175254 100644 --- a/agent/internal/container/runtime_linux.go +++ b/agent/internal/container/runtime_linux.go @@ -494,7 +494,7 @@ func writeDockerConfig(registryURL, username, password string) error { } func ImagePrune() { - exec.Command("podman", "image", "prune", "-f").Run() + exec.Command("podman", "image", "prune", "-a", "-f").Run() } type podmanContainer struct { From fb1bf7c3fa9a6892f650bee343937c2932275b5f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Sat, 14 Mar 2026 17:51:25 +1100 Subject: [PATCH 02/12] Fix wireguard after reboot --- agent/internal/agent/drift.go | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/agent/internal/agent/drift.go b/agent/internal/agent/drift.go index 6a71d42..81e8002 100644 --- a/agent/internal/agent/drift.go +++ b/agent/internal/agent/drift.go @@ -415,7 +415,10 @@ func (a *Agent) reconcileOne(actual *ActualState) error { Endpoint: p.Endpoint, } } - if wireguard.HashPeers(expectedWgPeers) != actual.WireguardHash { + wgPeersChanged := wireguard.HashPeers(expectedWgPeers) != actual.WireguardHash + wgIsUp := wireguard.IsUp(wireguard.DefaultInterface) + + if wgPeersChanged { log.Printf("[reconcile] updating WireGuard peers") if err := a.reconcileWireguard(expectedWgPeers); err != nil { return fmt.Errorf("failed to update WireGuard: %w", err) @@ -423,6 +426,14 @@ func (a *Agent) reconcileOne(actual *ActualState) error { return nil } + if !wgIsUp { + log.Printf("[reconcile] WireGuard interface is down, bringing it up") + if err := wireguard.Up(wireguard.DefaultInterface); err != nil { + return fmt.Errorf("failed to bring up WireGuard: %w", err) + } + return nil + } + return nil } From 62a09aa2272ad7f45314eaa20df899953233df8d Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 16 Mar 2026 07:59:41 +1100 Subject: [PATCH 03/12] Update release workflow --- ...gent-release.yml => agent-tip-release.yml} | 16 +-- .github/workflows/control-plane-release.yml | 64 --------- .github/workflows/release.yml | 132 ++++++++++++++++++ agent/cmd/agent/main.go | 2 +- agent/internal/agent/reporting.go | 9 +- web/public/setup.sh | 4 +- web/public/update.sh | 4 +- 7 files changed, 146 insertions(+), 85 deletions(-) rename .github/workflows/{agent-release.yml => agent-tip-release.yml} (85%) delete mode 100644 .github/workflows/control-plane-release.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/agent-release.yml b/.github/workflows/agent-tip-release.yml similarity index 85% rename from .github/workflows/agent-release.yml rename to .github/workflows/agent-tip-release.yml index 8fbfd81..6789fcc 100644 --- a/.github/workflows/agent-release.yml +++ b/.github/workflows/agent-tip-release.yml @@ -1,4 +1,4 @@ -name: Build and Release Agent +name: Agent Tip Release on: push: @@ -6,13 +6,13 @@ on: - main paths: - "agent/**" - workflow_dispatch: + - ".github/workflows/agent-tip-release.yml" permissions: contents: write jobs: - build: + agent: runs-on: ubuntu-latest strategy: matrix: @@ -43,7 +43,7 @@ jobs: GOARCH: ${{ matrix.goarch }} CGO_ENABLED: 0 run: | - go build -ldflags="-s -w" -o agent-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/agent + go build -ldflags="-s -w -X techulus/cloud-agent/internal/agent.Version=${{ github.sha }}" -o agent-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/agent - name: Upload artifact uses: actions/upload-artifact@v4 @@ -53,7 +53,7 @@ jobs: retention-days: 1 release: - needs: build + needs: agent runs-on: ubuntu-latest steps: - name: Download all artifacts @@ -67,7 +67,7 @@ jobs: cd binaries sha256sum agent-* > checksums.txt - - name: Delete existing release assets + - name: Update tip release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | @@ -76,10 +76,6 @@ jobs: gh api -X DELETE repos/${{ github.repository }}/releases/assets/$asset_id done - - name: Upload binaries to Tip release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | for binary in binaries/agent-*; do chmod +x "$binary" gh release upload tip "$binary" --repo ${{ github.repository }} --clobber diff --git a/.github/workflows/control-plane-release.yml b/.github/workflows/control-plane-release.yml deleted file mode 100644 index c7f27c7..0000000 --- a/.github/workflows/control-plane-release.yml +++ /dev/null @@ -1,64 +0,0 @@ -name: Build and Push Images - -on: - push: - branches: - - release - workflow_dispatch: - -permissions: - contents: read - packages: write - -jobs: - web: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: web - push: true - tags: ghcr.io/${{ github.repository }}/web:tip - platforms: linux/amd64,linux/arm64 - cache-from: type=gha,scope=web - cache-to: type=gha,mode=max,scope=web - - registry: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Login to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push - uses: docker/build-push-action@v6 - with: - context: registry - push: true - tags: ghcr.io/${{ github.repository }}/registry:tip - platforms: linux/amd64,linux/arm64 - cache-from: type=gha,scope=registry - cache-to: type=gha,mode=max,scope=registry diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..4810fe4 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,132 @@ +name: Release + +on: + push: + tags: + - "v*" + +permissions: + contents: write + packages: write + +jobs: + agent: + runs-on: ubuntu-latest + strategy: + matrix: + include: + - goos: linux + goarch: amd64 + - goos: linux + goarch: arm64 + - goos: darwin + goarch: amd64 + - goos: darwin + goarch: arm64 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version-file: agent/go.mod + cache-dependency-path: agent/go.sum + + - name: Build + working-directory: agent + env: + GOOS: ${{ matrix.goos }} + GOARCH: ${{ matrix.goarch }} + CGO_ENABLED: 0 + run: | + go build -ldflags="-s -w -X techulus/cloud-agent/internal/agent.Version=${{ github.ref_name }}" -o agent-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/agent + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: agent-${{ matrix.goos }}-${{ matrix.goarch }} + path: agent/agent-${{ matrix.goos }}-${{ matrix.goarch }} + retention-days: 1 + + release: + needs: agent + runs-on: ubuntu-latest + steps: + - name: Download all artifacts + uses: actions/download-artifact@v4 + with: + path: binaries + merge-multiple: true + + - name: Generate SHA256 checksums + run: | + cd binaries + sha256sum agent-* > checksums.txt + + - name: Create GitHub release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release create "${{ github.ref_name }}" \ + --repo ${{ github.repository }} \ + --title "${{ github.ref_name }}" \ + --generate-notes \ + binaries/agent-* binaries/checksums.txt + + web: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: web + push: true + tags: | + ghcr.io/${{ github.repository }}/web:${{ github.ref_name }} + ghcr.io/${{ github.repository }}/web:tip + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=web + cache-to: type=gha,mode=max,scope=web + + registry: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: registry + push: true + tags: | + ghcr.io/${{ github.repository }}/registry:${{ github.ref_name }} + ghcr.io/${{ github.repository }}/registry:tip + platforms: linux/amd64,linux/arm64 + cache-from: type=gha,scope=registry + cache-to: type=gha,mode=max,scope=registry diff --git a/agent/cmd/agent/main.go b/agent/cmd/agent/main.go index b4a7941..eb8842f 100644 --- a/agent/cmd/agent/main.go +++ b/agent/cmd/agent/main.go @@ -293,7 +293,7 @@ func main() { publicIP := getPublicIP() privateIP := getPrivateIP() - log.Printf("Agent started. Public IP: %s, Private IP: %s. Tick interval: %v", publicIP, privateIP, agent.TickInterval) + log.Printf("Agent v%s started. Public IP: %s, Private IP: %s. Tick interval: %v", agent.Version, publicIP, privateIP, agent.TickInterval) agentInstance := agent.NewAgent(client, reconciler, config, publicIP, privateIP, dataDir, logCollector, traefikLogCollector, builder, config.IsProxy, disableDNS) agentInstance.Run(ctx) diff --git a/agent/internal/agent/reporting.go b/agent/internal/agent/reporting.go index 6da1c5e..4513604 100644 --- a/agent/internal/agent/reporting.go +++ b/agent/internal/agent/reporting.go @@ -16,17 +16,14 @@ import ( "github.com/shirou/gopsutil/v3/mem" ) +var Version = "dev" + var ( agentStartTime = time.Now() - agentVersion = "dev" lastHealthCollect time.Time healthCollectMu sync.Mutex ) -func SetAgentVersion(version string) { - agentVersion = version -} - func (a *Agent) BuildStatusReport(includeResources bool) *agenthttp.StatusReport { report := &agenthttp.StatusReport{ PublicIP: a.PublicIP, @@ -46,7 +43,7 @@ func (a *Agent) BuildStatusReport(includeResources bool) *agenthttp.StatusReport report.NetworkHealth = health.CollectNetworkHealth("wg0") report.ContainerHealth = health.CollectContainerHealth() report.AgentHealth = &agenthttp.AgentHealth{ - Version: agentVersion, + Version: Version, UptimeSecs: int64(time.Since(agentStartTime).Seconds()), } lastHealthCollect = time.Now() diff --git a/web/public/setup.sh b/web/public/setup.sh index 1793f0b..d2ea963 100644 --- a/web/public/setup.sh +++ b/web/public/setup.sh @@ -300,8 +300,8 @@ fi echo "✓ crane installed" step "Downloading Techulus Cloud agent..." -AGENT_URL="https://github.com/techulus/cloud/releases/download/tip/agent-linux-${AGENT_ARCH}" -CHECKSUM_URL="https://github.com/techulus/cloud/releases/download/tip/checksums.txt" +AGENT_URL="https://github.com/techulus/cloud/releases/latest/download/agent-linux-${AGENT_ARCH}" +CHECKSUM_URL="https://github.com/techulus/cloud/releases/latest/download/checksums.txt" curl -fsSL -o /tmp/techulus-agent "$AGENT_URL" if [ ! -f /tmp/techulus-agent ]; then diff --git a/web/public/update.sh b/web/public/update.sh index b0cba23..114c5c5 100644 --- a/web/public/update.sh +++ b/web/public/update.sh @@ -36,8 +36,8 @@ esac echo "Architecture: $ARCH ($AGENT_ARCH)" echo "Downloading latest agent..." -AGENT_URL="https://github.com/techulus/cloud/releases/download/tip/agent-linux-${AGENT_ARCH}" -CHECKSUM_URL="https://github.com/techulus/cloud/releases/download/tip/checksums.txt" +AGENT_URL="https://github.com/techulus/cloud/releases/latest/download/agent-linux-${AGENT_ARCH}" +CHECKSUM_URL="https://github.com/techulus/cloud/releases/latest/download/checksums.txt" curl -fsSL -o /tmp/techulus-agent "$AGENT_URL" if [ ! -f /tmp/techulus-agent ]; then From 334e2990d385fd27894208d878a002f32e548971 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 16 Mar 2026 19:16:34 +1100 Subject: [PATCH 04/12] Fix release workflow --- .github/workflows/agent-tip-release.yml | 8 ++++---- .github/workflows/release.yml | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.github/workflows/agent-tip-release.yml b/.github/workflows/agent-tip-release.yml index 6789fcc..4732add 100644 --- a/.github/workflows/agent-tip-release.yml +++ b/.github/workflows/agent-tip-release.yml @@ -28,10 +28,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: agent/go.mod cache-dependency-path: agent/go.sum @@ -46,7 +46,7 @@ jobs: go build -ldflags="-s -w -X techulus/cloud-agent/internal/agent.Version=${{ github.sha }}" -o agent-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/agent - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: agent-${{ matrix.goos }}-${{ matrix.goarch }} path: agent/agent-${{ matrix.goos }}-${{ matrix.goarch }} @@ -57,7 +57,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Download all artifacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@v8 with: path: binaries merge-multiple: true diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4810fe4..a58cf9a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,10 +26,10 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: agent/go.mod cache-dependency-path: agent/go.sum @@ -44,7 +44,7 @@ jobs: go build -ldflags="-s -w -X techulus/cloud-agent/internal/agent.Version=${{ github.ref_name }}" -o agent-${{ matrix.goos }}-${{ matrix.goarch }} ./cmd/agent - name: Upload artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v7 with: name: agent-${{ matrix.goos }}-${{ matrix.goarch }} path: agent/agent-${{ matrix.goos }}-${{ matrix.goarch }} @@ -54,10 +54,11 @@ jobs: needs: agent runs-on: ubuntu-latest steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 + - name: Download agent artifacts + uses: actions/download-artifact@v8 with: path: binaries + pattern: agent-* merge-multiple: true - name: Generate SHA256 checksums @@ -79,13 +80,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} @@ -107,13 +108,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@v4 - name: Login to GHCR - uses: docker/login-action@v3 + uses: docker/login-action@v4 with: registry: ghcr.io username: ${{ github.actor }} From 6bcb376b9cb75a62038c8cbc3794bb8c181169b0 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 16 Mar 2026 20:06:45 +1100 Subject: [PATCH 05/12] Fix version display for server --- web/components/server/server-health-details.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/web/components/server/server-health-details.tsx b/web/components/server/server-health-details.tsx index 9ce243d..021955e 100644 --- a/web/components/server/server-health-details.tsx +++ b/web/components/server/server-health-details.tsx @@ -106,9 +106,7 @@ export function ServerHealthDetails({ } /> From 897bdc0c5f142e34d88971d3f66ed50c2cf3cef4 Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 16 Mar 2026 21:35:44 +1100 Subject: [PATCH 06/12] Avoid sending entire traefik log on restart --- agent/internal/logs/traefik_collector.go | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/agent/internal/logs/traefik_collector.go b/agent/internal/logs/traefik_collector.go index 3e39be8..7a3dc5a 100644 --- a/agent/internal/logs/traefik_collector.go +++ b/agent/internal/logs/traefik_collector.go @@ -79,6 +79,7 @@ type TraefikCollector struct { cancel context.CancelFunc wg sync.WaitGroup lastPos int64 + initialized bool droppedCount int } @@ -128,8 +129,15 @@ func (c *TraefikCollector) tailFile() error { } defer file.Close() - if c.lastPos == 0 { - log.Printf("[traefik-logs] started tailing %s", traefikLogPath) + if !c.initialized { + endPos, err := file.Seek(0, io.SeekEnd) + if err != nil { + return err + } + c.lastPos = endPos + c.initialized = true + log.Printf("[traefik-logs] started tailing %s (skipped to position %d)", traefikLogPath, endPos) + return nil } stat, err := file.Stat() From 090c26f7606c6c2361d998f21a7cb06077fba63f Mon Sep 17 00:00:00 2001 From: Arjun Komath Date: Mon, 16 Mar 2026 22:49:48 +1100 Subject: [PATCH 07/12] Remove github app setup flow --- .github/workflows/release.yml | 2 + web/Dockerfile | 2 + web/actions/projects.ts | 14 +- .../(dashboard)/dashboard/settings/page.tsx | 6 +- web/app/api/github/manifest/callback/route.ts | 60 --- web/components/github/github-app-setup.tsx | 371 ------------------ web/components/logs/log-viewer.tsx | 36 +- .../details/pending-changes-banner.tsx | 96 ++--- web/components/settings/global-settings.tsx | 39 +- 9 files changed, 100 insertions(+), 526 deletions(-) delete mode 100644 web/app/api/github/manifest/callback/route.ts delete mode 100644 web/components/github/github-app-setup.tsx diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a58cf9a..b6110f5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -100,6 +100,8 @@ jobs: tags: | ghcr.io/${{ github.repository }}/web:${{ github.ref_name }} ghcr.io/${{ github.repository }}/web:tip + build-args: | + APP_VERSION=${{ github.ref_name }} platforms: linux/amd64,linux/arm64 cache-from: type=gha,scope=web cache-to: type=gha,mode=max,scope=web diff --git a/web/Dockerfile b/web/Dockerfile index 63f2990..7c3ea01 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -6,11 +6,13 @@ RUN pnpm install --frozen-lockfile FROM node:24-slim AS builder WORKDIR /app +ARG APP_VERSION=dev COPY --from=deps /app/node_modules ./node_modules COPY . . ENV DATABASE_URL=postgres://build:build@localhost:5432/build ENV BETTER_AUTH_SECRET=build-secret ENV BETTER_AUTH_URL=http://localhost:3000 +ENV NEXT_PUBLIC_APP_VERSION=$APP_VERSION RUN npx next build FROM node:24-slim AS drizzle diff --git a/web/actions/projects.ts b/web/actions/projects.ts index 862f069..510bfa3 100644 --- a/web/actions/projects.ts +++ b/web/actions/projects.ts @@ -1047,14 +1047,12 @@ export async function abortRollout(serviceId: string) { }); if (workToDelete.length > 0) { - await db - .delete(workQueue) - .where( - inArray( - workQueue.id, - workToDelete.map((w) => w.id), - ), - ); + await db.delete(workQueue).where( + inArray( + workQueue.id, + workToDelete.map((w) => w.id), + ), + ); } return { success: true }; diff --git a/web/app/(dashboard)/dashboard/settings/page.tsx b/web/app/(dashboard)/dashboard/settings/page.tsx index d754876..849df55 100644 --- a/web/app/(dashboard)/dashboard/settings/page.tsx +++ b/web/app/(dashboard)/dashboard/settings/page.tsx @@ -24,7 +24,11 @@ export default async function SettingsPage() {

- + ); diff --git a/web/app/api/github/manifest/callback/route.ts b/web/app/api/github/manifest/callback/route.ts deleted file mode 100644 index b3f4afb..0000000 --- a/web/app/api/github/manifest/callback/route.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { NextRequest, NextResponse } from "next/server"; - -export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const code = searchParams.get("code"); - - if (!code || !/^[a-zA-Z0-9]+$/.test(code)) { - return NextResponse.redirect( - new URL("/dashboard/settings?github_error=missing_code", request.url), - ); - } - - try { - const response = await fetch( - `https://api.github.com/app-manifests/${code}/conversions`, - { - method: "POST", - headers: { - Accept: "application/vnd.github+json", - }, - }, - ); - - if (!response.ok) { - const error = await response.text(); - console.error("GitHub manifest conversion failed:", error); - return NextResponse.redirect( - new URL( - "/dashboard/settings?github_error=conversion_failed", - request.url, - ), - ); - } - - const data = await response.json(); - - const credentials = { - id: data.id, - slug: data.slug, - pem: Buffer.from(data.pem).toString("base64"), - webhookSecret: data.webhook_secret, - ownerType: data.owner?.type, - ownerLogin: data.owner?.login, - }; - - const credentialsParam = encodeURIComponent(JSON.stringify(credentials)); - - return NextResponse.redirect( - new URL( - `/dashboard/settings?github_credentials=${credentialsParam}`, - request.url, - ), - ); - } catch (error) { - console.error("GitHub manifest callback error:", error); - return NextResponse.redirect( - new URL("/dashboard/settings?github_error=unknown", request.url), - ); - } -} diff --git a/web/components/github/github-app-setup.tsx b/web/components/github/github-app-setup.tsx deleted file mode 100644 index 2b914fa..0000000 --- a/web/components/github/github-app-setup.tsx +++ /dev/null @@ -1,371 +0,0 @@ -"use client"; - -import { useState, useEffect } from "react"; -import { useSearchParams, useRouter } from "next/navigation"; -import { toast } from "sonner"; -import isFQDN from "validator/es/lib/isFQDN"; -import { - Github, - TriangleAlert, - ExternalLink, - Copy, - RotateCcw, -} from "lucide-react"; -import { Button, buttonVariants } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { Label } from "@/components/ui/label"; -import { Switch } from "@/components/ui/switch"; -import { Item, ItemContent, ItemMedia, ItemTitle } from "@/components/ui/item"; -import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; - -const HOSTNAME_KEY = "techulus_github_hostname"; -const STORAGE_TTL = 10 * 60 * 1000; - -type GitHubCredentials = { - id: string; - slug: string; - pem: string; - webhookSecret: string; - ownerType?: string; - ownerLogin?: string; -}; - -function getStoredHostname(): string { - if (typeof window === "undefined") return ""; - const item = sessionStorage.getItem(HOSTNAME_KEY); - if (!item) return ""; - try { - const { value, expires } = JSON.parse(item); - if (Date.now() > expires) { - sessionStorage.removeItem(HOSTNAME_KEY); - return ""; - } - return value; - } catch { - return ""; - } -} - -function setStoredHostname(value: string) { - const item = { value, expires: Date.now() + STORAGE_TTL }; - sessionStorage.setItem(HOSTNAME_KEY, JSON.stringify(item)); -} - -export function GitHubAppSetup() { - const searchParams = useSearchParams(); - const router = useRouter(); - - const [hostname, setHostname] = useState(""); - const [useOrg, setUseOrg] = useState(false); - const [orgId, setOrgId] = useState(""); - const [credentials, setCredentials] = useState( - null, - ); - const [copied, setCopied] = useState(false); - const [loading, setLoading] = useState(false); - - useEffect(() => { - const storedHostname = getStoredHostname(); - if (storedHostname) { - setHostname(storedHostname); - } - - const credentialsParam = searchParams.get("github_credentials"); - const errorParam = searchParams.get("github_error"); - - if (credentialsParam) { - try { - const parsed = JSON.parse(decodeURIComponent(credentialsParam)); - setCredentials(parsed); - setLoading(false); - } catch { - toast.error("Failed to parse GitHub credentials"); - } - router.replace("/dashboard/settings?tab=github", { scroll: false }); - } - - if (errorParam) { - const messages: Record = { - missing_code: "GitHub did not return an authorization code", - conversion_failed: - "Failed to exchange code for credentials. The code may have expired.", - unknown: "An unknown error occurred", - }; - toast.error(messages[errorParam] || "GitHub setup failed"); - setLoading(false); - router.replace("/dashboard/settings?tab=github", { scroll: false }); - } - }, [searchParams, router]); - - const isHostnameValid = (() => { - const h = hostname.trim().toLowerCase(); - if (!h) return false; - if (h === "localhost") return true; - return isFQDN(h); - })(); - - const isOrgValid = - !useOrg || /^[a-z0-9]([a-z0-9_-]*[a-z0-9])?$/i.test(orgId.trim()); - - const canSubmit = isHostnameValid && isOrgValid && !loading; - - const githubUrl = - useOrg && orgId.trim() - ? `https://github.com/organizations/${orgId.trim()}/settings/apps/new` - : "https://github.com/settings/apps/new"; - - const generateManifest = () => { - const h = hostname.trim().toLowerCase(); - const protocol = - h === "localhost" || h.endsWith(".local") ? "http" : "https"; - const appBaseUrl = `${protocol}://${h}`; - const callbackUrl = `${typeof window !== "undefined" ? window.location.origin : ""}/api/github/manifest/callback`; - const appName = `techulus-cloud-${useOrg && orgId ? orgId.trim() : Math.random().toString(36).substring(2, 8)}`; - - return JSON.stringify({ - name: appName, - url: appBaseUrl, - hook_attributes: { - url: - h === "localhost" - ? "http://example.com/api/webhooks/github" - : `${appBaseUrl}/api/webhooks/github`, - active: true, - }, - redirect_url: callbackUrl, - callback_urls: [ - `${appBaseUrl}/api/github/authorize/callback`, - `${appBaseUrl}/auth/github/callback`, - ], - setup_url: `${appBaseUrl}/api/github/setup`, - setup_on_update: true, - public: true, - default_permissions: { - administration: "write", - checks: "write", - contents: "write", - deployments: "write", - issues: "write", - metadata: "read", - pull_requests: "write", - repository_hooks: "write", - statuses: "write", - emails: "read", - }, - default_events: ["installation_target", "push", "repository"], - }); - }; - - const handleSubmit = (e: React.FormEvent) => { - if (!canSubmit) { - e.preventDefault(); - return; - } - setStoredHostname(hostname.trim().toLowerCase()); - }; - - const envOutput = credentials - ? `GITHUB_APP_ID="${credentials.id}" -GITHUB_APP_PRIVATE_KEY="${credentials.pem}" -GITHUB_WEBHOOK_SECRET="${credentials.webhookSecret}"` - : ""; - - const githubAppUrl = credentials - ? credentials.ownerType === "Organization" - ? `https://github.com/organizations/${credentials.ownerLogin}/settings/apps/${credentials.slug}` - : `https://github.com/settings/apps/${credentials.slug}` - : ""; - - const copyToClipboard = async () => { - try { - await navigator.clipboard.writeText(envOutput); - setCopied(true); - toast.success("Copied to clipboard"); - setTimeout(() => setCopied(false), 2000); - } catch { - toast.error("Failed to copy to clipboard"); - } - }; - - const reset = () => { - setCredentials(null); - setHostname(""); - setOrgId(""); - setUseOrg(false); - setCopied(false); - setLoading(false); - sessionStorage.removeItem(HOSTNAME_KEY); - }; - - const isLocalhost = hostname.trim().toLowerCase() === "localhost"; - - if (credentials || loading) { - return ( -
- - - - - - GitHub App Credentials - - -
-

- Copy the following environment variables into your{" "} - .env file. -

- - {loading && ( -
- - Retrieving credentials... - -
- )} - - {!loading && credentials && ( - <> - {isLocalhost && ( - - - Update webhook URL - - The webhook URL was set to a placeholder because GitHub - cannot reach localhost. Use a tunnel (e.g., ngrok) and{" "} - - update the webhook URL in the GitHub App settings - - . - - - )} - -