OverviewFilesCommitsIssuesDocsReleasesPeers

P2P-Hiverelay / test / unit / github-release-secrets-apply.test.js

318 lines · javascript ref:
import test from 'brittle'
import { execFile } from 'node:child_process'
import { chmod, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import path from 'node:path'

const TEST_GITHUB_TOKEN = `ghp_${'a'.repeat(36)}`
const TEST_GITHUB_TOKEN_ALT = `gho_${'b'.repeat(36)}`
const TEST_GITHUB_TOKEN_ECOSYSTEM = `ghu_${'e'.repeat(36)}`
const TEST_NPM_TOKEN = `npm_${'c'.repeat(36)}`
const TEST_PRIVATE_KEY = '-----BEGIN OPENSSH PRIVATE KEY-----\nfake-fleet-key\n-----END OPENSSH PRIVATE KEY-----'
const TEST_STARTOS_PRIVATE_KEY = '-----BEGIN PRIVATE KEY-----\nfake-startos-key\n-----END PRIVATE KEY-----'

function runApply (argv, env = {}) {
  return new Promise((resolve) => {
    execFile(process.execPath, ['scripts/apply-github-release-secrets.mjs', ...argv], {
      cwd: process.cwd(),
      env: {
        PATH: process.env.PATH || '',
        ...env
      },
      timeout: 10000
    }, (err, stdout, stderr) => {
      resolve({
        status: err && typeof err.code === 'number' ? err.code : 0,
        stdout,
        stderr
      })
    })
  })
}

async function candidateEnvFile (t, body) {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-release-apply-env-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })
  const file = path.join(dir, 'hiverelay-release-secrets.env')
  await writeFile(file, body)
  return file
}

function validCandidateEnvBody (overrides = {}) {
  const env = {
    FLEET_SSH_PRIVATE_KEY: TEST_PRIVATE_KEY,
    UMBREL_STORE_TOKEN: TEST_GITHUB_TOKEN,
    UMBREL_OFFICIAL_PR_TOKEN: TEST_GITHUB_TOKEN_ALT,
    ECOSYSTEM_CONSUMER_TOKEN: TEST_GITHUB_TOKEN_ECOSYSTEM,
    UMBREL_OFFICIAL_FORK: 'bigdestiny2/umbrel-apps',
    NPM_TOKEN: TEST_NPM_TOKEN,
    STARTOS_DEVELOPER_KEY_PEM: TEST_STARTOS_PRIVATE_KEY,
    STARTOS_REGISTRY_URL: 'https://registry.start9.com',
    ...overrides
  }
  const lines = [
    'FLEET_SSH_PRIVATE_KEY<<FLEET_KEY',
    env.FLEET_SSH_PRIVATE_KEY,
    'FLEET_KEY',
    `UMBREL_STORE_TOKEN=${env.UMBREL_STORE_TOKEN}`,
    `UMBREL_OFFICIAL_PR_TOKEN=${env.UMBREL_OFFICIAL_PR_TOKEN}`,
    `ECOSYSTEM_CONSUMER_TOKEN=${env.ECOSYSTEM_CONSUMER_TOKEN}`,
    `UMBREL_OFFICIAL_FORK=${env.UMBREL_OFFICIAL_FORK}`,
    `NPM_TOKEN=${env.NPM_TOKEN}`,
    'STARTOS_DEVELOPER_KEY_PEM<<STARTOS_KEY',
    env.STARTOS_DEVELOPER_KEY_PEM,
    'STARTOS_KEY',
    `STARTOS_REGISTRY_URL=${env.STARTOS_REGISTRY_URL}`
  ]
  if (env.FLEET_ROLLOUT_TIMEOUT_MS) lines.push(`FLEET_ROLLOUT_TIMEOUT_MS=${env.FLEET_ROLLOUT_TIMEOUT_MS}`)
  return lines.join('\n') + '\n'
}

