# Release checklist

The checks to run on a build folder before zipping it, each with the reason, and the commands that find the usual mistakes.

Run these on the build folder (`dist/`) before zipping it. Each item says why, so a game with an unusual
build can apply the reason. The bundle is served from `https://<slug>.<play domain>/<slug>/<version>/`
through the CDN and from the preview host under a cookie scoped to that path; both carry the CSP of the
[lifecycle page](/docs/games/lifecycle/).

## Structure

- `index.html` sits at the root of the folder. The upload refuses a zip without it at the root, and a zip
  of the folder itself puts it one level down: zip the content.
- No `..` in a path, no symbolic link, no encrypted entry, no file named `_upload.zip`. The upload refuses
  them.
- Zip at most 200 MB, at most 500 MB unpacked, at most 10,000 files, at most 64 MB per file. A few MB loads
  fast on a phone.
- No source maps unless you want them public.

<CodePanel title="Check the archive">
  <Fragment slot="javascript">
    ```bash
    cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..
    unzip -l gloop-1.4.0.zip | head -5          # index.html first, at the root
    du -sh dist && find dist -type f | wc -l
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```bash
    cd dist && zip -r ../gloop-1.4.0.zip . -x '*.map' && cd ..
    unzip -l gloop-1.4.0.zip | head -5          # index.html first, at the root
    du -sh dist && find dist -type f | wc -l
    ```

  </Fragment>
</CodePanel>

## Paths

- Every URL in HTML, CSS and JS is relative: `assets/x.png`, `./main.js`. Vite needs `base: './'`.
- No URL starts with `/`. On the CDN it leaves `/<slug>/<version>/`; in the preview it also loses the cookie
  and answers 404.
- Code that builds URLs at run time uses `new URL('assets/x.png', document.baseURI)` or `import.meta.url`,
  never `location.origin + '/...'`.
- Workers and WASM load from relative paths too:
  `new Worker(new URL('./w.js', import.meta.url), { type: 'module' })`.

<CodePanel title="Find root-relative URLs">
  <Fragment slot="javascript">
    ```bash
    grep -rnoE '(src|href)="/[^/]' dist --include='*.html'
    grep -rnoE "url\(/[^/]" dist --include='*.css'
    grep -rnoE "[\"'\`]/(assets|src|img|audio|data)/" dist --include='*.js' | head
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```bash
    grep -rnoE '(src|href)="/[^/]' dist --include='*.html'
    grep -rnoE "url\(/[^/]" dist --include='*.css'
    grep -rnoE "[\"'\`]/(assets|src|img|audio|data)/" dist --include='*.js' | head
    ```

  </Fragment>
</CodePanel>

## Scripts and the CSP

- No `<script src="https://...">`: the CSP blocks it. The SDK loads the CrazyGames, Poki and Playgama
  scripts on their own portal; remove the tags the old build had.
- No inline `<script>` with code, no `onclick=""` attribute, no `eval`, no `new Function`, no `setTimeout`
  with a string. The policy has no `unsafe-inline` or `unsafe-eval` for scripts: they throw.
- No `fetch`, `XMLHttpRequest`, `WebSocket` or `EventSource` to a host other than the bundle's own and the
  API. Analytics and error reporting go through `sdk.track()`.
- Fonts, images, audio and data files are inside the bundle. No Google Fonts link, no CDN library.
- The SDK is either bundled (ESM) or loaded by a relative script tag, `vendor/filudi-sdk-<version>.iife.js`,
  before the game's script.
- `sdk-trace.js` and any other playground helper are gone.

<CodePanel title="Find what the CSP will block">
  <Fragment slot="javascript">
    ```bash
    grep -rnE "<script[^>]+src=.https?:" dist --include='*.html'
    grep -rnE '<script>[^<]|<script type="module">[^<]| on[a-z]+="' dist --include='*.html'
    grep -rnE '\beval\(|new Function\(' dist --include='*.js' | grep -v filudi-sdk | head
    grep -rnE 'fonts.googleapis|cdn\.|unpkg|jsdelivr|sdk-trace' dist | head
    ```

  </Fragment>
  <Fragment slot="typescript">
    ```bash
    grep -rnE "<script[^>]+src=.https?:" dist --include='*.html'
    grep -rnE '<script>[^<]|<script type="module">[^<]| on[a-z]+="' dist --include='*.html'
    grep -rnE '\beval\(|new Function\(' dist --include='*.js' | grep -v filudi-sdk | head
    grep -rnE 'fonts.googleapis|cdn\.|unpkg|jsdelivr|sdk-trace' dist | head
    ```

  </Fragment>
</CodePanel>

## Frame and orientation

- The game fills its frame: it reads `innerWidth` and `innerHeight` or a `ResizeObserver`, not
  `screen.width`, and redraws on `resize`.
- The orientation declared on the game (`portrait`, `landscape` or `any`) matches what the layout handles.
- `mobile` and `minutes` are set on the game.
- The game does not draw its own fullscreen button on CrazyGames (`result.host === 'crazygames'`);
  elsewhere a button calls `sdk.fullscreen()`.
- Audio starts after a user gesture.
- A click in the game gives it the keyboard: a `pointerdown` handler that calls `preventDefault()` also
  calls `canvas.focus()`, with `tabindex="0"` on the canvas. Check it: click outside the game, click in the
  game, press an arrow key.

## SDK use

- `sdk.init({ gameId, version })`: `gameId` is the game's slug in the console, `version` the version you
  upload.
- `sdk.ready()` after the first frame, once.
- Gameplay brackets around real play only; mute on `ads.on('start')`; reward on `completed` only.
- Every board passed to `leaderboard.submit()` is declared in the console with its bounds.
- The CrazyGames leaderboard key comes from a build variable and is not in the repository.
- The game ran on each host of the playground with "Production CSP" on and no `CSP violation` line.

## Then

Upload the zip, preview it, request its review: [your first bundle](/docs/games/first-bundle/). Tag the
game's repository with the version you uploaded, so a rollback (activating the previous version again) has
a source to match.
