Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
125 lines
3.8 KiB
TypeScript
125 lines
3.8 KiB
TypeScript
const MASK_PLACEHOLDER = '\x00MASKED\x00'
|
|
|
|
function sanitizeVariables(errorOutput: string, secretValues?: string[]): string {
|
|
let sanitized = errorOutput
|
|
|
|
// Value-based masking: replace any occurrence of known secret values (like GitHub Actions).
|
|
// Uses an internal placeholder so regex patterns below can't re-match masked output.
|
|
// Sort longest-first so a longer secret is masked before a shorter substring of it.
|
|
if (secretValues) {
|
|
const sorted = secretValues.filter((v) => v.length >= 8).sort((a, b) => b.length - a.length)
|
|
for (const value of sorted) {
|
|
sanitized = sanitized.replaceAll(value, MASK_PLACEHOLDER)
|
|
}
|
|
}
|
|
|
|
// Sanitize wrangler --var KEY:VALUE patterns
|
|
sanitized = sanitized.replace(/(--var\s+)(\w+):[^ \n]+/g, '$1$2:`***`')
|
|
|
|
// Sanitize KEY=VALUE patterns where KEY looks like an env var and value is long enough
|
|
// to be a secret (e.g. flyctl secrets set). Short values like =debug are left alone.
|
|
sanitized = sanitized.replace(/\b([A-Z][A-Z_0-9]{2,})=([^ \n]{8,})/g, '$1=`***`')
|
|
|
|
// Sanitize --token VALUE and --token=VALUE patterns (e.g. vercel --token xxx)
|
|
sanitized = sanitized.replace(/(--token[\s=])\S+/g, '$1`***`')
|
|
|
|
// Sanitize connection strings (postgres://, redis://, etc.)
|
|
sanitized = sanitized.replace(
|
|
/\b(postgres|postgresql|mysql|redis|mongodb|amqp):\/\/[^\s"']+/gi,
|
|
'$1://`***`'
|
|
)
|
|
|
|
// Replace internal placeholder with final mask
|
|
sanitized = sanitized.replaceAll(MASK_PLACEHOLDER, '`***`')
|
|
|
|
return sanitized
|
|
}
|
|
|
|
export class Discord {
|
|
static AT_TEAM_MENTION = '<@&1414904550917144586>'
|
|
|
|
constructor(opts: {
|
|
webhookUrl: string
|
|
shouldNotify: boolean
|
|
totalSteps?: number
|
|
messagePrefix?: string
|
|
secretValues?: string[]
|
|
}) {
|
|
this.webhookUrl = opts.webhookUrl
|
|
this.shouldNotify = opts.shouldNotify
|
|
this.totalSteps = opts.totalSteps ?? 0
|
|
this.messagePrefix = opts.messagePrefix ?? ''
|
|
this.secretValues = opts.secretValues
|
|
}
|
|
|
|
webhookUrl: string
|
|
shouldNotify: boolean
|
|
totalSteps: number
|
|
currentStep = 0
|
|
messagePrefix: string
|
|
private secretValues?: string[]
|
|
|
|
private async send(method: string, url: string, body: unknown): Promise<any> {
|
|
try {
|
|
const response = await fetch(`${this.webhookUrl}${url}`, {
|
|
method,
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (!response.ok) {
|
|
console.warn(
|
|
`Discord webhook request failed: ${method} -> ${response.status} ${response.statusText}`
|
|
)
|
|
return null
|
|
}
|
|
return response.json()
|
|
} catch (err) {
|
|
const errString = err instanceof Error ? (err.stack ?? err.message) : String(err)
|
|
console.warn(
|
|
`Discord webhook request failed: ${method}\n${sanitizeVariables(errString, this.secretValues)}`
|
|
)
|
|
return null
|
|
}
|
|
}
|
|
|
|
async message(content: string, { always = false }: { always?: boolean } = {}) {
|
|
if (!always && !this.shouldNotify) {
|
|
return {
|
|
edit: () => {
|
|
// noop
|
|
},
|
|
}
|
|
}
|
|
|
|
const prefixedContent = this.messagePrefix ? `${this.messagePrefix} ${content}` : content
|
|
const message = await this.send('POST', '?wait=true', {
|
|
content: sanitizeVariables(prefixedContent, this.secretValues),
|
|
})
|
|
|
|
return {
|
|
edit: async (newContent: string) => {
|
|
if (!message?.id) return
|
|
const prefixedNewContent = this.messagePrefix
|
|
? `${this.messagePrefix} ${newContent}`
|
|
: newContent
|
|
await this.send('PATCH', `/messages/${message.id}`, {
|
|
content: sanitizeVariables(prefixedNewContent, this.secretValues),
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
async step<T>(content: string, cb: () => Promise<T>): Promise<T> {
|
|
this.currentStep++
|
|
const message = await this.message(`[${this.currentStep}/${this.totalSteps}] ${content}...`)
|
|
try {
|
|
const result = await cb()
|
|
await message.edit(`${content} ✅`)
|
|
return result
|
|
} catch (err) {
|
|
await message.edit(`${content} ❌`)
|
|
throw err
|
|
}
|
|
}
|
|
}
|