ci: Make claude-based smart dependency-bumper #4880
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Validate | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| permissions: | |
| contents: read | |
| statuses: write | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| env: | |
| DOCKER_APP_IMAGE_NAME: 'ghcr.io/hasadna/open-bus-map-search/open-bus-map-search' | |
| DOCKER_APP_IMAGE_TAG: 'latest' | |
| APPLITOOLS_API_KEY: ${{ secrets.APPLITOOLS_API_KEY }} | |
| # see: https://applitools.com/docs/eyes/integrations/ci-cd/github-actions#configuring-the-applitools-cicd-integration | |
| APPLITOOLS_BATCH_ID: ${{ github.event.pull_request.head.sha || github.sha }} | |
| APPLITOOLS_LOG_DIR: ./logs | |
| APPLITOOLS_SHOW_LOGS: true | |
| APPLITOOLS_MASK_LOG: true # mask the API key (and tokens) in Applitools logs | |
| APPLITOOLS_MASK_VALUES: ${{ secrets.APPLITOOLS_API_KEY }} # seed the mask set at logger startup so the key is hidden even in the early config dump | |
| jobs: | |
| local-tests: | |
| name: Local Tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'npm' | |
| - name: Check for yarn.json | |
| run: | | |
| if [ -f yarn.json ]; then | |
| echo "::error file=yarn.json::yarn.json is being added" | |
| exit 1 | |
| fi | |
| - name: Ensure package-lock.json exists | |
| run: | | |
| if [ ! -f package-lock.json ]; then | |
| echo "::error file=package-lock.json::package-lock.json is being removed" | |
| exit 1 | |
| fi | |
| - name: Validate package-lock.json is updated | |
| run: | | |
| if ! npm ci; then | |
| echo "::error file=package-lock.json::package-lock.json is not updated" | |
| exit 1 | |
| fi | |
| - name: Run linter | |
| run: npm run lint | |
| - name: Check for circular dependencies | |
| run: npx madge --extensions js,ts --circular . | |
| # Jest gates merges here: this is the only job in CI that runs it. | |
| - name: Run unit tests | |
| run: npm run test:unit | |
| # Should block merge (required status check): a PR must not introduce new vulnerable dependencies. | |
| dependency-review: | |
| name: Dependency Review | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'pull_request' | |
| permissions: | |
| contents: read | |
| # Needed for comment-summary-in-pr; on fork PRs the token stays read-only | |
| # and the action falls back to the job summary instead of a comment. | |
| pull-requests: write | |
| # deleteComment (the success-path cleanup) is gated on Issues:write even for | |
| # comments on a PR; pull-requests:write only covers create/update. | |
| issues: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Review dependency changes introduced by this PR | |
| id: review | |
| uses: actions/dependency-review-action@v5 | |
| with: | |
| fail-on-severity: low | |
| comment-summary-in-pr: on-failure | |
| # Exception policy: a GHSA may be allowlisted below only if | |
| # - severity is low or moderate (never high/critical), AND | |
| # - there is no patched version / alternative package, AND | |
| # - the entry has a justification. | |
| # allow-ghsas: GHSA-xxxx-xxxx-xxxx # <why it's acceptable> | |
| - name: Allowlist help comment | |
| # On failure: post a short how-to. On success: delete both this help comment | |
| # and the action's own summary, so a fixed PR cleans up after itself. | |
| if: always() && github.event_name == 'pull_request' | |
| continue-on-error: true # never let the comment housekeeping fail the gate (e.g. read-only token on fork PRs) | |
| uses: actions/github-script@v8 # v8 runs on node24 (v7 was node20, deprecated) | |
| env: | |
| REVIEW_OUTCOME: ${{ steps.review.outcome }} | |
| with: | |
| script: | | |
| const helpMarker = '<!-- dep-review-allowlist-help -->' | |
| const reviewMarker = '<!-- dependency-review-pr-comment-marker -->' // the action's own sticky marker | |
| const { owner, repo } = context.repo | |
| const issue_number = context.issue.number | |
| const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number }) | |
| const find = (m) => comments.find((c) => c.body && c.body.includes(m)) | |
| if (process.env.REVIEW_OUTCOME === 'failure') { | |
| const body = [ | |
| helpMarker, | |
| '### A dependency this PR adds is flagged 🛑', | |
| '', | |
| 'Best fix: bump it to a patched version so the advisory is gone.', | |
| '', | |
| "If there's no patch and a reviewer approves:", | |
| 'grab the `GHSA-…` id from the advisory link in the summary above (it only lives there),', | |
| 'and add it under `allow-ghsas` in `.github/workflows/validate.yaml`, with a reason why it is OK to allow it:', | |
| '', | |
| '```yaml', | |
| ' allow-ghsas: GHSA-xxxx-xxxx-xxxx # why it is OK', | |
| '```', | |
| '', | |
| 'On your way, feel free to delete any `allow-ghsas` lines that existed before this PR - (they are only needed on PR merge).', | |
| ].join('\n') | |
| const existing = find(helpMarker) | |
| if (existing) await github.rest.issues.updateComment({ owner, repo, comment_id: existing.id, body }) | |
| else await github.rest.issues.createComment({ owner, repo, issue_number, body }) | |
| } else if (process.env.REVIEW_OUTCOME === 'success') { | |
| for (const m of [helpMarker, reviewMarker]) { | |
| const c = find(m) | |
| if (c) await github.rest.issues.deleteComment({ owner, repo, comment_id: c.id }) | |
| } | |
| } | |
| build: | |
| name: Docker Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - name: Generate hash.txt with commit hash | |
| run: echo "$(git rev-parse --short HEAD)" >> public/hash.txt | |
| - name: Setup Docker Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build and export Docker image | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| tags: ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }} | |
| outputs: type=docker, dest=/tmp/docker-image.tar | |
| - name: Upload Docker image artifact | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: docker-image | |
| path: /tmp/docker-image.tar | |
| build-outside-docker: | |
| name: Node.js Build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v6 | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'npm' | |
| - name: Install dependencies | |
| run: npm ci | |
| - name: Build application | |
| run: npm run build | |
| - name: Build Storybook | |
| run: npm run build-storybook | |
| test: | |
| name: Application & Playwright Tests | |
| runs-on: ubuntu-latest | |
| needs: [build, build-outside-docker] | |
| # The Playwright container shares the app container's network namespace | |
| # (--network container:app on an --internal, no-egress network), so it reaches | |
| # nginx at http://localhost/ and genuinely cannot reach the internet — any | |
| # un-mocked external call fails hard, run against the real production build. | |
| # Use localhost, not http://app/: Chrome may HTTPS-upgrade a bare hostname to | |
| # :443 where nginx isn't listening; localhost is exempt from the upgrade. | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Download Docker image artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load -i /tmp/docker-image.tar | |
| - name: Create isolated no-egress network | |
| run: docker network create --internal isolated | |
| - name: Start application container (prod nginx image) | |
| run: docker run -d --name app --network isolated ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }} | |
| - name: Build Playwright runner image | |
| run: docker build -t open-bus-offline-test -f scripts/Dockerfile.offline-test . | |
| - name: Wait for nginx and validate hash.txt (shared netns) | |
| run: | | |
| for i in $(seq 1 20); do | |
| if docker run --rm --network container:app open-bus-offline-test \ | |
| node -e "const http=require('http');http.get('http://localhost/hash.txt',r=>{let b='';r.on('data',d=>b+=d);r.on('end',()=>{const t=r.headers['content-type']||'';if(r.statusCode===200&&/^text\/plain/.test(t)&&b.length<100){console.log('hash.txt OK: 200, '+t+', '+b.length+' bytes');process.exit(0)}console.log('bad: '+r.statusCode+' '+t+' '+b.length);process.exit(1)})}).on('error',e=>{console.log('err '+e.message);process.exit(1)})"; then | |
| echo "app is ready" | |
| exit 0 | |
| fi | |
| echo "waiting for nginx (#$i)" | |
| sleep 1 | |
| done | |
| echo "app did not become ready" | |
| docker logs app || true | |
| exit 1 | |
| - name: Run Playwright e2e against prod image (hard offline) | |
| run: | | |
| mkdir -p playwright-artifact | |
| docker run --rm \ | |
| --network container:app \ | |
| -e CI=true \ | |
| -e TZ=Asia/Jerusalem \ | |
| -e PW_BASE_URL=http://localhost \ | |
| -v "$PWD/playwright-artifact:/app/artifact-out" \ | |
| open-bus-offline-test \ | |
| sh -c 'npx playwright test --grep-invert visual; code=$?; cp -r test-results/. /app/artifact-out/ 2>/dev/null || true; cp -r playwright-report/. /app/artifact-out/ 2>/dev/null || true; touch /app/artifact-out/.keep; exit $code' | |
| - name: Upload Playwright test artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-test | |
| path: playwright-artifact | |
| - name: Show app logs on failure | |
| if: failure() | |
| run: docker logs app || true | |
| storybook-test: | |
| name: Storybook Visual Tests | |
| runs-on: ubuntu-22.04 # not working whit ubuntu 23+ | |
| needs: [build, build-outside-docker] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Configure blocked external hosts | |
| uses: ./.github/actions/block-network | |
| - name: Prepare for Testing | |
| run: | | |
| echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_ENV | |
| - name: Download Docker image artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load -i /tmp/docker-image.tar | |
| - name: Start application container | |
| run: docker run -d -p 3000:80 ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }} | |
| - run: export APPLITOOLS_SHOW_LOGS=true && export APPLITOOLS_LOG_DIR=./logs | |
| - run: npm i -g @applitools/eyes-storybook | |
| - name: Run Storybook visual tests | |
| if: env.APPLITOOLS_API_KEY | |
| env: | |
| APPLITOOLS_BATCH_NAME: open-bus-map-search/${{ github.ref }}/${{ env.SHORT_SHA }}/storybook | |
| run: npm run test:storybook -- --storybook-url http://localhost:3000/storybook/index.html | |
| - name: Upload logs if exists | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: logs-storybook-test | |
| path: logs | |
| if-no-files-found: warn | |
| visual-test: | |
| name: Playwright Visual Tests | |
| runs-on: ubuntu-latest | |
| needs: [build, build-outside-docker] | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| ref: ${{ github.event.pull_request.head.sha }} | |
| - name: Configure blocked external hosts | |
| uses: ./.github/actions/block-network | |
| - name: Prepare for Testing | |
| run: | | |
| echo "SHORT_SHA=$(echo ${{ github.event.pull_request.head.sha }} | cut -c1-7)" >> $GITHUB_ENV | |
| # Visual tests must run against the nginx-served Docker artifact (with the | |
| # production CSP/security headers), not the bare Vite dev server. Start the | |
| # container on :3000 so Playwright's webServer (reuseExistingServer) reuses | |
| # it instead of spinning up `npm start`. Otherwise CSP regressions (e.g. a | |
| # missing connect-src breaking MSW-mocked stories) go undetected. | |
| - name: Download Docker image artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: docker-image | |
| path: /tmp | |
| - name: Load Docker image | |
| run: docker load -i /tmp/docker-image.tar | |
| - name: Start application container | |
| run: docker run -d -p 3000:80 ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }} | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| cache: 'npm' | |
| - name: Install Dependencies and Playwright | |
| run: npm ci | |
| - name: Resolve Playwright cache key inputs | |
| id: playwright-version | |
| run: | | |
| echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT" | |
| echo "imageos=$ImageOS" >> "$GITHUB_OUTPUT" | |
| - name: Cache Playwright browsers | |
| uses: actions/cache@v5 | |
| id: playwright-cache | |
| with: | |
| path: ~/.cache/ms-playwright | |
| key: ${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-version.outputs.imageos }}-playwright-${{ steps.playwright-version.outputs.version }} | |
| - name: Install Playwright browser dependencies | |
| if: steps.playwright-cache.outputs.cache-hit != 'true' | |
| timeout-minutes: 15 | |
| run: npx playwright install chromium | |
| - name: Run Playwright Visual Tests | |
| env: | |
| APPLITOOLS_BATCH_NAME: open-bus-map-search/${{ github.ref }}/${{ env.SHORT_SHA }}/ | |
| PWTEST_CHILD_PROCESS_TIMEOUT: '1800000' # 30 min; default 300000 force-kills slow UFG result collection → false CI failure | |
| run: npm run test:e2e:visual | |
| - name: Prepare Playwright artifact directory | |
| if: always() | |
| run: | | |
| mkdir -p playwright-artifact | |
| if [ -d test-results ]; then cp -r test-results/* playwright-artifact/; fi | |
| if [ -d playwright-report ]; then cp -r playwright-report/* playwright-artifact/; fi | |
| touch playwright-artifact/.keep | |
| - name: Upload Playwright test artifact | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: playwright-visual-test | |
| path: playwright-artifact | |
| - name: Upload logs if exists | |
| if: always() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: logs-visual-test | |
| path: logs | |
| if-no-files-found: warn | |
| publish-test-results: | |
| name: Publish Test Results | |
| runs-on: ubuntu-latest | |
| needs: [test] | |
| if: always() && github.event.repository.fork == false && github.actor != 'dependabot[bot]' | |
| env: | |
| AWS_KEY_ID: ${{ secrets.AWS_KEY_ID }} | |
| steps: | |
| - name: Download Playwright test artifact | |
| uses: actions/download-artifact@v8 | |
| with: | |
| name: playwright-test | |
| path: test-results | |
| - name: Publish test results to S3 | |
| if: always() && env.AWS_KEY_ID != '' | |
| continue-on-error: true | |
| env: | |
| AWS_ACCESS_KEY_ID: ${{ env.AWS_KEY_ID }} | |
| AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} | |
| AWS_REGION: us-east-1 | |
| run: aws s3 cp test-results 's3://noam-gaash.co.il/${{ github.run_id }}/open-bus/${{ github.event.pull_request.head.sha }}/test-results' --recursive --acl public-read --only-show-errors | |
| - name: Set commit status for test results | |
| uses: myrotvorets/set-commit-status-action@master | |
| if: always() && env.AWS_KEY_ID != '' | |
| with: | |
| sha: ${{ github.event.pull_request.head.sha }} | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| description: 'Playwright html report with traces' | |
| status: ${{needs.test.result}} | |
| context: 'Test results (click to see!)' | |
| targetUrl: 'https://s3.amazonaws.com/noam-gaash.co.il/${{ github.run_id }}/open-bus/${{ github.event.pull_request.head.sha }}/test-results/index.html' | |
| close-visual-batches: | |
| name: Close Applitools Visual Test Batches | |
| runs-on: ubuntu-latest | |
| needs: [visual-test, storybook-test] | |
| if: always() && github.event.repository.fork == false && github.actor != 'dependabot[bot]' | |
| steps: | |
| - name: Close Applitools batches | |
| run: | | |
| export APPLITOOLS_SERVER_URL=https://eyesapi.applitools.com | |
| curl -v -X DELETE "$APPLITOOLS_SERVER_URL/api/sessions/batches/$APPLITOOLS_BATCH_ID/close/bypointerid?apiKey=$APPLITOOLS_API_KEY" | |
| all-passed: | |
| name: All Jobs Passed | |
| runs-on: ubuntu-latest | |
| needs: [test, visual-test, storybook-test] | |
| steps: | |
| - name: All jobs passed | |
| run: echo "All passed" |