deps(deps-dev): bump typescript from 5.9.3 to 7.0.2 #54
Workflow file for this run
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: CI | |
| # One workflow, two deployables that share a repository and nothing else. | |
| # | |
| # the app workers/, packages/, migrations/, tests/ -> the `podium` Worker | |
| # the marketing site www/ -> the `podium-www` Worker | |
| # | |
| # Pull requests get the checks. Pushes to main get the checks and, only if they | |
| # all pass, a deploy — of whichever of the two actually changed. | |
| # | |
| # Why the `changes` job rather than `on.push.paths`: path filters in `on:` are | |
| # workflow-level, so they would gate both deployables together. The whole point | |
| # of the split is that a copy fix on the landing page must not run a D1 | |
| # migration against production, and that only holds if the filter is per job. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| concurrency: | |
| group: ${{ github.workflow }}-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| changes: | |
| name: Detect what changed | |
| runs-on: ubuntu-latest | |
| outputs: | |
| app: ${{ steps.filter.outputs.app }} | |
| www: ${{ steps.filter.outputs.www }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # Needed to diff against the base commit; the default shallow clone | |
| # does not contain it. | |
| fetch-depth: 0 | |
| - id: filter | |
| env: | |
| EVENT: ${{ github.event_name }} | |
| BEFORE: ${{ github.event.before }} | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| HEAD_SHA: ${{ github.sha }} | |
| run: | | |
| set -euo pipefail | |
| # Pick a base to diff against. A manual run has none, and the first | |
| # push to a branch reports an all-zero `before` — both mean "assume | |
| # everything changed". That is the safe direction to be wrong in: the | |
| # cost is a redundant deploy, where the cost of the other direction is | |
| # a change that silently never ships. | |
| base="" | |
| if [ "$EVENT" = "pull_request" ]; then | |
| base="$BASE_SHA" | |
| elif [ "$EVENT" = "push" ] && [ "$BEFORE" != "0000000000000000000000000000000000000000" ] \ | |
| && git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then | |
| base="$BEFORE" | |
| fi | |
| app=false | |
| www=false | |
| if [ -z "$base" ]; then | |
| echo "No usable base ref ($EVENT) — treating everything as changed." | |
| app=true | |
| www=true | |
| else | |
| files=$(git diff --name-only "$base" "$HEAD_SHA") | |
| echo "Changed files:" | |
| echo "$files" | sed 's/^/ /' | |
| while IFS= read -r f; do | |
| [ -z "$f" ] && continue | |
| case "$f" in | |
| # This file gates both, so a change to it must run both. | |
| .github/workflows/ci.yml) app=true; www=true ;; | |
| www/*) www=true ;; | |
| *) app=true ;; | |
| esac | |
| done <<< "$files" | |
| fi | |
| echo "app=$app" >> "$GITHUB_OUTPUT" | |
| echo "www=$www" >> "$GITHUB_OUTPUT" | |
| echo "-> app=$app www=$www" | |
| check: | |
| name: Typecheck, tests, model drift, security | |
| needs: changes | |
| if: needs.changes.outputs.app == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| # `semgrep --baseline-commit` checks out and re-scans the base, so | |
| # the default shallow clone is not enough. | |
| fetch-depth: 0 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| cache: npm | |
| - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 | |
| with: | |
| python-version: '3.12' | |
| - run: npm ci | |
| # worker-configuration.d.ts is generated, not committed (.gitignore), so | |
| # a fresh checkout has no Env type and `tsc` fails without this. Reads | |
| # wrangler.jsonc only — no Cloudflare credentials involved. | |
| - name: Generate binding types | |
| run: npx wrangler types | |
| - run: npm run typecheck | |
| - name: Unit tests | |
| run: npm run test:unit | |
| # @cloudflare/vitest-pool-workers runs workerd locally against real | |
| # bindings; still no account credentials needed. | |
| - name: Integration tests | |
| run: npm run test:integration | |
| # CLAUDE.md: the domain model is the specification, and a change that | |
| # lands in code without landing in docs/domain/ is an incomplete commit. | |
| # `--check` exits non-zero on drift, which is the point of running it here. | |
| - name: Domain model drift | |
| run: npm run drift | |
| # `.claude/skills/security-audit/`. Two halves, both cheap and offline: | |
| # | |
| # npm run security the attack-surface inventory — fails only on | |
| # surface that is *new* since the last accepted | |
| # baseline (a route with no guard, a fresh | |
| # `raw()`, a `db.raw()` naming no org_id) | |
| # npm run security:semgrep taint and pattern rules for this repo's own | |
| # sinks, which no stock ruleset knows about | |
| # | |
| # Deliberately in `check` rather than a job of its own: these gate the | |
| # deploy exactly like the tests do. A new unguarded admin route is not a | |
| # warning to read later. | |
| - name: Attack surface | |
| run: npm run security | |
| - name: Install semgrep | |
| run: pip install --disable-pip-version-check --quiet semgrep | |
| # Gated on what is *new*, not on the total — the same shape as | |
| # `npm run security`'s baseline, and for the same reason. Most of what | |
| # these rules report is an inventory a human has to read (16 `raw()` | |
| # interpolations, 36 `db.raw()` calls doing their own org-scoping); it is | |
| # worth reviewing and it is not worth blocking every unrelated PR on. | |
| # `--baseline-commit` reruns the scan against the base and subtracts, so | |
| # only a finding this change introduced fails the build. | |
| # | |
| # No base ref (a manual run, or the first push to a branch) means no | |
| # subtraction is possible. That reports everything, which would be a wall | |
| # of pre-existing findings rather than a signal, so the gate steps aside | |
| # and says so — the scheduled CodeQL run and the quarterly audit are what | |
| # cover that case. | |
| - name: Semgrep (Podium rules) | |
| env: | |
| BASE_SHA: ${{ github.event.pull_request.base.sha }} | |
| EVENT: ${{ github.event_name }} | |
| run: | | |
| set -euo pipefail | |
| base="" | |
| if [ "$EVENT" = "pull_request" ] && [ -n "$BASE_SHA" ]; then | |
| base="$BASE_SHA" | |
| elif git rev-parse --verify --quiet HEAD~1 >/dev/null; then | |
| base=$(git rev-parse HEAD~1) | |
| fi | |
| if [ -z "$base" ]; then | |
| echo "No base commit to diff against — reporting without gating." | |
| npm run security:semgrep | |
| exit 0 | |
| fi | |
| echo "Gating on findings new since $base" | |
| semgrep scan --metrics=off --error --baseline-commit "$base" \ | |
| --config .claude/skills/security-audit/rules/ \ | |
| --exclude=node_modules --exclude=www --exclude=.wrangler | |
| deploy: | |
| name: Deploy the app | |
| needs: [changes, check] | |
| if: needs.changes.outputs.app == 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| # Gate approvals, and the production secrets, on a GitHub Environment. | |
| environment: | |
| name: production | |
| # The app does not know its own hostname — see scripts/deploy-config.mjs. | |
| # Neither does this workflow; it reads the one the deployment was given. | |
| url: https://${{ vars.PODIUM_HOSTNAME }} | |
| # Never two deploys at once: migrations and `wrangler deploy` are not safe | |
| # to interleave. Not cancel-in-progress — a half-applied migration set is | |
| # worse than a queued deploy. | |
| concurrency: | |
| group: deploy-production | |
| cancel-in-progress: false | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| PODIUM_HOSTNAME: ${{ vars.PODIUM_HOSTNAME }} | |
| CF_D1_DATABASE_ID: ${{ secrets.CF_D1_DATABASE_ID }} | |
| CF_KV_CACHE_ID: ${{ secrets.CF_KV_CACHE_ID }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| cache: npm | |
| - run: npm ci | |
| # Migrations before the Worker: `migrations/` is append-only, so the new | |
| # schema is always compatible with the currently-deployed code, while the | |
| # reverse order leaves new code reading columns that do not exist yet. | |
| - name: Apply D1 migrations | |
| run: npm run deploy:migrate | |
| - name: Deploy Worker | |
| run: npm run deploy | |
| # Fail the run if the deploy produced a site that does not answer. /login | |
| # rather than / because it renders without an event or a session. | |
| # | |
| # Following redirects rather than asserting 200 on the first response: on | |
| # a deployment with no Organization yet, /login legitimately 303s to | |
| # /setup (01, "First-run setup"). What this step is for is proving the | |
| # Worker is up and serving, and the end of the chain is where that shows. | |
| - name: Smoke check | |
| run: | | |
| # Split with parameter expansion, not `read`: curl's -w output has no | |
| # trailing newline, so `read` returns non-zero at EOF and the step | |
| # dies under `bash -e` before it can report anything. | |
| out=$(curl -sSL -o /dev/null \ | |
| -w '%{http_code} %{url_effective}' --retry 5 --retry-delay 5 \ | |
| --retry-all-errors --max-redirs 5 --max-time 30 \ | |
| "https://${PODIUM_HOSTNAME}/login") | |
| code=${out%% *} | |
| url=${out#* } | |
| echo "GET /login -> $code ($url)" | |
| test "$code" = "200" | |
| deploy-www: | |
| name: Deploy the marketing site | |
| needs: changes | |
| if: needs.changes.outputs.www == 'true' && github.ref == 'refs/heads/main' && github.event_name != 'pull_request' | |
| runs-on: ubuntu-latest | |
| # Reuses the `production` environment for its two secrets. If that | |
| # environment ever gains required reviewers, split this onto its own | |
| # `production-www` holding the same secrets with no gate — a landing-page | |
| # copy fix should not need an approval. | |
| environment: | |
| name: production | |
| url: https://podiumstack.com | |
| # Independent of deploy-production: nothing here touches D1, so there is no | |
| # reason for a marketing deploy to queue behind an app deploy or vice versa. | |
| concurrency: | |
| group: deploy-www | |
| cancel-in-progress: false | |
| env: | |
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | |
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | |
| defaults: | |
| run: | |
| working-directory: www | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 | |
| with: | |
| node-version: '24' | |
| cache: npm | |
| cache-dependency-path: www/package-lock.json | |
| # Note what is absent: no typecheck of app code, no tests, and above all | |
| # no `deploy:migrate`. This job cannot reach the production database. | |
| - run: npm ci | |
| - name: Build and deploy | |
| run: npm run deploy | |
| # Assert the redirect *target*, not merely that some 3xx came back. A | |
| # `_redirects` rule that fails to parse is logged and skipped rather than | |
| # failing the deploy, so "not a 200" is not enough to prove the file | |
| # survived the trip. | |
| - name: Smoke check | |
| run: | | |
| root=$(curl -sS -o /dev/null -w '%{http_code}' --retry 5 --retry-delay 5 \ | |
| --retry-all-errors --max-time 30 https://podiumstack.com/) | |
| loc=$(curl -sS -o /dev/null -w '%{redirect_url}' --max-time 30 https://podiumstack.com/login) | |
| echo "GET / -> $root ; GET /login -> $loc" | |
| test "$root" = "200" | |
| test "$loc" = "https://app.podiumstack.com/login" |