async function fakeGh (t) {
  const dir = await mkdtemp(path.join(tmpdir(), 'hiverelay-apply-fake-gh-'))
  t.teardown(async () => {
    await rm(dir, { recursive: true, force: true })
  })
  const log = path.join(dir, 'gh.jsonl')
  const file = path.join(dir, 'gh')
  await writeFile(file, `#!/usr/bin/env node
const fs = require('fs')
const args = process.argv.slice(2)
const input = fs.readFileSync(0, 'utf8')
const row = { args, input }
fs.appendFileSync(process.env.HIVERELAY_FAKE_GH_LOG, JSON.stringify(row) + '\\n')
if (process.env.HIVERELAY_FAKE_GH_FAIL) {
  process.stderr.write(process.env.HIVERELAY_FAKE_GH_FAIL)
  process.exit(1)
}
if (process.env.HIVERELAY_FAKE_GH_FAIL_WITH_INPUT) {
  process.stderr.write(process.env.HIVERELAY_FAKE_GH_FAIL_WITH_INPUT + input)
  process.exit(1)
}
if (args[0] === 'secret' && args[1] === 'set') process.exit(0)
if (args[0] === 'variable' && args[1] === 'set') process.exit(0)
process.stderr.write('unexpected fake gh args: ' + args.join(' '))
process.exit(2)
`)
  await chmod(file, 0o755)
  return { file, log }
}

async function readGhLog (log) {
  const body = await readFile(log, 'utf8')
  return body.trim().split('\n').filter(Boolean).map(line => JSON.parse(line))
}

test('GitHub release secret apply dry-run validates candidate without printing values', async (t) => {
  const candidate = await candidateEnvFile(t, validCandidateEnvBody({
    FLEET_ROLLOUT_TIMEOUT_MS: '1800000'
  }))
  const res = await runApply([
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--dry-run'
  ])

  t.is(res.status, 0)
  t.ok(res.stdout.includes('Release value candidate file is valid'))
  t.ok(res.stdout.includes('Would set masked GitHub release values as Secrets: FLEET_SSH_PRIVATE_KEY'))
  t.ok(res.stdout.includes('Would set GitHub Variables: FLEET_ROLLOUT_TIMEOUT_MS'))
  t.ok(res.stdout.includes('After applying the candidate file, run:'))
  t.ok(res.stdout.includes('npm run release:check-github-setup -- --repo bigdestiny2/P2P-Hiverelay'))
  t.ok(res.stdout.includes('gh workflow run release-distribution-preflight.yml --repo bigdestiny2/P2P-Hiverelay -f channel=both -f prerelease=false'))
  t.absent(res.stdout.includes(TEST_GITHUB_TOKEN))
  t.absent(res.stdout.includes(TEST_PRIVATE_KEY))
  t.absent(res.stderr.includes(TEST_GITHUB_TOKEN))
})

