Files
zpan/scripts/report-deploy-telemetry.mjs
T
2026-06-08 14:43:34 -04:00

104 lines
3.3 KiB
JavaScript

import { spawnSync } from 'node:child_process'
import { randomBytes } from 'node:crypto'
const INTERNAL_TOKEN_ENV = 'ZPAN_INTERNAL_API_TOKEN'
const REPORT_PATH = '/api/internal/instance-telemetry/report'
const TOP_LEVEL_ENV_ARG = '--env='
const SECRET_PROPAGATION_DELAY_MS = 5000
const REPORT_RETRY_COUNT = 12
const REPORT_RETRY_DELAY_MS = 5000
class ReportError extends Error {
constructor(status, body) {
super(`HTTP ${status}: ${body}`)
this.status = status
}
}
const token = randomBytes(32).toString('hex')
if (process.env.GITHUB_ACTIONS === 'true') {
console.log(`::add-mask::${token}`)
}
let secretSet = false
try {
const deployUrl = resolveDeployUrl()
putInternalToken(token)
secretSet = true
const reported = await reportDeployTelemetry(token, deployUrl)
if (!reported) {
console.warn('Deploy telemetry report was not delivered after retries; continuing deploy.')
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
console.warn(`Deploy telemetry setup failed; continuing deploy. error=${message}`)
} finally {
if (secretSet) {
deleteInternalToken()
}
}
function putInternalToken(internalToken) {
console.log(`Setting ${INTERNAL_TOKEN_ENV}`)
const res = spawnSync('wrangler', ['secret', 'put', INTERNAL_TOKEN_ENV, TOP_LEVEL_ENV_ARG], {
input: internalToken,
encoding: 'utf8',
stdio: ['pipe', 'inherit', 'inherit'],
})
if (res.status !== 0) throw new Error(`wrangler secret put failed status=${res.status ?? 1}`)
}
function deleteInternalToken() {
console.log(`Deleting ${INTERNAL_TOKEN_ENV}`)
const res = spawnSync('wrangler', ['secret', 'delete', INTERNAL_TOKEN_ENV, TOP_LEVEL_ENV_ARG], {
input: 'y\n',
encoding: 'utf8',
stdio: ['pipe', 'inherit', 'inherit'],
})
if (res.status !== 0) throw new Error(`wrangler secret delete failed status=${res.status ?? 1}`)
}
async function reportDeployTelemetry(internalToken, deployUrl) {
const url = new URL(REPORT_PATH, deployUrl)
await sleep(SECRET_PROPAGATION_DELAY_MS)
for (let attempt = 1; attempt <= REPORT_RETRY_COUNT; attempt += 1) {
try {
const res = await fetch(url, {
method: 'POST',
headers: {
authorization: `Bearer ${internalToken}`,
},
})
if (!res.ok) throw new ReportError(res.status, await res.text())
console.log(`Reported deployed instance telemetry: ${url}`)
return true
} catch (err) {
if (err instanceof ReportError && err.status === 404) {
console.warn(`Deploy telemetry endpoint is waiting for secret propagation attempt=${attempt}`)
} else {
const code = err instanceof Error ? err.message : String(err)
console.warn(`Deploy telemetry report failed attempt=${attempt} error=${code}`)
}
if (attempt < REPORT_RETRY_COUNT) await sleep(REPORT_RETRY_DELAY_MS)
}
}
return false
}
function resolveDeployUrl() {
const value = process.argv[2]?.trim()
if (!value) {
throw new Error('Deploy telemetry URL is required. Usage: node scripts/report-deploy-telemetry.mjs https://your-zpan.example')
}
const url = new URL(value)
if (url.protocol !== 'http:' && url.protocol !== 'https:') {
throw new Error(`Deploy telemetry URL must use http or https: ${value}`)
}
return url.origin
}
function sleep(ms) {
return new Promise((resolve) => setTimeout(resolve, ms))
}