Skip to content

Commit 1deeb70

Browse files
RISCfutureclaude
andcommitted
chore: normalize build, Sentry, and CI configuration
Applies the approved fleet normalization proposals for this site: - preload-recovery-everywhere: install src/utils/preloadRecovery.ts and call it from main.ts before createApp - sentry-release-and-environment: release/environment in Sentry.init, VITE_SENTRY_RELEASE wired through the deploy build - shared-sentry-ignore-errors: the canonical five-entry ignoreErrors array - stop-publishing-sourcemaps: build.sourcemap 'hidden', and the deploy strips .map files after the Sentry upload and before wrangler deploy - ci-lint-and-unit-parity: CI lint job runs the aggregate pnpm run lint (eslint, stylelint, oxlint, knip) and unit tests run with --run - workflow-hygiene-block: contents:read on CI, deploy concurrency never cancels in flight, submodules checkout dropped, dead ci:build removed - vue-devtools-serve-only: devtools gated on command === 'serve' and launchEditor read from VITE_LAUNCH_EDITOR - manifest-installability: theme_color reconciled with index.html, icon entries that misreport apple-touch-icon.png's size removed - tsconfig-node-target-alignment: extend @tsconfig/node26, drop the nightwatch include, give tsconfig.vitest.json a real lib array Also harmonizes registerServiceWorker() with the fleet template by composing the worker URL and scope from import.meta.env.BASE_URL. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 78dec2a commit 1deeb70

12 files changed

Lines changed: 136 additions & 61 deletions

File tree

‎.github/workflows/ci.yml‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
# This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2-
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions
3-
41
name: CI
52

63
on:
@@ -9,14 +6,19 @@ on:
96
pull_request:
107
branches: [master]
118

9+
permissions:
10+
contents: read
11+
12+
concurrency:
13+
group: ${{ github.workflow }}-${{ github.ref }}
14+
cancel-in-progress: true
15+
1216
jobs:
1317
unit:
1418
name: Unit Tests
1519
runs-on: ubuntu-latest
1620
steps:
1721
- uses: actions/checkout@v6
18-
with:
19-
submodules: true
2022
- name: Set up pnpm
2123
uses: pnpm/action-setup@v6
2224
- name: Set up Node.js
@@ -27,14 +29,13 @@ jobs:
2729
- name: Install dependencies
2830
run: pnpm install --frozen-lockfile
2931
- name: Run unit tests
30-
run: pnpm run test:unit
32+
run: pnpm run test:unit -- --run
33+
3134
e2e:
3235
name: E2E Tests
3336
runs-on: ubuntu-latest
3437
steps:
3538
- uses: actions/checkout@v6
36-
with:
37-
submodules: true
3839
- name: Set up pnpm
3940
uses: pnpm/action-setup@v6
4041
- name: Set up Node.js
@@ -44,17 +45,22 @@ jobs:
4445
node-version-file: .nvmrc
4546
- name: Install dependencies
4647
run: pnpm install --frozen-lockfile
47-
- name: Install Playwright browsers
48-
run: pnpm exec playwright install --with-deps
48+
- name: Install Playwright system dependencies
49+
run: pnpm exec playwright install-deps
50+
timeout-minutes: 3
51+
- name: Install Playwright Browsers
52+
run: pnpm exec playwright install
53+
timeout-minutes: 3
54+
- name: Build project
55+
run: pnpm run build
4956
- name: Run E2E tests
5057
run: pnpm run test:e2e
58+
5159
lint:
5260
name: Linters
5361
runs-on: ubuntu-latest
5462
steps:
5563
- uses: actions/checkout@v6
56-
with:
57-
submodules: true
5864
- name: Set up pnpm
5965
uses: pnpm/action-setup@v6
6066
- name: Set up Node.js
@@ -64,9 +70,7 @@ jobs:
6470
node-version-file: .nvmrc
6571
- name: Install dependencies
6672
run: pnpm install --frozen-lockfile
67-
- name: Run ESLint
68-
run: pnpm run lint:eslint
69-
- name: Run Stylelint
70-
run: pnpm run lint:stylelint
73+
- name: Run linters
74+
run: pnpm run lint
7175
- name: Type check
7276
run: pnpm run type-check

‎.github/workflows/deploy.yml‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,18 +13,14 @@ on:
1313
permissions:
1414
contents: read
1515

16-
# Allow one concurrent deployment
16+
# Allow one concurrent deployment, and never cancel one in flight
1717
concurrency:
1818
group: deploy
19-
cancel-in-progress: true
19+
cancel-in-progress: false
2020

