diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000..315cab5 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,25 @@ +dependencies: + - changed-files: + - any-glob-to-any-file: + - "**/package.json" + - "**/package-lock.json" + - "**/pnpm-lock.yaml" + - "**/yarn.lock" + - "**/requirements*.txt" + - "**/poetry.lock" + - "**/go.mod" + - "**/go.sum" + +javascript: + - changed-files: + - any-glob-to-any-file: + - "**/*.js" + - "**/*.jsx" + - "**/*.ts" + - "**/*.tsx" + +documentation: + - changed-files: + - any-glob-to-any-file: + - "**/*.md" + - "docs/**" diff --git a/.github/workflows/chromium-pipeline.yml b/.github/workflows/chromium-pipeline.yml new file mode 100644 index 0000000..ec9516d --- /dev/null +++ b/.github/workflows/chromium-pipeline.yml @@ -0,0 +1,224 @@ +name: Chromium Pipeline – Docker Build & Artifacts + +on: + push: + branches: ["master"] + paths: + - "src/**" + - "webapp/**" + - "marketing-site/**" + - ".github/workflows/chromium-pipeline.yml" + pull_request: + branches: ["master"] + paths: + - "src/**" + - "webapp/**" + - "marketing-site/**" + - ".github/workflows/chromium-pipeline.yml" + workflow_dispatch: + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/pinkflow-app + +permissions: + contents: read + packages: write + +concurrency: + group: chromium-pipeline-${{ github.ref }} + cancel-in-progress: true + +jobs: + # ──────────────────────────────────────────────── + # 1. Chromium headless smoke-test + # ──────────────────────────────────────────────── + chromium-check: + name: Chromium Headless Check + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Chromium + run: | + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends chromium-browser + + - name: Verify Chromium version + run: chromium-browser --version + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + + - name: Install Puppeteer (headless Chromium driver) + run: | + npm install puppeteer-core + + - name: Run headless Chromium smoke test + run: | + node - <<'EOF' 2>&1 | tee /tmp/chromium-smoke.log + const fs = require('fs'); + const puppeteer = require('puppeteer-core'); + const candidates = [ + process.env.CHROME_BIN, + '/usr/bin/chromium-browser', + '/usr/bin/chromium', + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + ].filter(Boolean); + const executablePath = candidates.find((p) => fs.existsSync(p)); + if (!executablePath) { + throw new Error(`Chromium executable not found. Tried: ${candidates.join(', ')}`); + } + (async () => { + const browser = await puppeteer.launch({ + executablePath, + args: ['--no-sandbox', '--disable-setuid-sandbox', '--headless=new'], + }); + const page = await browser.newPage(); + await page.goto('about:blank'); + const title = await page.title(); + console.log('Chromium smoke test OK – page title:', title); + await browser.close(); + })(); + EOF + + - name: Upload Chromium test log + if: always() + uses: actions/upload-artifact@v4 + with: + name: chromium-smoke-log-${{ github.run_id }} + path: /tmp/chromium-*.log + if-no-files-found: ignore + retention-days: 7 + + # ──────────────────────────────────────────────── + # 2. Discover deeply nested source artifacts + # ──────────────────────────────────────────────── + discover-artifacts: + name: Discover Deep Nested Source Files + runs-on: ubuntu-latest + outputs: + file_list: ${{ steps.scan.outputs.file_list }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Scan for deeply nested files (depth ≥ 5) + id: scan + run: | + echo "=== Deeply nested files (depth >= 5) ===" + find . -mindepth 5 \ + \( -path '*/.git' -o -path '*/node_modules' -o -path '*/.next' -o -path '*/dist' -o -path '*/build' -o -path '*/target' -o -path '*/.cache' \) -prune -o \ + -type f -print \ + | sort > /tmp/deep-nested-files.txt + cat /tmp/deep-nested-files.txt + COUNT=$(wc -l < /tmp/deep-nested-files.txt | tr -d ' ') + echo "Total: $COUNT files at depth >= 5" + { + echo "file_list<> "$GITHUB_OUTPUT" + + - name: Upload deep-file discovery report + uses: actions/upload-artifact@v4 + with: + name: deep-nested-files-${{ github.run_id }} + path: /tmp/deep-nested-files.txt + retention-days: 14 + + # ──────────────────────────────────────────────── + # 3. Docker image build + # ──────────────────────────────────────────────── + docker-build: + name: Docker Image Build + runs-on: ubuntu-latest + needs: [chromium-check] + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + if: github.event_name == 'push' && github.ref == 'refs/heads/master' + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=sha,format=short,prefix=sha- + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/master' }} + + - name: Build Docker image + id: docker-build + uses: docker/build-push-action@v6 + with: + context: . + file: marketing-site/Dockerfile + push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} + load: ${{ github.event_name == 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Compute short SHA + if: github.event_name == 'pull_request' + id: short-sha + run: echo "value=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT + + - name: Export image as tar artifact (PRs only) + if: github.event_name == 'pull_request' + run: | + IMAGE_TAG="${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ steps.short-sha.outputs.value }}" + if docker image inspect "$IMAGE_TAG" > /dev/null 2>&1; then + docker save "$IMAGE_TAG" -o /tmp/pinkflow-image.tar + else + echo "Expected image $IMAGE_TAG not found; skipping export." + exit 1 + fi + + - name: Upload image artifact (PRs only) + if: github.event_name == 'pull_request' + uses: actions/upload-artifact@v4 + with: + name: docker-image-${{ github.run_id }} + path: /tmp/pinkflow-image.tar + retention-days: 3 + + # ──────────────────────────────────────────────── + # 4. Pipeline summary + # ──────────────────────────────────────────────── + pipeline-summary: + name: Pipeline Summary + runs-on: ubuntu-latest + needs: [chromium-check, discover-artifacts, docker-build] + if: always() + steps: + - name: Write job summary + run: | + echo "## Chromium Pipeline Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY + echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY + echo "| Chromium Headless Check | ${{ needs.chromium-check.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Discover Deep Nested Files | ${{ needs.discover-artifacts.result }} |" >> $GITHUB_STEP_SUMMARY + echo "| Docker Image Build | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`${{ github.ref_name }}\` · SHA: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml index 4613569..323a315 100644 --- a/.github/workflows/label.yml +++ b/.github/workflows/label.yml @@ -17,6 +17,81 @@ jobs: pull-requests: write steps: - - uses: actions/labeler@v4 - with: - repo-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Fetch labeler config from pull request + uses: actions/github-script@v7 + with: + script: | + const core = require('@actions/core'); + const fs = require('fs'); + const path = require('path'); + const configPath = '.github/labeler.yml'; + const pullRequest = context.payload.pull_request; + const headRepo = pullRequest.head.repo; + const isSameRepositoryPullRequest = headRepo && headRepo.full_name === `${context.repo.owner}/${context.repo.repo}`; + const sources = [ + ...(isSameRepositoryPullRequest ? [{ + owner: headRepo.owner.login, + repo: headRepo.name, + ref: pullRequest.head.sha, + }] : []), + { + owner: context.repo.owner, + repo: context.repo.repo, + ref: pullRequest.base.sha, + }, + ]; + + let configContent; + + for (const source of sources) { + let response; + + try { + response = await github.rest.repos.getContent({ + owner: source.owner, + repo: source.repo, + path: configPath, + ref: source.ref, + }); + } catch (error) { + if (error.status === 404) { + continue; + } + + throw new Error(`Failed to load ${configPath} from ${source.owner}/${source.repo}@${source.ref}: ${error.message}`); + } + + let reason; + + if (Array.isArray(response.data)) { + reason = 'received a directory listing'; + } else if (response.data.type !== 'file') { + reason = `received type "${response.data.type}"`; + } else if (response.data.content === undefined) { + reason = 'received no file content'; + } + + if (reason) { + throw new Error(`Unable to read ${configPath} at ${source.owner}/${source.repo}@${source.ref}: ${reason}.`); + } + + try { + configContent = Buffer.from(response.data.content, response.data.encoding).toString('utf8'); + } catch (error) { + throw new Error(`Unable to decode ${configPath} at ${source.owner}/${source.repo}@${source.ref} using ${response.data.encoding}: ${error.message}`); + } + + break; + } + + if (!configContent) { + core.setFailed(`Unable to load ${configPath} from the pull request head or base branch.`); + return; + } + + fs.mkdirSync(path.dirname(configPath), { recursive: true }); + fs.writeFileSync(configPath, configContent); + + - uses: actions/labeler@v4 + with: + repo-token: "${{ secrets.GITHUB_TOKEN }}" diff --git a/.github/workflows/pr-security.yml b/.github/workflows/pr-security.yml index fee2d59..7667c42 100644 --- a/.github/workflows/pr-security.yml +++ b/.github/workflows/pr-security.yml @@ -22,6 +22,7 @@ jobs: with: node-version: '20' cache: 'npm' + cache-dependency-path: marketing-site/package-lock.json - name: Check for new dependencies id: deps @@ -40,6 +41,7 @@ jobs: fi - name: Install dependencies + working-directory: marketing-site run: npm ci - name: Run dependency audit