Spaces:
Running
Running
Deploy fa03242 to Docker Space
Browse filesSource: MisonL/gpt-image-playground-customer@fa03242bd6b72fa8fe64e177f690cb8f81ba2a8c
- public/hf-space-deploy-marker.json +5 -0
- scripts/agent-doctor.mjs +8 -1
- scripts/command-center.test.mjs +90 -0
- scripts/deploy-hf-space.mjs +49 -5
- scripts/deploy-hf-space.test.mjs +27 -0
public/hf-space-deploy-marker.json
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"schema_version": 1,
|
| 3 |
+
"local_sha": "fa03242bd6b72fa8fe64e177f690cb8f81ba2a8c",
|
| 4 |
+
"created_at": "2026-06-20T10:52:58.463Z"
|
| 5 |
+
}
|
scripts/agent-doctor.mjs
CHANGED
|
@@ -350,7 +350,7 @@ function buildSummary({ capabilities, runtime, contract, smoke }) {
|
|
| 350 |
capabilities.ok && capabilities.body?.agent_streaming?.page_sse?.auth?.required === true
|
| 351 |
? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
|
| 352 |
: capabilities.ok,
|
| 353 |
-
page_sse_real_smoke:
|
| 354 |
responses_page_sse_generate_smoke: summarizeSmokeCheck(smoke, PAGE_SSE_GENERATE_SMOKE_NAME),
|
| 355 |
real_smoke_checks: summarizeSmokeChecks(smoke),
|
| 356 |
responses_gpt2image_ready:
|
|
@@ -371,6 +371,13 @@ function summarizeSmokeCheck(smoke, name) {
|
|
| 371 |
return check.ok ? 'passed' : 'failed';
|
| 372 |
}
|
| 373 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 374 |
function summarizeSmokeChecks(smoke) {
|
| 375 |
return {
|
| 376 |
agent_generate_1k: summarizeSmokeCheck(smoke, 'generate_1k'),
|
|
|
|
| 350 |
capabilities.ok && capabilities.body?.agent_streaming?.page_sse?.auth?.required === true
|
| 351 |
? Boolean(process.env.GPT_IMAGE_APP_PASSWORD_HASH)
|
| 352 |
: capabilities.ok,
|
| 353 |
+
page_sse_real_smoke: summarizePageSseSmoke(smoke),
|
| 354 |
responses_page_sse_generate_smoke: summarizeSmokeCheck(smoke, PAGE_SSE_GENERATE_SMOKE_NAME),
|
| 355 |
real_smoke_checks: summarizeSmokeChecks(smoke),
|
| 356 |
responses_gpt2image_ready:
|
|
|
|
| 371 |
return check.ok ? 'passed' : 'failed';
|
| 372 |
}
|
| 373 |
|
| 374 |
+
function summarizePageSseSmoke(smoke) {
|
| 375 |
+
const states = [PAGE_SSE_GENERATE_SMOKE_NAME, 'page_sse_edit_2k'].map((name) => summarizeSmokeCheck(smoke, name));
|
| 376 |
+
if (states.includes('failed')) return 'failed';
|
| 377 |
+
if (states.includes('passed')) return 'passed';
|
| 378 |
+
return 'skipped';
|
| 379 |
+
}
|
| 380 |
+
|
| 381 |
function summarizeSmokeChecks(smoke) {
|
| 382 |
return {
|
| 383 |
agent_generate_1k: summarizeSmokeCheck(smoke, 'generate_1k'),
|
scripts/command-center.test.mjs
CHANGED
|
@@ -882,6 +882,96 @@ describe('Command center scripts', () => {
|
|
| 882 |
);
|
| 883 |
});
|
| 884 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 885 |
it('prints localized agent:doctor help and option errors', async () => {
|
| 886 |
const help = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--help'], {
|
| 887 |
env: process.env,
|
|
|
|
| 882 |
);
|
| 883 |
});
|
| 884 |
|
| 885 |
+
it('reports failed agent:doctor page SSE smoke checks without hiding the failure', async () => {
|
| 886 |
+
await withServer(
|
| 887 |
+
(request, response) => {
|
| 888 |
+
if (request.url === '/api/agent/capabilities') {
|
| 889 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 890 |
+
response.end(
|
| 891 |
+
JSON.stringify({
|
| 892 |
+
defaults: { state_backend: 'memory' },
|
| 893 |
+
storage: { image_storage_mode: 'indexeddb', postgres_configured: false },
|
| 894 |
+
agent_streaming: { page_sse: { supported: true } },
|
| 895 |
+
agent_jobs: { supported: true },
|
| 896 |
+
routing_rules: {},
|
| 897 |
+
supported: {
|
| 898 |
+
image_backend_requirements: {
|
| 899 |
+
'responses-image-generation': {
|
| 900 |
+
supported: true,
|
| 901 |
+
enabled: true,
|
| 902 |
+
missing_env: []
|
| 903 |
+
}
|
| 904 |
+
}
|
| 905 |
+
}
|
| 906 |
+
})
|
| 907 |
+
);
|
| 908 |
+
return;
|
| 909 |
+
}
|
| 910 |
+
if (request.url === '/api/runtime-capabilities') {
|
| 911 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 912 |
+
response.end(
|
| 913 |
+
JSON.stringify({
|
| 914 |
+
streaming: {
|
| 915 |
+
defaultMode: 'auto',
|
| 916 |
+
unavailableMarkScope: 'channel+backend+strategy+operation'
|
| 917 |
+
},
|
| 918 |
+
streamingBatch: { enabled: true },
|
| 919 |
+
responsesImageBackend: { enabled: true, mode: 'experimental' }
|
| 920 |
+
})
|
| 921 |
+
);
|
| 922 |
+
return;
|
| 923 |
+
}
|
| 924 |
+
if (request.url === '/api/agent/images/generate') {
|
| 925 |
+
response.writeHead(200, { 'content-type': 'application/json' });
|
| 926 |
+
response.end(JSON.stringify({ images: [{ id: 'doctor-generate', filename: 'doctor.png' }] }));
|
| 927 |
+
return;
|
| 928 |
+
}
|
| 929 |
+
if (request.url === '/api/images') {
|
| 930 |
+
response.writeHead(503, { 'content-type': 'text/plain' });
|
| 931 |
+
response.end('503 Service temporarily unavailable');
|
| 932 |
+
return;
|
| 933 |
+
}
|
| 934 |
+
if (request.url === '/api/agent/jobs/images/generate') {
|
| 935 |
+
response.writeHead(400, { 'content-type': 'application/json' });
|
| 936 |
+
response.end(
|
| 937 |
+
JSON.stringify({
|
| 938 |
+
error: {
|
| 939 |
+
code: 'idempotency_key_required',
|
| 940 |
+
message: 'missing key',
|
| 941 |
+
retryable: false
|
| 942 |
+
}
|
| 943 |
+
})
|
| 944 |
+
);
|
| 945 |
+
return;
|
| 946 |
+
}
|
| 947 |
+
response.writeHead(404, { 'content-type': 'application/json' });
|
| 948 |
+
response.end(JSON.stringify({ error: 'missing' }));
|
| 949 |
+
},
|
| 950 |
+
async (baseUrl) => {
|
| 951 |
+
const result = await runNodeCommandAsync(
|
| 952 |
+
['scripts/agent-doctor.mjs', '--base-url', baseUrl, '--allow-billable'],
|
| 953 |
+
{ timeoutMs: 15_000 }
|
| 954 |
+
);
|
| 955 |
+
|
| 956 |
+
assert.equal(result.ok, false);
|
| 957 |
+
const body = parseJsonPayload(result.stdout, 'agent doctor');
|
| 958 |
+
assert.equal(body.ok, false);
|
| 959 |
+
assert.equal(body.summary.billable_smoke, 'failed');
|
| 960 |
+
assert.equal(body.summary.page_sse_real_smoke, 'failed');
|
| 961 |
+
assert.equal(body.summary.responses_page_sse_generate_smoke, 'failed');
|
| 962 |
+
assert.equal(body.summary.real_smoke_checks.agent_generate_1k, 'passed');
|
| 963 |
+
assert.equal(body.summary.real_smoke_checks.responses_page_sse_generate_1k, 'failed');
|
| 964 |
+
assert.equal(body.summary.real_smoke_checks.page_sse_edit_2k, 'skipped');
|
| 965 |
+
assert.match(
|
| 966 |
+
body.layers
|
| 967 |
+
.find((layer) => layer.name === 'billable_smoke')
|
| 968 |
+
.checks.find((check) => check.name === 'responses_page_sse_generate_1k').output,
|
| 969 |
+
/503 Service temporarily unavailable/
|
| 970 |
+
);
|
| 971 |
+
}
|
| 972 |
+
);
|
| 973 |
+
});
|
| 974 |
+
|
| 975 |
it('prints localized agent:doctor help and option errors', async () => {
|
| 976 |
const help = await runNodeCommandAsync(['scripts/agent-doctor.mjs', '--help'], {
|
| 977 |
env: process.env,
|
scripts/deploy-hf-space.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
|
| 3 |
import { spawnSync } from 'node:child_process';
|
| 4 |
-
import { mkdtempSync, rmSync } from 'node:fs';
|
| 5 |
import { tmpdir } from 'node:os';
|
| 6 |
import { join } from 'node:path';
|
| 7 |
import { setTimeout as delay } from 'node:timers/promises';
|
|
@@ -13,6 +13,8 @@ const STATUS_POLL_ATTEMPTS = 40;
|
|
| 13 |
const STATUS_POLL_INTERVAL_MS = 10_000;
|
| 14 |
const PUBLIC_ENDPOINT_TIMEOUT_MS = 10_000;
|
| 15 |
const HF_CLI_TIMEOUT_MS = 120_000;
|
|
|
|
|
|
|
| 16 |
export const GIT_ARCHIVE_MAX_BUFFER_BYTES = 256 * 1024 * 1024;
|
| 17 |
|
| 18 |
function parseArgs(argv) {
|
|
@@ -138,8 +140,30 @@ export function findRemoteDeletePaths(localFiles, remoteFiles) {
|
|
| 138 |
return [...remoteFiles].filter((filename) => !localFiles.has(filename)).sort();
|
| 139 |
}
|
| 140 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
function uploadSourceTree(sourceDir, localSha) {
|
| 142 |
-
|
|
|
|
| 143 |
const output = runText('hf', buildUploadArgs({ sourceDir, localSha, repoSlug: readRepositorySlug(), deletePaths }));
|
| 144 |
return extractUploadCommitSha(output);
|
| 145 |
}
|
|
@@ -149,7 +173,7 @@ function readSpaceInfo() {
|
|
| 149 |
return parseJsonPayload(output, 'hf spaces info');
|
| 150 |
}
|
| 151 |
|
| 152 |
-
async function waitForRunning(spaceCommitSha) {
|
| 153 |
let lastStage = 'unknown';
|
| 154 |
let lastSha = 'unknown';
|
| 155 |
for (let attempt = 1; attempt <= STATUS_POLL_ATTEMPTS; attempt += 1) {
|
|
@@ -162,13 +186,33 @@ async function waitForRunning(spaceCommitSha) {
|
|
| 162 |
}
|
| 163 |
await delay(STATUS_POLL_INTERVAL_MS);
|
| 164 |
}
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 166 |
}
|
| 167 |
|
| 168 |
async function fetchJson(path) {
|
| 169 |
return fetchJsonWithTimeout(new URL(path, HF_SPACE_URL), { timeoutMs: PUBLIC_ENDPOINT_TIMEOUT_MS });
|
| 170 |
}
|
| 171 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 172 |
async function verifyPublicEndpoints() {
|
| 173 |
const authStatus = await fetchJson('/api/auth-status');
|
| 174 |
const capabilities = await fetchJson('/api/agent/capabilities');
|
|
@@ -199,7 +243,7 @@ async function deploy() {
|
|
| 199 |
const sourceDir = prepareSourceTree();
|
| 200 |
try {
|
| 201 |
const spaceCommitSha = uploadSourceTree(sourceDir, localSha);
|
| 202 |
-
const runtime = await waitForRunning(spaceCommitSha);
|
| 203 |
const verification = await verifyPublicEndpoints();
|
| 204 |
console.log(
|
| 205 |
JSON.stringify(
|
|
|
|
| 1 |
#!/usr/bin/env node
|
| 2 |
|
| 3 |
import { spawnSync } from 'node:child_process';
|
| 4 |
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
| 5 |
import { tmpdir } from 'node:os';
|
| 6 |
import { join } from 'node:path';
|
| 7 |
import { setTimeout as delay } from 'node:timers/promises';
|
|
|
|
| 13 |
const STATUS_POLL_INTERVAL_MS = 10_000;
|
| 14 |
const PUBLIC_ENDPOINT_TIMEOUT_MS = 10_000;
|
| 15 |
const HF_CLI_TIMEOUT_MS = 120_000;
|
| 16 |
+
const DEPLOY_MARKER_REPO_PATH = 'public/hf-space-deploy-marker.json';
|
| 17 |
+
const DEPLOY_MARKER_PUBLIC_PATH = '/hf-space-deploy-marker.json';
|
| 18 |
export const GIT_ARCHIVE_MAX_BUFFER_BYTES = 256 * 1024 * 1024;
|
| 19 |
|
| 20 |
function parseArgs(argv) {
|
|
|
|
| 140 |
return [...remoteFiles].filter((filename) => !localFiles.has(filename)).sort();
|
| 141 |
}
|
| 142 |
|
| 143 |
+
export function buildDeployMarker(localSha, createdAt = new Date()) {
|
| 144 |
+
if (!/^[0-9a-f]{40}$/.test(String(localSha || ''))) throw new Error('localSha must be a full git commit SHA.');
|
| 145 |
+
return {
|
| 146 |
+
schema_version: 1,
|
| 147 |
+
local_sha: localSha,
|
| 148 |
+
created_at: createdAt.toISOString()
|
| 149 |
+
};
|
| 150 |
+
}
|
| 151 |
+
|
| 152 |
+
function writeDeployMarker(sourceDir, marker) {
|
| 153 |
+
const markerPath = join(sourceDir, DEPLOY_MARKER_REPO_PATH);
|
| 154 |
+
mkdirSync(join(sourceDir, 'public'), { recursive: true });
|
| 155 |
+
writeFileSync(markerPath, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
function readLocalGitFilesWithDeployMarker() {
|
| 159 |
+
const files = readLocalGitFiles();
|
| 160 |
+
files.add(DEPLOY_MARKER_REPO_PATH);
|
| 161 |
+
return files;
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
function uploadSourceTree(sourceDir, localSha) {
|
| 165 |
+
writeDeployMarker(sourceDir, buildDeployMarker(localSha));
|
| 166 |
+
const deletePaths = findRemoteDeletePaths(readLocalGitFilesWithDeployMarker(), readRemoteFilePaths());
|
| 167 |
const output = runText('hf', buildUploadArgs({ sourceDir, localSha, repoSlug: readRepositorySlug(), deletePaths }));
|
| 168 |
return extractUploadCommitSha(output);
|
| 169 |
}
|
|
|
|
| 173 |
return parseJsonPayload(output, 'hf spaces info');
|
| 174 |
}
|
| 175 |
|
| 176 |
+
async function waitForRunning(spaceCommitSha, localSha) {
|
| 177 |
let lastStage = 'unknown';
|
| 178 |
let lastSha = 'unknown';
|
| 179 |
for (let attempt = 1; attempt <= STATUS_POLL_ATTEMPTS; attempt += 1) {
|
|
|
|
| 186 |
}
|
| 187 |
await delay(STATUS_POLL_INTERVAL_MS);
|
| 188 |
}
|
| 189 |
+
const marker = await verifyDeployMarker(localSha);
|
| 190 |
+
return {
|
| 191 |
+
stage: lastStage,
|
| 192 |
+
sha: lastSha,
|
| 193 |
+
management_status: 'runtime_stage_not_running',
|
| 194 |
+
service_marker_verified: true,
|
| 195 |
+
warning: `Space did not reach RUNNING for ${spaceCommitSha}; last stage=${lastStage} sha=${lastSha}`,
|
| 196 |
+
marker
|
| 197 |
+
};
|
| 198 |
}
|
| 199 |
|
| 200 |
async function fetchJson(path) {
|
| 201 |
return fetchJsonWithTimeout(new URL(path, HF_SPACE_URL), { timeoutMs: PUBLIC_ENDPOINT_TIMEOUT_MS });
|
| 202 |
}
|
| 203 |
|
| 204 |
+
async function verifyDeployMarker(localSha) {
|
| 205 |
+
const marker = await fetchJson(`${DEPLOY_MARKER_PUBLIC_PATH}?t=${Date.now()}`);
|
| 206 |
+
if (marker?.schema_version !== 1) throw new Error('deploy marker schema_version was not 1.');
|
| 207 |
+
if (marker.local_sha !== localSha) {
|
| 208 |
+
throw new Error(`deploy marker local_sha mismatch: expected ${localSha}, received ${marker.local_sha || 'missing'}.`);
|
| 209 |
+
}
|
| 210 |
+
if (typeof marker.created_at !== 'string' || !marker.created_at.trim()) {
|
| 211 |
+
throw new Error('deploy marker created_at was missing.');
|
| 212 |
+
}
|
| 213 |
+
return marker;
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
async function verifyPublicEndpoints() {
|
| 217 |
const authStatus = await fetchJson('/api/auth-status');
|
| 218 |
const capabilities = await fetchJson('/api/agent/capabilities');
|
|
|
|
| 243 |
const sourceDir = prepareSourceTree();
|
| 244 |
try {
|
| 245 |
const spaceCommitSha = uploadSourceTree(sourceDir, localSha);
|
| 246 |
+
const runtime = await waitForRunning(spaceCommitSha, localSha);
|
| 247 |
const verification = await verifyPublicEndpoints();
|
| 248 |
console.log(
|
| 249 |
JSON.stringify(
|
scripts/deploy-hf-space.test.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import { describe, it } from 'node:test';
|
|
| 3 |
|
| 4 |
import {
|
| 5 |
GIT_ARCHIVE_MAX_BUFFER_BYTES,
|
|
|
|
| 6 |
buildUploadArgs,
|
| 7 |
extractUploadCommitSha,
|
| 8 |
findRemoteDeletePaths,
|
|
@@ -125,4 +126,30 @@ describe('HF Space deploy script', () => {
|
|
| 125 |
['old.md']
|
| 126 |
);
|
| 127 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 128 |
});
|
|
|
|
| 3 |
|
| 4 |
import {
|
| 5 |
GIT_ARCHIVE_MAX_BUFFER_BYTES,
|
| 6 |
+
buildDeployMarker,
|
| 7 |
buildUploadArgs,
|
| 8 |
extractUploadCommitSha,
|
| 9 |
findRemoteDeletePaths,
|
|
|
|
| 126 |
['old.md']
|
| 127 |
);
|
| 128 |
});
|
| 129 |
+
|
| 130 |
+
it('builds a non-secret deploy marker for service-side deployment verification', () => {
|
| 131 |
+
const marker = buildDeployMarker('3333333333333333333333333333333333333333', new Date('2026-06-20T10:00:00.000Z'));
|
| 132 |
+
|
| 133 |
+
assert.deepEqual(marker, {
|
| 134 |
+
schema_version: 1,
|
| 135 |
+
local_sha: '3333333333333333333333333333333333333333',
|
| 136 |
+
created_at: '2026-06-20T10:00:00.000Z'
|
| 137 |
+
});
|
| 138 |
+
assert.doesNotMatch(JSON.stringify(marker), /key|token|secret|password/i);
|
| 139 |
+
});
|
| 140 |
+
|
| 141 |
+
it('rejects short deploy marker commit shas before upload', () => {
|
| 142 |
+
assert.throws(() => buildDeployMarker('3333333'), /full git commit SHA/);
|
| 143 |
+
});
|
| 144 |
+
|
| 145 |
+
it('keeps the generated deploy marker from being deleted on upload', () => {
|
| 146 |
+
assert.deepEqual(
|
| 147 |
+
findRemoteDeletePaths(new Set(['README.md', 'public/hf-space-deploy-marker.json']), [
|
| 148 |
+
'README.md',
|
| 149 |
+
'public/hf-space-deploy-marker.json',
|
| 150 |
+
'old.md'
|
| 151 |
+
]),
|
| 152 |
+
['old.md']
|
| 153 |
+
);
|
| 154 |
+
});
|
| 155 |
});
|