test('GitHub release secret apply sends validated values to gh secret set via stdin', async (t) => {
  const candidate = await candidateEnvFile(t, validCandidateEnvBody({
    FLEET_ROLLOUT_TIMEOUT_MS: '1800000'
  }))
  const gh = await fakeGh(t)
  const res = await runApply([
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--gh', gh.file
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log
  })

  t.is(res.status, 0)
  t.ok(res.stdout.includes('Set masked GitHub release value UMBREL_STORE_TOKEN as a Secret'))
  t.ok(res.stdout.includes('Set GitHub Variable FLEET_ROLLOUT_TIMEOUT_MS'))
  t.ok(res.stdout.includes('GitHub release values applied.'))
  t.ok(res.stdout.includes('Next steps:'))
  t.ok(res.stdout.includes('npm run release:check-github-setup -- --repo bigdestiny2/P2P-Hiverelay'))
  t.ok(res.stdout.includes('gh workflow run release-distribution-preflight.yml --repo bigdestiny2/P2P-Hiverelay -f channel=both -f prerelease=false'))
  t.absent(res.stdout.includes(TEST_GITHUB_TOKEN))
  t.absent(res.stdout.includes(TEST_PRIVATE_KEY))

  const rows = await readGhLog(gh.log)
  t.is(rows.length, 9)
  const secretRows = rows.filter(row => row.args[0] === 'secret')
  const variableRows = rows.filter(row => row.args[0] === 'variable')
  t.is(secretRows.length, 8)
  t.is(variableRows.length, 1)
  t.ok(secretRows.some(row => row.args.join(' ') === 'secret set FLEET_SSH_PRIVATE_KEY --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_PRIVATE_KEY))
  t.ok(secretRows.some(row => row.args.join(' ') === 'secret set UMBREL_STORE_TOKEN --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_GITHUB_TOKEN))
  t.ok(secretRows.some(row => row.args.join(' ') === 'secret set ECOSYSTEM_CONSUMER_TOKEN --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_GITHUB_TOKEN_ECOSYSTEM))
  t.ok(secretRows.some(row => row.args.join(' ') === 'secret set NPM_TOKEN --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_NPM_TOKEN))
  t.ok(variableRows.some(row => row.args.join(' ') === 'variable set FLEET_ROLLOUT_TIMEOUT_MS --repo bigdestiny2/P2P-Hiverelay --body 1800000' && row.input === ''))
})

test('GitHub release secret apply repairs only issue 120 masked values', async (t) => {
  const candidate = await candidateEnvFile(t, [
    `UMBREL_STORE_TOKEN=${TEST_GITHUB_TOKEN}`,
    `UMBREL_OFFICIAL_PR_TOKEN=${TEST_GITHUB_TOKEN_ALT}`,
    'UMBREL_OFFICIAL_FORK=https://github.com/bigdestiny2/umbrel-apps.git',
    'STARTOS_REGISTRY_URL= https://registry.start9.com/startos/ '
  ].join('\n') + '\n')
  const gh = await fakeGh(t)
  const res = await runApply([
    '--issue-120-repair',
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--gh', gh.file
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log
  })

  t.is(res.status, 0)
  t.ok(res.stdout.includes('Set masked GitHub release value UMBREL_STORE_TOKEN as a Secret'))
  t.ok(res.stdout.includes('Set masked GitHub release value STARTOS_REGISTRY_URL as a Secret'))
  t.absent(res.stdout.includes('NPM_TOKEN'))
  t.absent(res.stdout.includes(TEST_GITHUB_TOKEN))

  const rows = await readGhLog(gh.log)
  t.is(rows.length, 4)
  t.ok(rows.every(row => row.args[0] === 'secret'))
  t.ok(rows.some(row => row.args.join(' ') === 'secret set UMBREL_STORE_TOKEN --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_GITHUB_TOKEN))
  t.ok(rows.some(row => row.args.join(' ') === 'secret set UMBREL_OFFICIAL_PR_TOKEN --repo bigdestiny2/P2P-Hiverelay' && row.input === TEST_GITHUB_TOKEN_ALT))
  t.ok(rows.some(row => row.args.join(' ') === 'secret set UMBREL_OFFICIAL_FORK --repo bigdestiny2/P2P-Hiverelay' && row.input === 'bigdestiny2/umbrel-apps'))
  t.ok(rows.some(row => row.args.join(' ') === 'secret set STARTOS_REGISTRY_URL --repo bigdestiny2/P2P-Hiverelay' && row.input === 'https://registry.start9.com/startos'))
})

test('GitHub release secret apply dry-runs only issue 120 masked values', async (t) => {
  const candidate = await candidateEnvFile(t, [
    `UMBREL_STORE_TOKEN=${TEST_GITHUB_TOKEN}`,
    `UMBREL_OFFICIAL_PR_TOKEN=${TEST_GITHUB_TOKEN_ALT}`,
    'UMBREL_OFFICIAL_FORK=bigdestiny2/umbrel-apps',
    'STARTOS_REGISTRY_URL=https://registry.start9.com'
  ].join('\n') + '\n')
  const res = await runApply([
    '--issue-120-repair',
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--dry-run'
  ])

  t.is(res.status, 0)
  t.ok(res.stdout.includes('Would set masked GitHub release values as Secrets: UMBREL_STORE_TOKEN, UMBREL_OFFICIAL_PR_TOKEN, UMBREL_OFFICIAL_FORK, STARTOS_REGISTRY_URL'))
  t.ok(res.stdout.includes('Issue #120 repair mode; no GitHub Variables would be changed.'))
  t.absent(res.stdout.includes('FLEET_SSH_PRIVATE_KEY'))
  t.absent(res.stdout.includes('NPM_TOKEN'))
  t.absent(res.stdout.includes(TEST_GITHUB_TOKEN))
})

test('GitHub release secret apply rejects malformed candidate without calling gh or echoing values', async (t) => {
  const secretValue = `ghp_${'s'.repeat(36)}`
  const candidate = await candidateEnvFile(t, validCandidateEnvBody({
    UMBREL_STORE_TOKEN: ` ${secretValue}`
  }))
  const gh = await fakeGh(t)
  const res = await runApply([
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--gh', gh.file
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log
  })

  t.is(res.status, 1)
  t.ok(res.stderr.includes('UMBREL_STORE_TOKEN must be a GitHub token'))
  t.absent(res.stdout.includes(secretValue))
  t.absent(res.stderr.includes(secretValue))

  let logMissing = false
  try {
    await readFile(gh.log, 'utf8')
  } catch (err) {
    logMissing = err && err.code === 'ENOENT'
  }
  t.ok(logMissing, 'fake gh was not called')
})

test('GitHub release secret apply rejects malformed repo names before gh calls', async (t) => {
  const candidate = await candidateEnvFile(t, validCandidateEnvBody())
  const gh = await fakeGh(t)
  const res = await runApply([
    '--repo', 'bad/repo\nHIVERELAY_ATTACKER_VALUE=owned',
    '--env-file', candidate,
    '--gh', gh.file
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log
  })

  t.is(res.status, 1)
  t.ok(res.stderr.includes('Invalid --repo'))
  t.absent(res.stderr.includes('HIVERELAY_ATTACKER_VALUE=owned'))

  let logMissing = false
  try {
    await readFile(gh.log, 'utf8')
  } catch (err) {
    logMissing = err && err.code === 'ENOENT'
  }
  t.ok(logMissing, 'fake gh was not called')
})

test('GitHub release secret apply rejects prerelease validation mode before gh calls', async (t) => {
  const candidate = await candidateEnvFile(t, validCandidateEnvBody())
  const gh = await fakeGh(t)
  const res = await runApply([
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--gh', gh.file,
    '--prerelease', 'true'
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log
  })

  t.is(res.status, 1)
  t.ok(res.stderr.includes('only validates full-release secret values'))
  t.absent(res.stdout.includes(TEST_GITHUB_TOKEN))
  t.absent(res.stderr.includes(TEST_GITHUB_TOKEN))

  let logMissing = false
  try {
    await readFile(gh.log, 'utf8')
  } catch (err) {
    logMissing = err && err.code === 'ENOENT'
  }
  t.ok(logMissing, 'fake gh was not called')
})

test('GitHub release secret apply redacts gh failure output before printing', async (t) => {
  const candidate = await candidateEnvFile(t, validCandidateEnvBody())
  const gh = await fakeGh(t)
  const res = await runApply([
    '--repo', 'bigdestiny2/P2P-Hiverelay',
    '--env-file', candidate,
    '--gh', gh.file
  ], {
    HIVERELAY_FAKE_GH_LOG: gh.log,
    HIVERELAY_FAKE_GH_FAIL_WITH_INPUT: `gh echoed token ${TEST_GITHUB_TOKEN_ALT} and stdin `
  })

  t.is(res.status, 1)
  t.ok(res.stderr.includes('gh secret set failed for secret FLEET_SSH_PRIVATE_KEY'))
  t.ok(res.stderr.includes('[redacted-github-token]'))
  t.ok(res.stderr.includes('[redacted-secret]'))
  t.absent(res.stderr.includes(TEST_GITHUB_TOKEN_ALT))
  t.absent(res.stderr.includes(TEST_PRIVATE_KEY))
  t.absent(res.stdout.includes(TEST_PRIVATE_KEY))
})