diff --git a/.env.template b/.env.template index 8e6edfb..dd5be70 100644 --- a/.env.template +++ b/.env.template @@ -3,6 +3,7 @@ DATABASE_URL="" # NextAuth Configuration NEXTAUTH_URL="" NEXTAUTH_SECRET="" +AUTH_TRUST_HOST="true" # GitHub OAuth (replace with your actual values) GITHUB_CLIENT_ID="" diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml new file mode 100644 index 0000000..fc68c70 --- /dev/null +++ b/.github/workflows/docker-build-push.yml @@ -0,0 +1,123 @@ +name: Docker Build and Push + +permissions: + contents: read + packages: write + +on: + push: + branches: [main, master] + workflow_dispatch: + inputs: + push_to_registry: + description: "Push to Docker registry" + required: false + default: true + type: boolean + +env: + DOCKER_IMAGE: ${{ vars.DOCKERHUB_USERNAME || 'defaultuser' }}/fullstack-agent + REGISTRY_GHCR: ghcr.io + +jobs: + build-and-push: + name: Build and Push Docker Image + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push_to_registry) + uses: docker/login-action@v3 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Login to GitHub Container Registry + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push_to_registry) + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY_GHCR }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ${{ env.DOCKER_IMAGE }} + ${{ env.REGISTRY_GHCR }}/${{ github.repository }} + tags: | + type=ref,event=branch + type=sha,prefix=sha- + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + labels: | + org.opencontainers.image.title=FullStack Agent + org.opencontainers.image.description=Full Stack Development Agent + org.opencontainers.image.vendor=${{ github.repository_owner }} + + - name: Build and push multi-platform image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && inputs.push_to_registry) }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: | + type=gha,scope=build-amd64 + type=gha,scope=build-arm64 + cache-to: type=gha,mode=max,scope=build-multiplatform + provenance: true + sbom: true + + - name: Generate build summary + if: always() + run: | + echo "## ๐Ÿš€ Docker Build & Push Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Build Status" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" = "success" ]; then + echo "- โœ… Multi-platform build successful" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Platforms: \`linux/amd64\`, \`linux/arm64\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ github.event_name }}" = "push" ] || [ "${{ inputs.push_to_registry }}" = "true" ]; then + echo "- โœ… Pushed to Docker Hub: \`${{ env.DOCKER_IMAGE }}\`" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Pushed to GHCR: \`${{ env.REGISTRY_GHCR }}/${{ github.repository }}\`" >> $GITHUB_STEP_SUMMARY + fi + else + echo "- โŒ Build failed" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Build Information" >> $GITHUB_STEP_SUMMARY + echo "- **Commit SHA**: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Triggered by**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + echo "- **Event**: \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Build time**: $(date '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Image Tags" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "${{ steps.meta.outputs.tags }}" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + + - name: Test image (quick smoke test) + if: success() + run: | + echo "### ๐Ÿงช Image Test" >> $GITHUB_STEP_SUMMARY + echo "Running quick smoke test on built image..." >> $GITHUB_STEP_SUMMARY + # Pull the image we just built (from cache/local) + docker images | head -n 5 + echo "" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Image built successfully and available locally" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 0000000..4bbba9d --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,285 @@ +name: PR Check + +# Workflow-level permissions (for pull_request events) +permissions: + contents: read + +on: + pull_request: + branches: [main, master, develop] + types: [opened, synchronize, reopened] + # Dedicated trigger for PR comments (with write permissions) + pull_request_target: + branches: [main, master, develop] + types: [opened, synchronize, reopened] + +env: + NODE_VERSION: "22.9.0" + PNPM_VERSION: "10.20.0" + +jobs: + lint-and-build: + name: Lint and Build Check + runs-on: ubuntu-latest + # Only run on pull_request events (execute code checks) + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: ${{ env.PNPM_VERSION }} + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: "pnpm" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run linter + run: pnpm run lint + continue-on-error: false + + - name: Build project + run: pnpm run build + continue-on-error: false + env: + NEXT_TELEMETRY_DISABLED: 1 + + - name: Generate build summary + if: always() + run: | + echo "## ๐Ÿ—๏ธ Build & Lint Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Build Status" >> $GITHUB_STEP_SUMMARY + echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" = "success" ]; then + echo "| Lint | โœ… Passed |" >> $GITHUB_STEP_SUMMARY + echo "| Build | โœ… Passed |" >> $GITHUB_STEP_SUMMARY + else + echo "| Lint | โŒ Failed |" >> $GITHUB_STEP_SUMMARY + echo "| Build | โŒ Failed |" >> $GITHUB_STEP_SUMMARY + fi + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Details" >> $GITHUB_STEP_SUMMARY + echo "- **Commit**: \`${{ github.event.pull_request.head.sha }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Branch**: \`${{ github.head_ref }}\`" >> $GITHUB_STEP_SUMMARY + echo "- **Author**: @${{ github.actor }}" >> $GITHUB_STEP_SUMMARY + + docker-build-test: + name: Docker Build Test + runs-on: ubuntu-latest + needs: lint-and-build + # Only run on pull_request events (execute Docker build tests) + if: github.event_name == 'pull_request' + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: fullstack-agent + tags: | + type=ref,event=pr + type=sha,prefix=sha- + + - name: Build Docker image (AMD64 only for PR) + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64 + push: false + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=pr-amd64 + cache-to: type=gha,mode=max,scope=pr-amd64 + provenance: false + sbom: false + + - name: Generate Docker build summary + if: always() + run: | + echo "## ๐Ÿณ Docker Build Report" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Build Status" >> $GITHUB_STEP_SUMMARY + if [ "${{ job.status }}" = "success" ]; then + echo "- โœ… Docker image build successful" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Platform: \`linux/amd64\`" >> $GITHUB_STEP_SUMMARY + echo "- โœ… Cache optimization enabled" >> $GITHUB_STEP_SUMMARY + else + echo "- โŒ Docker image build failed" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Troubleshooting Tips" >> $GITHUB_STEP_SUMMARY + echo "- Check Dockerfile syntax" >> $GITHUB_STEP_SUMMARY + echo "- Verify dependency versions" >> $GITHUB_STEP_SUMMARY + echo "- Review build context" >> $GITHUB_STEP_SUMMARY + fi + + # ๐Ÿ”’ Safe PR comment job (uses pull_request_target, but does not checkout external code) + pr-comment: + name: Comment PR Results + runs-on: ubuntu-latest + # Only run on pull_request_target events (with write permissions) + if: github.event_name == 'pull_request_target' + # No 'needs' dependency - we'll wait for checks via API + permissions: + issues: write + pull-requests: write + actions: read + + steps: + # ๐Ÿ”’ IMPORTANT: Do not checkout any code, especially PR code! + # ๐Ÿ”’ Only use GitHub API to retrieve workflow run results + + - name: Wait for checks to complete + uses: actions/github-script@v7 + id: wait-checks + with: + script: | + const { owner, repo } = context.repo; + const headSha = context.payload.pull_request.head.sha; + console.log(`Waiting for checks on commit ${headSha} to complete...`); + + // Wait for maximum 10 minutes + const maxWaitTime = 10 * 60 * 1000; + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime) { + try { + const { data: checkRuns } = await github.rest.checks.listForRef({ + owner, + repo, + ref: headSha, + }); + + // Find our checks + const lintBuild = checkRuns.check_runs.find(run => + run.name === 'Lint and Build Check' + ); + const dockerBuild = checkRuns.check_runs.find(run => + run.name === 'Docker Build Test' + ); + + if (lintBuild && dockerBuild) { + console.log(`Lint & Build: ${lintBuild.status}, Docker Build: ${dockerBuild.status}`); + + if (lintBuild.status === 'completed' && dockerBuild.status === 'completed') { + console.log(`Checks completed - Lint: ${lintBuild.conclusion}, Docker: ${dockerBuild.conclusion}`); + return { + completed: true, + lintConclusion: lintBuild.conclusion, + dockerConclusion: dockerBuild.conclusion, + lintUrl: lintBuild.details_url, + dockerUrl: dockerBuild.details_url + }; + } + } + + await new Promise(resolve => setTimeout(resolve, 30000)); + } catch (error) { + console.log(`Error fetching check status: ${error.message}`); + await new Promise(resolve => setTimeout(resolve, 30000)); + } + } + + return { + completed: false, + lintConclusion: 'timed_out', + dockerConclusion: 'timed_out' + }; + + - name: Comment on PR + uses: actions/github-script@v7 + with: + script: | + const checkResult = ${{ steps.wait-checks.outputs.result }}; + const prNumber = context.payload.pull_request.number; + const commitSha = context.payload.pull_request.head.sha; + const branchName = context.payload.pull_request.head.ref; + + let allPassed = checkResult.lintConclusion === 'success' && checkResult.dockerConclusion === 'success'; + let emoji = allPassed ? 'โœ…' : 'โŒ'; + let status = allPassed ? 'Passed' : 'Failed'; + + let body = `## ${emoji} PR Check Results: ${status}\n\n`; + body += `### Build Checks\n\n`; + body += `| Check | Status |\n`; + body += `|-------|--------|\n`; + body += `| Lint & Build | ${checkResult.lintConclusion === 'success' ? 'โœ… Passed' : 'โŒ Failed'} |\n`; + body += `| Docker Build | ${checkResult.dockerConclusion === 'success' ? 'โœ… Passed' : 'โŒ Failed'} |\n\n`; + + if (allPassed) { + body += `### โœจ Great work!\n\n`; + body += `All checks passed successfully. Your PR is ready for review.\n\n`; + body += `**Details:**\n`; + body += `- โœ… Code quality verified (linting passed)\n`; + body += `- โœ… Build successful\n`; + body += `- โœ… Docker image build verified (linux/amd64)\n`; + } else { + body += `### โš ๏ธ Action Required\n\n`; + body += `Some checks failed. Please review the errors and update your PR.\n\n`; + if (checkResult.lintConclusion !== 'success') { + body += `**Lint/Build Issues:**\n`; + body += `- Check the "Lint and Build Check" job for details\n`; + body += `- Fix linting errors with \`pnpm run lint:fix\`\n`; + body += `- Ensure the project builds locally with \`pnpm run build\`\n\n`; + } + if (checkResult.dockerConclusion !== 'success') { + body += `**Docker Build Issues:**\n`; + body += `- Check the "Docker Build Test" job for details\n`; + body += `- Verify Dockerfile changes\n`; + body += `- Test Docker build locally\n\n`; + } + } + + body += `**Commit:** \`${commitSha}\`\n`; + body += `**Branch:** \`${branchName}\`\n`; + + if (checkResult.lintUrl || checkResult.dockerUrl) { + body += `\n**๐Ÿ”— View Details:**\n`; + if (checkResult.lintUrl) body += `- [Lint & Build](${checkResult.lintUrl})\n`; + if (checkResult.dockerUrl) body += `- [Docker Build](${checkResult.dockerUrl})\n`; + } + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('PR Check Results') + ); + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: body + }); + console.log('โœ… Successfully updated existing PR comment'); + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: body + }); + console.log('โœ… Successfully created new PR comment'); + } diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 1eb9efe..9dcffd3 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -71,7 +71,7 @@ export const POST = withAuth(async (req, _context, session) // Generate K8s compatible names const k8sProjectName = KubernetesUtils.toK8sProjectName(name) - const randomSuffix = KubernetesUtils.generateRandomString(8) + const randomSuffix = KubernetesUtils.generateRandomString() const databaseName = `${k8sProjectName}-${randomSuffix}` const sandboxName = `${k8sProjectName}-${randomSuffix}` diff --git a/lib/auth.ts b/lib/auth.ts index c04f3ac..0ef0b03 100644 --- a/lib/auth.ts +++ b/lib/auth.ts @@ -5,6 +5,7 @@ import GitHub from 'next-auth/providers/github' import { prisma } from '@/lib/db' import { isJWTExpired, parseSealosJWT } from '@/lib/jwt' +import { updateUserKubeconfig } from '@/lib/k8s/k8s-service-helper' import { logger as baseLogger } from '@/lib/logger' import { createAiproxyToken } from '@/lib/services/aiproxy' @@ -161,25 +162,9 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }, }) - // Update KUBECONFIG in UserConfig - await prisma.userConfig.upsert({ - where: { - userId_key: { - userId: existingIdentity.user.id, - key: 'KUBECONFIG', - }, - }, - create: { - userId: existingIdentity.user.id, - key: 'KUBECONFIG', - value: sealosKubeconfig, - category: 'kc', - isSecret: true, - }, - update: { - value: sealosKubeconfig, - }, - }) + // Update KUBECONFIG in UserConfig using helper function + // This will automatically clear the cached service instance + await updateUserKubeconfig(existingIdentity.user.id, sealosKubeconfig) // Create aiproxy token try { @@ -252,20 +237,13 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ logger.error(`Failed to create aiproxy token for new user ${sealosUserId}: ${error}`) } - // Prepare configs array + // Prepare configs array (excluding KUBECONFIG, which will be set via updateUserKubeconfig) const configs: Array<{ key: string value: string category: string isSecret: boolean - }> = [ - { - key: 'KUBECONFIG', - value: sealosKubeconfig, - category: 'kc', - isSecret: true, - }, - ] + }> = [] // Add aiproxy configs if token was created successfully if (aiproxyTokenInfo?.token?.key && aiproxyTokenInfo.anthropicBaseUrl) { @@ -303,6 +281,10 @@ export const { handlers, signIn, signOut, auth } = NextAuth({ }, }) + // Set KUBECONFIG using helper function + // This will automatically clear the cached service instance + await updateUserKubeconfig(newUser.id, sealosKubeconfig) + return { id: newUser.id, name: newUser.name || sealosUserId, diff --git a/lib/k8s/kubernetes-utils.ts b/lib/k8s/kubernetes-utils.ts index 191e98e..0d916ce 100644 --- a/lib/k8s/kubernetes-utils.ts +++ b/lib/k8s/kubernetes-utils.ts @@ -7,7 +7,7 @@ const logger = baseLogger.child({ module: 'lib/k8s/kubernetes-utils' }) // Create nanoid generator with lowercase letters only for k8s resource name compatibility // nanoid uses cryptographically secure random source -const nanoidLowercase = customAlphabet('abcdefghijklmnopqrstuvwxyz') +const nanoidLowercase = customAlphabet('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ') export class KubernetesUtils { /** @@ -56,7 +56,7 @@ export class KubernetesUtils { * @param length - Length of the random string (default: 8) * @returns Random string containing only lowercase letters */ - static generateRandomString(length: number = 8): string { + static generateRandomString(length: number = 12): string { return nanoidLowercase(length) } diff --git a/package.json b/package.json index b9ff07e..4526c7d 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,10 @@ "scripts": { "prepare": "prisma generate", "dev": "next dev -H 0.0.0.0 -p 3000 --turbo", - "build": "next build", + "build": "prisma generate && next build", "start": "next start -H 0.0.0.0 -p 3000", - "lint": "eslint", + "lint": "eslint .", + "lint:fix": "eslint . --fix", "prisma:format": "prisma format" }, "dependencies": {