Harden release integration bootstrap gate

This commit is contained in:
rcourtman 2026-06-14 22:26:13 +01:00
parent 6169f3cea5
commit 377fd5131d
4 changed files with 168 additions and 3 deletions

View file

@ -9,7 +9,7 @@ on:
required: true
type: string
release_notes:
description: 'Release notes (markdown) - generated by Claude'
description: 'Release notes (markdown)'
required: true
type: string
promoted_from_tag:
@ -529,6 +529,19 @@ jobs:
node scripts/apply-entitlement-profile.mjs
echo "Validating seeded bootstrap token..."
BOOTSTRAP_STATUS=$(curl -s -o /tmp/bootstrap-token-validation.txt -w "%{http_code}" \
-X POST \
-H "Content-Type: application/json" \
--data "{\"token\":\"${PULSE_E2E_BOOTSTRAP_TOKEN}\"}" \
http://localhost:7655/api/security/validate-bootstrap-token || true)
echo "Bootstrap token validation endpoint returned HTTP ${BOOTSTRAP_STATUS}"
if [ "${BOOTSTRAP_STATUS}" != "204" ]; then
cat /tmp/bootstrap-token-validation.txt || true
docker logs pulse-test-server || true
exit 1
fi
echo "Running update API route smoke check..."
STATUS=$(curl -s -o /tmp/update-status.json -w "%{http_code}" http://localhost:7655/api/updates/status || true)
echo "Update status endpoint returned HTTP ${STATUS}"

View file

@ -6,10 +6,17 @@ services:
container_name: pulse-test-seed-bootstrap-token
environment:
- PULSE_E2E_BOOTSTRAP_TOKEN=${PULSE_E2E_BOOTSTRAP_TOKEN:-0123456789abcdef0123456789abcdef0123456789abcdef}
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
volumes:
- test-data:/data
command: >
sh -c "set -e; umask 077; echo \"$$PULSE_E2E_BOOTSTRAP_TOKEN\" > /data/.bootstrap_token"
sh -c "set -e;
umask 077;
printf '%s\n' \"$$PULSE_E2E_BOOTSTRAP_TOKEN\" > /data/.bootstrap_token;
chown \"$${PUID:-1000}:$${PGID:-1000}\" /data /data/.bootstrap_token;
chmod 700 /data;
chmod 600 /data/.bootstrap_token"
networks:
- test-network
@ -59,6 +66,8 @@ services:
- PULSE_MULTI_TENANT_ENABLED=${PULSE_MULTI_TENANT_ENABLED:-false}
- PULSE_ALLOW_DOCKER_UPDATES=true
- PULSE_UPDATE_STAGE_DELAY_MS=250
- PUID=${PUID:-1000}
- PGID=${PGID:-1000}
volumes:
- test-data:/data
extra_hosts:

View file

@ -31,6 +31,7 @@ const truthy = (value) => {
if (!value) return false;
return ['1', 'true', 'yes', 'on'].includes(String(value).trim().toLowerCase());
};
const trim = (value) => String(value ?? '').trim();
const shouldSkipDocker = truthy(process.env.PULSE_E2E_SKIP_DOCKER);
const shouldSkipPlaywrightInstall = truthy(process.env.PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL);
@ -145,6 +146,54 @@ export const waitForHealth = async (
throw new Error(`Timed out waiting for ${healthURL} after ${attempt} attempts`);
};
export const validateBootstrapToken = async (
baseURL,
bootstrapToken,
{ fetchImpl = fetch } = {},
) => {
const token = trim(bootstrapToken);
if (token === '') {
return;
}
const response = await fetchImpl(`${baseURL.replace(/\/+$/, '')}/api/security/validate-bootstrap-token`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token }),
});
if (!response.ok) {
const detail = typeof response.text === 'function' ? await response.text() : '';
throw new Error(
`Bootstrap token validation failed: HTTP ${response.status}${detail ? ` ${detail}` : ''}`,
);
}
};
export const validateBootstrapTokenForFirstRun = async (
baseURL,
bootstrapToken,
{ fetchImpl = fetch } = {},
) => {
const token = trim(bootstrapToken);
if (token === '') {
return;
}
const normalizedBaseURL = baseURL.replace(/\/+$/, '');
const statusResponse = await fetchImpl(`${normalizedBaseURL}/api/security/status`);
if (statusResponse.ok && typeof statusResponse.json === 'function') {
const status = await statusResponse.json().catch(() => null);
if (status?.hasAuthentication !== false) {
return;
}
}
await validateBootstrapToken(normalizedBaseURL, token, { fetchImpl });
};
export const main = async () => {
if (!shouldSkipPlaywrightInstall) {
await run(npxCmd, ['playwright', 'install', 'chromium']);
@ -191,6 +240,7 @@ export const main = async () => {
try {
await waitForHealth(`${baseURL}/api/health`);
console.log('[pretest] Health check passed!');
await validateBootstrapTokenForFirstRun(baseURL, process.env.PULSE_E2E_BOOTSTRAP_TOKEN);
await applyRequestedEntitlementProfile();
} catch (error) {
console.error('[pretest] Health check failed:', error.message);

View file

@ -1,7 +1,11 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { resolveHealthCheckStrategy } from './pretest.mjs';
import {
resolveHealthCheckStrategy,
validateBootstrapToken,
validateBootstrapTokenForFirstRun,
} from './pretest.mjs';
test('resolveHealthCheckStrategy uses plain HTTP mode for HTTP health URLs', async () => {
const strategy = await resolveHealthCheckStrategy('http://localhost:7655/api/health');
@ -49,3 +53,92 @@ test('resolveHealthCheckStrategy scopes insecure HTTPS mode to curl when availab
assert.equal(strategy.mode, 'curl');
assert.equal(strategy.parsedURL.protocol, 'https:');
});
test('validateBootstrapToken posts the deterministic token to the public validation route', async () => {
const calls = [];
await validateBootstrapToken('http://localhost:7655/', ' token-123 ', {
fetchImpl: async (url, options) => {
calls.push({ url, options });
return {
ok: true,
status: 204,
text: async () => '',
};
},
});
assert.deepEqual(calls, [
{
url: 'http://localhost:7655/api/security/validate-bootstrap-token',
options: {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ token: 'token-123' }),
},
},
]);
});
test('validateBootstrapToken fails with response detail when validation is rejected', async () => {
await assert.rejects(
() => validateBootstrapToken('http://localhost:7655', 'bad-token', {
fetchImpl: async () => ({
ok: false,
status: 409,
text: async () => 'Bootstrap token unavailable',
}),
}),
/HTTP 409 Bootstrap token unavailable/,
);
});
test('validateBootstrapTokenForFirstRun skips validation once auth is configured', async () => {
const calls = [];
await validateBootstrapTokenForFirstRun('http://localhost:7655', 'token-123', {
fetchImpl: async (url, options) => {
calls.push({ url, options });
return {
ok: true,
status: 200,
json: async () => ({ hasAuthentication: true }),
text: async () => '',
};
},
});
assert.deepEqual(calls, [
{
url: 'http://localhost:7655/api/security/status',
options: undefined,
},
]);
});
test('validateBootstrapTokenForFirstRun validates the seed token before first-run setup', async () => {
const calls = [];
await validateBootstrapTokenForFirstRun('http://localhost:7655', 'token-123', {
fetchImpl: async (url, options) => {
calls.push({ url, options });
if (url.endsWith('/api/security/status')) {
return {
ok: true,
status: 200,
json: async () => ({ hasAuthentication: false }),
text: async () => '',
};
}
return {
ok: true,
status: 204,
text: async () => '',
};
},
});
assert.equal(calls.length, 2);
assert.equal(calls[0].url, 'http://localhost:7655/api/security/status');
assert.equal(calls[1].url, 'http://localhost:7655/api/security/validate-bootstrap-token');
assert.equal(calls[1].options.body, JSON.stringify({ token: 'token-123' }));
});