diff --git a/.github/workflows/build-runtime.yml b/.github/workflows/build-runtime.yml index 8aba508..997c39f 100644 --- a/.github/workflows/build-runtime.yml +++ b/.github/workflows/build-runtime.yml @@ -2,19 +2,6 @@ name: Build Runtime Image on: workflow_dispatch: - pull_request: - branches: [main, master] - types: [opened, synchronize, reopened] - paths: - - "runtime/**" - - ".github/workflows/build-runtime.yml" - - "!runtime/*.md" - push: - branches: [main, master] - paths: - - "runtime/**" - - ".github/workflows/build-runtime.yml" - - "!runtime/*.md" permissions: pull-requests: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..0c98dcd --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,51 @@ +name: CI + +on: + pull_request: + branches: [main] + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +env: + NODE_VERSION: "22.12.0" + PNPM_VERSION: "10.20.0" + NEXT_TELEMETRY_DISABLED: 1 + SKIP_ENV_VALIDATION: 1 + +jobs: + lint-test-build: + name: Lint, Test, Build + runs-on: ubuntu-latest + + 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 lint + + - name: Run tests + run: pnpm test + + - name: Build project + run: pnpm build diff --git a/.github/workflows/docker-build-push.yml b/.github/workflows/docker-build-push.yml deleted file mode 100644 index e0d176d..0000000 --- a/.github/workflows/docker-build-push.yml +++ /dev/null @@ -1,295 +0,0 @@ -name: Docker Build and Push - -on: - workflow_dispatch: - pull_request: - branches: [main, master] - types: [opened, synchronize, reopened] - paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" - - "**/*.jsx" - - "Dockerfile" - - "package*.json" - - "pnpm-lock.yaml" - - "prisma/**" - - ".github/workflows/docker-build-push.yml" - - "!**/*.md" - push: - branches: [main, master] - paths: - - "**/*.ts" - - "**/*.tsx" - - "**/*.js" - - "**/*.jsx" - - "Dockerfile" - - "package*.json" - - "pnpm-lock.yaml" - - "prisma/**" - - ".github/workflows/docker-build-push.yml" - - "!**/*.md" - -permissions: - pull-requests: write - packages: write - contents: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -env: - DOCKERHUB_USERNAME: ${{ vars.DOCKERHUB_USERNAME }} - -jobs: - build-images: - name: Build Docker Images - permissions: - packages: write - strategy: - matrix: - include: - - arch: amd64 - runs-on: ubuntu-24.04 - - arch: arm64 - runs-on: ubuntu-24.04-arm - runs-on: ${{ matrix.runs-on }} - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Convert repository owner to lowercase - id: repo-owner - run: | - echo "lowercase=${GITHUB_REPOSITORY_OWNER@L}" >> $GITHUB_OUTPUT - - - name: Set up QEMU - if: ${{ matrix.arch != runner.arch }} - 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 != 'pull_request' && github.actor != 'dependabot[bot]' && env.DOCKERHUB_USERNAME != '' }} - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ steps.repo-owner.outputs.lowercase }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent - ${{ env.DOCKERHUB_USERNAME && format('docker.io/{0}/fullstack-agent', env.DOCKERHUB_USERNAME) || '' }} - - - name: Build for ${{ matrix.arch }} - id: docker-build - uses: docker/build-push-action@v6 - with: - context: . - file: ./Dockerfile - labels: ${{ steps.meta.outputs.labels }} - platforms: linux/${{ matrix.arch }} - # PR builds: load locally for validation, Push builds: push by digest - push: false - load: ${{ github.event_name == 'pull_request' }} - outputs: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' && format('type=image,"name=ghcr.io/{0}/fullstack-agent{1}",name-canonical=true,push-by-digest=true,push=true', steps.repo-owner.outputs.lowercase, env.DOCKERHUB_USERNAME && format(',docker.io/{0}/fullstack-agent', env.DOCKERHUB_USERNAME) || '') || '' }} - cache-from: type=gha,scope=build-${{ matrix.arch }} - cache-to: type=gha,mode=max,scope=build-${{ matrix.arch }} - - - name: Export digest - if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} - run: | - mkdir -p ${{ runner.temp }}/digests - digest="${{ steps.docker-build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} - uses: actions/upload-artifact@v4 - with: - name: digests-${{ matrix.arch }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - - name: Comment on PR - if: github.event_name == 'pull_request' && matrix.arch == 'amd64' && always() - uses: actions/github-script@v7 - continue-on-error: true - with: - script: | - const buildSuccess = '${{ steps.docker-build.outcome }}' === 'success'; - const emoji = buildSuccess ? '✅' : '❌'; - const status = buildSuccess ? 'Success' : 'Failed'; - - let body = `## ${emoji} FullStack Agent Docker Build ${status}\n\n`; - body += `### Build Details\n\n`; - body += `| Item | Value |\n`; - body += `|------|-------|\n`; - body += `| Build Status | ${buildSuccess ? '✅ Passed' : '❌ Failed'} |\n`; - body += `| Platforms | linux/amd64 (PR validation) |\n`; - body += `| Push to Registry | ⚠️ No (PR build only) |\n`; - body += `| Framework | Next.js 16 + Prisma |\n`; - body += `| Base Image | node:current-alpine |\n\n`; - - if (buildSuccess) { - body += `### 📦 Multi-arch images will be published after merge\n\n`; - body += `**Note**: PR builds only verify the Docker build process for linux/amd64. `; - body += `Multi-platform images (amd64 + arm64) are built and pushed to registries only when merged to main.\n\n`; - body += `**Registries**:\n`; - body += `- GitHub Container Registry: \`ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent\`\n`; - if ('${{ env.DOCKERHUB_USERNAME }}') { - body += `- Docker Hub: \`docker.io/${{ env.DOCKERHUB_USERNAME }}/fullstack-agent\`\n`; - } - } else { - body += `### ❌ Build Failed\n\n`; - body += `Please check the workflow logs for detailed error information.\n`; - } - - body += `\n---\n`; - body += `**Commit**: \`${{ github.sha }}\`\n`; - body += `**Triggered by**: @${{ github.actor }}\n`; - - try { - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const botComment = comments.find(comment => - comment.user.type === 'Bot' && - comment.body.includes('FullStack Agent Docker Build') - ); - - if (botComment) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: botComment.id, - body: body - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body: body - }); - } - } catch (error) { - console.log('Failed to post comment:', error.message); - console.log('This might be expected for PRs from forks'); - } - - release-images: - name: Push Multi-Arch Docker Images - permissions: - packages: write - needs: build-images - runs-on: ubuntu-24.04 - if: ${{ github.event_name != 'pull_request' && github.actor != 'dependabot[bot]' }} - - steps: - - name: Convert repository owner to lowercase - id: repo-owner - run: | - echo "lowercase=${GITHUB_REPOSITORY_OWNER@L}" >> $GITHUB_OUTPUT - - - name: Login to Docker Hub - if: ${{ env.DOCKERHUB_USERNAME != '' }} - uses: docker/login-action@v3 - with: - username: ${{ vars.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Login to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ steps.repo-owner.outputs.lowercase }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: ${{ runner.temp }}/digests - pattern: digests-* - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: | - ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent - ${{ env.DOCKERHUB_USERNAME && format('docker.io/{0}/fullstack-agent', env.DOCKERHUB_USERNAME) || '' }} - tags: | - type=ref,event=branch - type=ref,event=tag - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=semver,pattern={{major}} - type=sha,prefix=sha- - type=raw,value=latest,enable=${{ github.ref == format('refs/heads/{0}', 'main') || github.ref == format('refs/heads/{0}', 'master') }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - for TAG in $DOCKER_METADATA_OUTPUT_TAGS; do - docker buildx imagetools create -t $TAG \ - $(printf 'ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent@sha256:%s ' *) - sleep 3 - done - - - name: Inspect image - run: | - docker buildx imagetools inspect ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent:${{ steps.meta.outputs.version }} - - - name: Generate build summary - if: always() - run: | - echo "## 🚀 Multi-Architecture Docker Build & Push Report" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### Build Status" >> $GITHUB_STEP_SUMMARY - if [ "${{ job.status }}" = "success" ]; then - echo "- ✅ Multi-architecture build successful" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Platforms: \`linux/amd64\`, \`linux/arm64\`" >> $GITHUB_STEP_SUMMARY - echo "- ✅ Pushed to GitHub Container Registry: \`ghcr.io/${{ steps.repo-owner.outputs.lowercase }}/fullstack-agent\`" >> $GITHUB_STEP_SUMMARY - if [ -n "${{ env.DOCKERHUB_USERNAME }}" ]; then - echo "- ✅ Pushed to Docker Hub: \`docker.io/${{ env.DOCKERHUB_USERNAME }}/fullstack-agent\`" >> $GITHUB_STEP_SUMMARY - fi - else - echo "- ❌ Build or push 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 diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml deleted file mode 100644 index 9b3b4a8..0000000 --- a/.github/workflows/pr-check.yml +++ /dev/null @@ -1,293 +0,0 @@ -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.12.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 - SKIP_ENV_VALIDATION: 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 - filter all matches, then find non-skipped one - const lintBuilds = checkRuns.check_runs.filter(run => - run.name === 'Lint and Build Check' - ); - const dockerBuilds = checkRuns.check_runs.filter(run => - run.name === 'Docker Build Test' - ); - - // Prefer non-skipped check runs, fallback to first one if all are skipped - const lintBuild = lintBuilds.find(run => run.conclusion !== 'skipped') || lintBuilds[0]; - const dockerBuild = dockerBuilds.find(run => run.conclusion !== 'skipped') || dockerBuilds[0]; - - 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; - - // Security: Sanitize branch name for safe display in Markdown - const safeBranchName = branchName.replace(/[`\[\]]/g, '\\$&'); - - 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:** \`${safeBranchName}\`\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/.github/workflows/release-image.yml b/.github/workflows/release-image.yml new file mode 100644 index 0000000..2cafc4b --- /dev/null +++ b/.github/workflows/release-image.yml @@ -0,0 +1,73 @@ +name: Release Image + +on: + release: + types: [published] + +permissions: + contents: read + packages: write + +concurrency: + group: ${{ github.workflow }}-${{ github.event.release.tag_name }} + cancel-in-progress: false + +env: + IMAGE_NAME: ghcr.io/fullagent/fulling + +jobs: + publish-image: + name: Publish GHCR Image + runs-on: ubuntu-24.04 + + steps: + - name: Checkout release tag + uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ github.event.release.tag_name }} + + - name: Resolve release commit + id: release-commit + run: | + commit_sha="$(git rev-list -n 1 "${{ github.event.release.tag_name }}")" + short_sha="$(git rev-parse --short "$commit_sha")" + echo "sha=$commit_sha" >> "$GITHUB_OUTPUT" + echo "short_sha=$short_sha" >> "$GITHUB_OUTPUT" + + - name: Verify release commit is on main + run: | + git fetch origin main + git merge-base --is-ancestor "${{ steps.release-commit.outputs.sha }}" origin/main + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.IMAGE_NAME }} + tags: | + type=ref,event=tag + type=raw,value=sha-${{ steps.release-commit.outputs.short_sha }} + type=raw,value=latest,enable=${{ !github.event.release.prerelease }} + + - name: Build and push image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + platforms: linux/amd64,linux/arm64 + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha,scope=release-image + cache-to: type=gha,mode=max,scope=release-image