[READ-ONLY] Mirror of https://github.com/thoda-dev/shhh. Self-hostable zero-knowledge pastebin for secrets that expire on their own
docker end-to-end-encryption nuxt nuxtjs pastebin secrets selft-hosted zero-knowledge
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201import { spawnSync } from 'node:child_process'import { readFileSync } from 'node:fs'import { createInterface } from 'node:readline/promises'import { consola } from 'consola'
/** * Publishing the documentation image on its own, between releases. The docs move far more often than * the code, and cutting a version of shhh to fix a sentence means rebuilding both images and * announcing a release that contains no change to the app. * * Moves `:latest` and adds an immutable `:sha-<short>`. Never writes the version tags — `1.1.9`, * `1.1`, `1` — which belong to `pnpm release`: they mean "the documentation as it shipped with that * version of the app", and that only stays true as long as nothing rewrites them. * * Helpers are duplicated from release.mjs rather than extracted: the release path works, and a * convenience script is no reason to touch it. * * Usage: pnpm release:docs [options] * --dry-run runs nothing irreversible, prints the commands instead * --yes skips the confirmation * --skip-checks does not rebuild the docs locally first * --allow-branch allows a branch other than master */
const DOCS_IMAGE = 'thodadev/shhh-docs'const GHCR_DOCS_IMAGE = 'ghcr.io/thoda-dev/shhh-docs'const PLATFORMS = 'linux/amd64,linux/arm64'const BUILDER = 'shhh-release'const BRANCH = 'master'const REPO_URL = 'https://github.com/thoda-dev/shhh'
const flags = new Set(process.argv.slice(2).filter(a => a.startsWith('--')))const dryRun = flags.has('--dry-run')
function run(command, cmdArgs, options = {}) { const printable = `${command} ${cmdArgs.join(' ')}` if (dryRun && !options.readOnly) { consola.log(` [dry-run] ${printable}`) return { status: 0, stdout: '' } } const result = spawnSync(command, cmdArgs, { stdio: options.capture ? 'pipe' : 'inherit', encoding: 'utf8' }) if (result.status !== 0 && !options.allowFailure) { consola.error(`Échec : ${printable}`) if (options.capture && result.stderr) consola.log(result.stderr.trim()) process.exit(1) } return { status: result.status, stdout: (result.stdout ?? '').trim() }}
// Read-only, so never stubbed out by --dry-run — otherwise the preflight would check nothing.function capture(command, cmdArgs) { return run(command, cmdArgs, { capture: true, allowFailure: true, readOnly: true }).stdout}
// No `shell: true`: Node deprecates passing args to a shell (DEP0190), and a missing binary reports itself through `error`, not an exit code.function has(binary) { return spawnSync(binary, ['--version'], { stdio: 'ignore' }).error === undefined}
// ---------------------------------------------------------------- preflight
consola.start('Préparation de la publication des docs')
const problems = []
if (capture('git', ['rev-parse', '--is-inside-work-tree']) !== 'true') { problems.push('pas dans un dépôt git')}// The image is tagged with the commit it was built from, so a dirty tree would produce a `sha-` tag// that describes something nobody can check out.if (capture('git', ['status', '--porcelain'])) { problems.push('arbre de travail non propre — commite ou remise tes modifications')}
const branch = capture('git', ['rev-parse', '--abbrev-ref', 'HEAD'])if (branch !== BRANCH && !flags.has('--allow-branch')) { problems.push(`branche ${branch} au lieu de ${BRANCH} (--allow-branch pour passer outre)`)}
consola.info('Synchronisation avec origin…')run('git', ['fetch', '--quiet', 'origin'], { readOnly: true, allowFailure: true })
const local = capture('git', ['rev-parse', 'HEAD'])// `--verify --quiet` because a bare `rev-parse` echoes an unknown ref back on stdout: without it a// never-pushed branch compares a sha against the string `origin/<branch>` and reports itself as// desynchronised, with empty ahead/behind counts.const remote = capture('git', ['rev-parse', '--verify', '--quiet', `origin/${branch}`])// Same reason as the dirty tree: a `sha-` tag naming a commit nobody can fetch is a dead reference,// and a branch missing from origin is the worst case of that.if (!remote) { problems.push(`branche ${branch} jamais poussée sur origin — pousse-la avant de publier`)} else if (local !== remote) { const ahead = capture('git', ['rev-list', '--count', `origin/${branch}..HEAD`]) const behind = capture('git', ['rev-list', '--count', `HEAD..origin/${branch}`]) problems.push(`branche désynchronisée avec origin (${ahead} devant, ${behind} derrière) — pousse avant de publier`)}
if (!has('docker')) { problems.push('docker introuvable')} else if (capture('docker', ['buildx', 'version']) === '') { problems.push('docker buildx introuvable')}
// A `credsStore` entry proves nothing — Docker Desktop always writes it. Only the keychain can say which registries are actually in there.function loggedInRegistries() { let config try { config = JSON.parse(readFileSync(`${process.env.HOME}/.docker/config.json`, 'utf8')) } catch { return [] } return config.credsStore ? Object.keys(JSON.parse(capture(`docker-credential-${config.credsStore}`, ['list']) || '{}')) : Object.keys(config.auths ?? {})}
// Both checked before the build: a push landing on one registry only would leave the two drifting.const registries = loggedInRegistries()if (!registries.some(r => r.includes('docker.io'))) { problems.push('pas de session Docker Hub — lance `docker login`')}if (!registries.some(r => r.includes('ghcr.io'))) { problems.push('pas de session GHCR — lance `gh auth token | docker login ghcr.io -u <utilisateur> --password-stdin` (le jeton a besoin du scope write:packages)')}
if (problems.length) { consola.error('Préflight échoué :') for (const p of problems) consola.log(` · ${p}`) process.exit(1)}
// ---------------------------------------------------------------- confirmation
const sha = capture('git', ['rev-parse', 'HEAD'])const shortSha = sha.slice(0, 7)const shaTag = `sha-${shortSha}`const subject = capture('git', ['log', '-1', '--pretty=%s'])
const tags = [DOCS_IMAGE, GHCR_DOCS_IMAGE].flatMap(repo => [`${repo}:latest`, `${repo}:${shaTag}`])
consola.box([ `Commit ${shortSha} ${subject}`, `Image ${tags.join('\n ')}`, `Plateformes ${PLATFORMS}`, '', 'Les tags de version (1.1.9, 1.1, 1) ne sont pas touchés : ils appartiennent à `pnpm release`.'].join('\n'))
if (!flags.has('--yes') && !dryRun) { const rl = createInterface({ input: process.stdin, output: process.stdout }) const answer = await rl.question('Confirmer la publication de l\'image docs ? (o/N) ') rl.close() if (!['o', 'oui', 'y', 'yes'].includes(answer.trim().toLowerCase())) { consola.error('Annulé.') process.exit(1) }}
// ---------------------------------------------------------------- checks
// The image rebuilds from scratch anyway; this only fails in two minutes instead of ten.if (!flags.has('--skip-checks')) { consola.start('Build des docs') run('pnpm', ['install', '--frozen-lockfile']) run('pnpm', ['build:docs']) consola.success('Build passé')}
// ---------------------------------------------------------------- Docker image
// The default `docker` driver can't produce a multi-architecture manifest, so a dedicated `docker-container` builder is created rather than touching the current one.// `buildx ls` prints a table, and a substring search on it would match a builder named// `shhh-release2`; `inspect` resolves the name exactly and stays silent when there is none.if (!capture('docker', ['buildx', 'inspect', BUILDER])) { consola.info(`Création du builder ${BUILDER} (multi-architecture)`) run('docker', ['buildx', 'create', '--name', BUILDER, '--driver', 'docker-container'])}
consola.start(`Build et push de l'image docs (${PLATFORMS}) — c'est l'étape longue`)run('docker', [ 'buildx', 'build', '--builder', BUILDER, '--platform', PLATFORMS, '-f', 'docker/docs.Dockerfile', ...tags.flatMap(t => ['-t', t]), // Not the app's version: this image is not the one that shipped with it, and saying so would undo // the whole point of leaving the version tags alone. '--label', `org.opencontainers.image.version=${shaTag}`, '--label', `org.opencontainers.image.revision=${sha}`, '--label', `org.opencontainers.image.source=${REPO_URL}`, '--label', 'org.opencontainers.image.licenses=MIT', '--push', '.'])
consola.success(`Image publiée : ${tags.join(', ')}`)consola.info('Rien à annuler : aucun commit, aucun tag git, aucune release.')