Files
a88bf2c5a5 chore(web): harden CI permissions and expose /version.json (#7918)
* chore(ci): set default workflow permissions to none

Workflows previously missing a top-level permissions block inherit the
default GITHUB_TOKEN with write access. Set permissions: {} at the
workflow level so each job must declare what it needs. For two jobs
that lacked explicit permissions, grant contents: read so checkout
still works.

* feat(web): expose build metadata at /version.json

Generate apps/web/public/version.json during the build with the package
version, commit SHA, optional git tag, branch, and build time. After
deploy, the served bundle exposes /version.json so the running
release can be identified without dashboard access — useful for
external monitoring that compares the live deploy against the latest
GitHub release. The generated file is gitignored.

* chore(ci): address review comments — explicit contents:read + accurate version SHA

- Add `contents: read` to jobs in web-checks.yml (lint/prettier/type-check/
  knip-exports), web-deploy-dev.yml (deploy), and tx-builder-deploy.yml
  (pr-preview, deploy-staging). With workflow-level `permissions: {}`,
  these jobs no longer inherit the contents scope that actions/checkout
  relies on; granting it explicitly removes the ambiguity.
- generate-version.mjs: prefer `git rev-parse HEAD` over `GITHUB_SHA`.
  On pull_request events `GITHUB_SHA` is the merge commit, but the deploy
  workflow checks out `pull_request.head.sha` — the script was reporting
  the wrong SHA. Also use `GITHUB_HEAD_REF` for PR branch resolution
  (`GITHUB_REF_NAME` is "<N>/merge" on PRs) and filter detached "HEAD"
  out of the git fallback for tag builds.

---------

Co-authored-by: Hanzo Dev <[email protected]>
2026-05-26 13:54:22 +02:00

52 lines
1.9 KiB
JavaScript

#!/usr/bin/env node
import { execFileSync } from 'node:child_process'
import { readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
const here = dirname(fileURLToPath(import.meta.url))
const webRoot = resolve(here, '..')
const pkg = JSON.parse(readFileSync(resolve(webRoot, 'package.json'), 'utf8'))
const git = (...args) => {
try {
return execFileSync('git', args, { cwd: webRoot, stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim()
} catch {
return ''
}
}
// Prefer the checked-out commit over GITHUB_SHA: on pull_request events GITHUB_SHA is
// the merge commit, but our deploy workflows check out github.event.pull_request.head.sha
// — git rev-parse HEAD always matches the artifact that's actually being built.
const commit = git('rev-parse', 'HEAD') || process.env.GITHUB_SHA || 'unknown'
const commitShort = commit === 'unknown' ? 'unknown' : commit.slice(0, 7)
const refName = process.env.GITHUB_REF_NAME || ''
const refType = process.env.GITHUB_REF_TYPE || ''
const headRef = process.env.GITHUB_HEAD_REF || ''
const tag = git('describe', '--tags', '--exact-match') || (refType === 'tag' ? refName : null) || null
// On pull_request events GITHUB_REF_NAME is "<N>/merge" and HEAD is detached, so prefer
// GITHUB_HEAD_REF (the PR's source branch). Fall back to GITHUB_REF_NAME for push events,
// then to git (which returns "HEAD" when detached — filter that out).
const gitBranch = git('rev-parse', '--abbrev-ref', 'HEAD')
const branch =
headRef || (refType === 'branch' ? refName : '') || (gitBranch && gitBranch !== 'HEAD' ? gitBranch : '') || null
const version = {
version: pkg.version,
commit,
commitShort,
tag,
branch,
buildTime: new Date().toISOString(),
}
const outPath = resolve(webRoot, 'public', 'version.json')
writeFileSync(outPath, JSON.stringify(version, null, 2) + '\n')
console.log(`Wrote ${outPath}:`, version)