2121
jobs:
2222
deploy:
23-
name: Build and Deploy
24-
if: ${{ github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' }}
25-
environment:
26-
name: production
27-
url: https://icrashedmyplane.com/
23+
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
2824
runs-on: ubuntu-latest
2925
steps:
3026
- name: Checkout
@@ -44,10 +40,8 @@ jobs:
4440
run: pnpm run build
4541
env:
4642
VITE_SENTRY_DSN: ${{ secrets.SENTRY_DSN }}
47-
- name: Deploy to Cloudflare Workers
48-
run: pnpm exec wrangler deploy
49-
env:
50-
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
43+
VITE_SENTRY_RELEASE: ${{ github.sha }}
44+
# Sourcemaps must reach Sentry BEFORE they are deleted. Do not reorder.
5145
- name: Create Sentry release
5246
uses: getsentry/action-release@v3
5347
env:
@@ -58,3 +52,10 @@ jobs:
5852
environment: production
5953
release: ${{ github.sha }}
6054
sourcemaps: './dist'
55+
# Sourcemaps must not reach the public. Do not reorder.
56+
- name: Strip source maps from the published assets
57+
run: find dist -name '*.map' -delete
58+
- name: Deploy to Cloudflare Workers
59+
run: pnpm exec wrangler deploy
60+
env:
61+
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}

‎.gitignore‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,6 @@ coverage/
2222
/blob-report/
2323
/test-results/
2424
.claude/settings.local.json
25+
26+
# Wrangler
27+
.wrangler/

‎env.d.ts‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
11
/// <reference types="vite/client" />
2+
3+
interface ImportMetaEnv {
4+
readonly VITE_SENTRY_DSN: string
5+
readonly VITE_SENTRY_RELEASE: string
6+
}

‎package.json‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,6 @@
99
"scripts": {
1010
"build": "run-p type-check \"build-only {@}\" --",
1111
"build-only": "vite build",
12-
"ci:build": "run-p type-check \"build-only {@}\" --",
13-
"ci:unit": "vitest --run",
1412
"dev": "vite",
1513
"doc": "npx typedoc --out docs src",
1614
"format": "prettier --write .",
@@ -41,7 +39,7 @@
4139
"@pinia/testing": "^1.0.3",
4240
"@playwright/test": "^1.61.1",
4341
"@testing-library/vue": "^8.1.0",
44-
"@tsconfig/node22": "^22.0.5",
42+
"@tsconfig/node26": "^26.0.1",
4543
"@types/cldrjs": "^0.4.29",
4644
"@types/jsdom": "^28.0.3",
4745
"@types/lodash-es": "^4.17.12",

‎pnpm-lock.yaml‎

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎public/site.webmanifest‎

Lines changed: 1 addition & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"scope": "/",
77
"display": "standalone",
88
"orientation": "portrait",
9-
"theme_color": "#0e6ba8",
9+
"theme_color": "#ffffff",
1010
"background_color": "#ffffff",
1111
"lang": "en",
1212
"dir": "ltr",
@@ -28,18 +28,6 @@
2828
"sizes": "any",
2929
"type": "image/svg+xml",
3030
"purpose": "maskable"
31-
},
32-
{
33-
"src": "/apple-touch-icon.png",
34-
"sizes": "192x192",
35-
"type": "image/png",
36-
"purpose": "any"
37-
},
38-
{
39-
"src": "/apple-touch-icon.png",
40-
"sizes": "512x512",
41-
"type": "image/png",
42-
"purpose": "any"
4331
}
4432
]
4533
}

‎src/main.ts‎

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import { createApp } from 'vue'
99
import { createPinia } from 'pinia'
1010
import * as Sentry from '@sentry/vue'
1111
import i18n from '@/i18n'
12+
import { recoverFromPreloadErrors } from '@/utils/preloadRecovery'
1213
import App from './App.vue'
1314

15+
recoverFromPreloadErrors()
16+
1417
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument -- .vue default export is untyped
1518
const app = createApp(App)
1619

