1818 DOCKER_APP_IMAGE_NAME : ' ghcr.io/hasadna/open-bus-map-search/open-bus-map-search'
1919 DOCKER_APP_IMAGE_TAG : ' latest'
2020 APPLITOOLS_API_KEY : ${{ secrets.APPLITOOLS_API_KEY }}
21- APPLITOOLS_BATCH_ID : ${{ github.event.pull_request.head.sha }}
21+ # see: https://applitools.com/docs/eyes/integrations/ci-cd/github-actions#configuring-the-applitools-cicd-integration
22+ APPLITOOLS_BATCH_ID : ${{ github.event.pull_request.head.sha || github.sha }}
23+ APPLITOOLS_BRANCH : ${{ github.repository }}/${{ github.head_ref || github.ref_name }}
24+ APPLITOOLS_PARENT_BRANCH : ${{ github.repository }}/${{ github.event.pull_request.base.ref || 'main' }}
2225 APPLITOOLS_LOG_DIR : ./logs
2326 APPLITOOLS_SHOW_LOGS : true
2427 APPLITOOLS_MASK_LOG : true # mask the API key (and tokens) in Applitools logs
5659 run : npm run lint
5760 - name : Check for circular dependencies
5861 run : npx madge --extensions js,ts --circular .
62+ # Vitest gates merges here: this is the only job in CI that runs it.
63+ - name : Run unit tests
64+ run : npm run test:unit
5965
6066 # Should block merge (required status check): a PR must not introduce new vulnerable dependencies.
6167 dependency-review :
@@ -168,85 +174,63 @@ jobs:
168174 name : Application & Playwright Tests
169175 runs-on : ubuntu-latest
170176 needs : [build, build-outside-docker]
177+ # The Playwright container shares the app container's network namespace
178+ # (--network container:app on an --internal, no-egress network), so it reaches
179+ # nginx at http://localhost/ and genuinely cannot reach the internet — any
180+ # un-mocked external call fails hard, run against the real production build.
181+ # Use localhost, not http://app/: Chrome may HTTPS-upgrade a bare hostname to
182+ # :443 where nginx isn't listening; localhost is exempt from the upgrade.
171183 steps :
172- - name : Set timezone
173- run : echo "TZ=Asia/Jerusalem" >> $GITHUB_ENV
184+ - uses : actions/checkout@v6
185+ with :
186+ ref : ${{ github.event.pull_request.head.sha }}
174187 - name : Download Docker image artifact
175188 uses : actions/download-artifact@v8
176189 with :
177190 name : docker-image
178191 path : /tmp
179192 - name : Load Docker image
180193 run : docker load -i /tmp/docker-image.tar
181- - name : Start application container
182- run : docker run -d -p 3000:80 ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }}
183- - uses : actions/checkout@v6
184- with :
185- ref : ${{ github.event.pull_request.head.sha }}
186- - uses : actions/setup-node@v6
187- with :
188- node-version : 24
189- cache : ' npm'
190- - name : Configure blocked external hosts
191- uses : ./.github/actions/block-network
192- - name : Validate hash.txt endpoint
194+ - name : Create isolated no-egress network
195+ run : docker network create --internal isolated
196+ - name : Start application container (prod nginx image)
197+ run : docker run -d --name app --network isolated ${{ env.DOCKER_APP_IMAGE_NAME }}:${{ env.DOCKER_APP_IMAGE_TAG }}
198+ - name : Build Playwright runner image
199+ run : docker build -t open-bus-offline-test -f scripts/Dockerfile.offline-test .
200+ - name : Wait for nginx and validate hash.txt (shared netns)
193201 run : |
194- response=$(curl -s -w "%{http_code}" -o /tmp/hash.txt http://localhost:3000/hash.txt)
195- http_code=${response: -3}
196- if [ "$http_code" -ne 200 ]; then
197- echo "Error: HTTP request failed with status code $http_code"
198- exit 1
199- fi
200- mime_type=$(file --mime-type -b /tmp/hash.txt)
201- if [ "$mime_type" != "text/plain" ]; then
202- echo "Error: hash.txt does not have MIME type text/plain. Found: $mime_type"
203- exit 1
204- fi
205- content_length=$(wc -c < /tmp/hash.txt | xargs)
206- if [ "$content_length" -ge 100 ]; then
207- echo "Error: hash.txt content exceeds 100 characters. Length: $content_length"
208- exit 1
209- fi
210- echo "hash.txt is valid with MIME type $mime_type and length $content_length characters."
211- - name : Install Dependencies and Playwright
212- run : npm ci
213- - name : Resolve Playwright cache key inputs
214- id : playwright-version
215- run : |
216- echo "version=$(node -p 'require("@playwright/test/package.json").version')" >> "$GITHUB_OUTPUT"
217- echo "imageos=$ImageOS" >> "$GITHUB_OUTPUT"
218- - name : Cache Playwright browsers
219- uses : actions/cache@v5
220- id : playwright-cache
221- with :
222- path : ~/.cache/ms-playwright
223- key : ${{ runner.os }}-${{ runner.arch }}-${{ steps.playwright-version.outputs.imageos }}-playwright-${{ steps.playwright-version.outputs.version }}
224- - name : Install Playwright browser dependencies
225- if : steps.playwright-cache.outputs.cache-hit != 'true'
226- timeout-minutes : 15
227- run : npx playwright install chromium
228- - name : Run Playwright tests
229- run : npm test
230- - name : Prepare Playwright artifact directory
231- if : always()
202+ for i in $(seq 1 20); do
203+ if docker run --rm --network container:app open-bus-offline-test \
204+ 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
205+ echo "app is ready"
206+ exit 0
207+ fi
208+ echo "waiting for nginx (#$i)"
209+ sleep 1
210+ done
211+ echo "app did not become ready"
212+ docker logs app || true
213+ exit 1
214+ - name : Run Playwright e2e against prod image (hard offline)
232215 run : |
233216 mkdir -p playwright-artifact
234- if [ -d test-results ]; then cp -r test-results/* playwright-artifact/; fi
235- if [ -d playwright-report ]; then cp -r playwright-report/* playwright-artifact/; fi
236- touch playwright-artifact/.keep
217+ docker run --rm \
218+ --network container:app \
219+ -e CI=true \
220+ -e TZ=Asia/Jerusalem \
221+ -e PW_BASE_URL=http://localhost \
222+ -v "$PWD/playwright-artifact:/app/artifact-out" \
223+ open-bus-offline-test \
224+ 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'
237225 - name : Upload Playwright test artifact
238226 if : always()
239227 uses : actions/upload-artifact@v7
240228 with :
241229 name : playwright-test
242230 path : playwright-artifact
243- - name : Upload logs if exists
244- if : always()
245- uses : actions/upload-artifact@v7
246- with :
247- name : logs-test
248- path : logs
249- if-no-files-found : warn
231+ - name : Show app logs on failure
232+ if : failure()
233+ run : docker logs app || true
250234
251235 storybook-test :
252236 name : Storybook Visual Tests
@@ -256,6 +240,14 @@ jobs:
256240 - uses : actions/checkout@v6
257241 with :
258242 ref : ${{ github.event.pull_request.head.sha }}
243+ fetch-depth : 0
244+ - name : Compute Applitools merge-base timestamp
245+ env :
246+ BASE_REF : ${{ github.event.pull_request.base.ref || 'main' }}
247+ run : |
248+ git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" || true
249+ mb=$(git merge-base HEAD "origin/$BASE_REF") || exit 0
250+ echo "APPLITOOLS_GIT_MERGE_BASE_TIMESTAMP=$(git show -s --format=%aI "$mb")" >> "$GITHUB_ENV"
259251 - name : Configure blocked external hosts
260252 uses : ./.github/actions/block-network
261253 - name : Prepare for Testing
@@ -293,6 +285,14 @@ jobs:
293285 - uses : actions/checkout@v6
294286 with :
295287 ref : ${{ github.event.pull_request.head.sha }}
288+ fetch-depth : 0
289+ - name : Compute Applitools merge-base timestamp
290+ env :
291+ BASE_REF : ${{ github.event.pull_request.base.ref || 'main' }}
292+ run : |
293+ git fetch --no-tags origin "+refs/heads/$BASE_REF:refs/remotes/origin/$BASE_REF" || true
294+ mb=$(git merge-base HEAD "origin/$BASE_REF") || exit 0
295+ echo "APPLITOOLS_GIT_MERGE_BASE_TIMESTAMP=$(git show -s --format=%aI "$mb")" >> "$GITHUB_ENV"
296296 - name : Configure blocked external hosts
297297 uses : ./.github/actions/block-network
298298 - name : Prepare for Testing
@@ -336,7 +336,6 @@ jobs:
336336 - name : Run Playwright Visual Tests
337337 env :
338338 APPLITOOLS_BATCH_NAME : open-bus-map-search/${{ github.ref }}/${{ env.SHORT_SHA }}/
339- PWTEST_CHILD_PROCESS_TIMEOUT : ' 1800000' # 30 min; default 300000 force-kills slow UFG result collection → false CI failure
340339 run : npm run test:e2e:visual
341340 - name : Prepare Playwright artifact directory
342341 if : always()
0 commit comments