@@ -22,6 +25,8 @@ const sentryDSN = import.meta.env.VITE_SENTRY_DSN as string | undefined
2225
Sentry.init({
2326
app,
2427
dsn: sentryDSN,
28+
release: import.meta.env.VITE_SENTRY_RELEASE || undefined,
29+
environment: import.meta.env.PROD ? 'production' : 'development',
2530
sendDefaultPii: false,
2631
integrations: [
2732
Sentry.vueIntegration({
@@ -32,6 +37,30 @@ Sentry.init({
3237
],
3338
tracesSampleRate: 1.0,
3439
enableLogs: true,
40+
ignoreErrors: [
41+
// Browser-extension content scripts inject WebExtension messaging into
42+
// the page; their failures are not our code and are unfixable here.
43+
// Sentry TIM-DOT-CODES-6.
44+
/runtime\.sendMessage/u,
45+
// vite-plugin-pwa's injected SW registration throws InvalidStateError
46+
// when Chrome registers during prerender. No elegant in-plugin or
47+
// newer-version fix exists, so we filter the noise. Sentry
48+
// TIM-DOT-CODES-5.
49+
/Failed to register a ServiceWorker/u,
50+
// Native in-app browsers (WKWebView wrappers) inject a bridge script that
51+
// calls `window.webkit.messageHandlers`; it throws when that handler is
52+
// absent. Not our code and unfixable here. Sentry TIM-DOT-CODES-8.
53+
/messageHandlers/u,
54+
// Android WebView tears down its JS bridge mid-post, so a `postMessage`
55+
// from the injected bridge rejects with "Java object is gone". Not our
56+
// code and unfixable here. Sentry RACCOONBETS-FRONTEND-D.
57+
/Java object is gone/u,
58+
// Microsoft's Outlook SafeLinks crawler rejects a promise from its own
59+
// injected instrumentation while previewing a link. It arrives without a
60+
// stacktrace from an Azure address, never from a visitor. Sentry
61+
// TIM-DOT-CODES-C.
62+
/Object Not Found Matching Id/u,
63+
],
3564
})
3665

3766
/**
@@ -40,15 +69,18 @@ Sentry.init({
4069
* A failed registration costs offline caching and nothing else, so the
4170
* rejection is logged rather than left to surface as an unhandled error.
4271
*/
43-
function registerServiceWorker() {
72+
function registerServiceWorker(): void {
4473
if (!('serviceWorker' in navigator)) return
4574

4675
window.addEventListener('load', () => {
47-
navigator.serviceWorker.register('/sw.js', { scope: '/' }).catch((error: unknown) => {
48-
Sentry.logger.warn('Service worker registration failed', {
49-
reason: error instanceof Error ? error.message : String(error),
76+
const swURL = `${import.meta.env.BASE_URL}sw.js`
77+
navigator.serviceWorker
78+
.register(swURL, { scope: import.meta.env.BASE_URL })
79+
.catch((error: unknown) => {
80+
Sentry.logger.warn('Service worker registration failed', {
81+
reason: error instanceof Error ? error.message : String(error),
82+
})
5083
})
51-
})
5284
})
5385
}
5486

‎src/utils/preloadRecovery.ts‎

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// A tab may spend one reload per cooldown recovering a failed chunk. Beyond that, an asset that
2+
// is genuinely unreachable would put the tab in a refresh loop.
3+
const RELOAD_KEY = 'preload-error-reload'
4+
const RELOAD_COOLDOWN_MS = 10 * 60 * 1000
5+
6+
/**
7+
* Reloads the page when a lazily-loaded chunk can't be fetched.
8+
*
9+
* Vite's preload helper rejects when a code-split chunk or its stylesheet fails to load — a
10+
* dropped request, or a tab whose `index.html` predates the current deploy and so asks for asset
11+
* hashes that no longer exist. Nothing downstream recovers on its own, so the reload fetches a
12+
* fresh `index.html` and with it the current hashes.
13+
*
14+
* Reloads are limited to one per cooldown: a deploy that really is broken surfaces the failure to
15+
* error reporting rather than refreshing forever, while a tab left open across several deploys
16+
* still heals each time.
17+
*/
18+
export function recoverFromPreloadErrors(): void {
19+
window.addEventListener('vite:preloadError', reloadOnce)
20+
}
21+
22+
function reloadOnce(event: VitePreloadErrorEvent): void {
23+
if (!claimReload()) return
24+
25+
// Suppresses Vite's rethrow: the reload is the recovery, so the rejection isn't worth
26+
// reporting. A failure that can't claim a reload stays unprevented and reaches Sentry.
27+
event.preventDefault()
28+
window.location.reload()
29+
}
30+
31+
function claimReload(): boolean {
32+
try {
33+
const lastReload = Number(window.sessionStorage.getItem(RELOAD_KEY) ?? 0)
34+
if (Number.isFinite(lastReload) && Date.now() - lastReload < RELOAD_COOLDOWN_MS) return false
35+
36+
window.sessionStorage.setItem(RELOAD_KEY, String(Date.now()))
37+
return true
38+
} catch {
39+
// sessionStorage may be unavailable (private mode, etc.). With nowhere to record the attempt
40+
// a reload could loop, so decline it.
41+
return false
42+
}
43+
}

‎tsconfig.node.json‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
{
2-
"extends": "@tsconfig/node22/tsconfig.json",
3-
"include": ["vite.config.*", "vitest.config.*", "nightwatch.conf.*", "playwright.config.*"],
2+
"extends": "@tsconfig/node26/tsconfig.json",
3+
"include": ["vite.config.*", "vitest.config.*", "playwright.config.*"],
44
"compilerOptions": {
55
"noEmit": true,
66
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
77

8+
"strict": true,
89
"module": "ESNext",
910
"moduleResolution": "Bundler",
1011
"types": ["node"]

0 commit comments

Comments
 (0)