misonL commited on
Commit
6825c0c
·
verified ·
1 Parent(s): 5599c48

Deploy a623efc to Docker Space

Browse files

Source: MisonL/gpt-image-playground-customer@a623efcc28ea071e4a51a2753173829a0ee01002

public/hf-space-deploy-marker.json CHANGED
@@ -1,5 +1,6 @@
1
  {
2
  "schema_version": 1,
3
- "local_sha": "9bbfa7b63748719962755d5bcd75a62ce2415c5a",
4
- "created_at": "2026-06-20T13:57:39.833Z"
 
5
  }
 
1
  {
2
  "schema_version": 1,
3
+ "local_sha": "a623efcc28ea071e4a51a2753173829a0ee01002",
4
+ "created_at": "2026-06-21T06:16:41.993Z",
5
+ "deploy_id": "83a9c54d-c4df-43fc-8045-97fe4feadb16"
6
  }
scripts/deploy-hf-space.mjs CHANGED
@@ -1,6 +1,7 @@
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';
@@ -141,15 +142,34 @@ export function findRemoteDeletePaths(localFiles, remoteFiles) {
141
  return [...remoteFiles].filter((filename) => !localFiles.has(filename)).sort();
142
  }
143
 
144
- export function buildDeployMarker(localSha, createdAt = new Date()) {
145
  if (!/^[0-9a-f]{40}$/.test(String(localSha || ''))) throw new Error('localSha must be a full git commit SHA.');
 
 
 
146
  return {
147
  schema_version: 1,
148
  local_sha: localSha,
149
- created_at: createdAt.toISOString()
 
150
  };
151
  }
152
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
153
  export function buildDeployMarkerRouteSource(marker) {
154
  const markerJson = JSON.stringify(marker);
155
  return `import { NextResponse } from 'next/server';
@@ -184,10 +204,10 @@ function readLocalGitFilesWithDeployMarker() {
184
  return files;
185
  }
186
 
187
- function uploadSourceTree(sourceDir, localSha) {
188
- writeDeployMarker(sourceDir, buildDeployMarker(localSha));
189
  const deletePaths = findRemoteDeletePaths(readLocalGitFilesWithDeployMarker(), readRemoteFilePaths());
190
- const output = runText('hf', buildUploadArgs({ sourceDir, localSha, repoSlug: readRepositorySlug(), deletePaths }));
191
  return extractUploadCommitSha(output);
192
  }
193
 
@@ -196,26 +216,39 @@ function readSpaceInfo() {
196
  return parseJsonPayload(output, 'hf spaces info');
197
  }
198
 
199
- async function waitForRunning(spaceCommitSha, localSha) {
 
 
 
 
 
 
200
  let lastStage = 'unknown';
201
  let lastSha = 'unknown';
202
- for (let attempt = 1; attempt <= STATUS_POLL_ATTEMPTS; attempt += 1) {
203
- const info = readSpaceInfo();
 
204
  lastStage = info.runtime?.stage || 'unknown';
205
  lastSha = info.sha || info.runtime?.raw?.sha || 'unknown';
206
- console.log(`attempt=${attempt} stage=${lastStage} sha=${lastSha}`);
207
  if (lastStage === 'RUNNING' && lastSha === spaceCommitSha) {
208
- return { stage: lastStage, sha: lastSha };
 
 
 
 
 
 
209
  }
210
- await delay(STATUS_POLL_INTERVAL_MS);
211
  }
212
- const marker = await verifyDeployMarker(localSha);
213
  return {
214
  stage: lastStage,
215
  sha: lastSha,
216
  management_status: 'runtime_stage_not_running',
217
  service_marker_verified: true,
218
- warning: `Space did not reach RUNNING for ${spaceCommitSha}; last stage=${lastStage} sha=${lastSha}`,
219
  marker
220
  };
221
  }
@@ -224,16 +257,9 @@ async function fetchJson(path) {
224
  return fetchJsonWithTimeout(new URL(path, HF_SPACE_URL), { timeoutMs: PUBLIC_ENDPOINT_TIMEOUT_MS });
225
  }
226
 
227
- async function verifyDeployMarker(localSha) {
228
  const marker = await fetchJson(`${DEPLOY_MARKER_SERVICE_PATH}?t=${Date.now()}`);
229
- if (marker?.schema_version !== 1) throw new Error('deploy marker schema_version was not 1.');
230
- if (marker.local_sha !== localSha) {
231
- throw new Error(`deploy marker local_sha mismatch: expected ${localSha}, received ${marker.local_sha || 'missing'}.`);
232
- }
233
- if (typeof marker.created_at !== 'string' || !marker.created_at.trim()) {
234
- throw new Error('deploy marker created_at was missing.');
235
- }
236
- return marker;
237
  }
238
 
239
  async function verifyPublicEndpoints() {
@@ -263,10 +289,11 @@ async function deploy() {
263
  assertCleanGitWorktree();
264
  runText('hf', ['auth', 'whoami']);
265
  const localSha = runText('git', ['rev-parse', 'HEAD']);
 
266
  const sourceDir = prepareSourceTree();
267
  try {
268
- const spaceCommitSha = uploadSourceTree(sourceDir, localSha);
269
- const runtime = await waitForRunning(spaceCommitSha, localSha);
270
  const verification = await verifyPublicEndpoints();
271
  console.log(
272
  JSON.stringify(
 
1
  #!/usr/bin/env node
2
 
3
  import { spawnSync } from 'node:child_process';
4
+ import { randomUUID } from 'node:crypto';
5
  import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
6
  import { tmpdir } from 'node:os';
7
  import { join } from 'node:path';
 
142
  return [...remoteFiles].filter((filename) => !localFiles.has(filename)).sort();
143
  }
144
 
145
+ export function buildDeployMarker(localSha, createdAt = new Date(), deployId = randomUUID()) {
146
  if (!/^[0-9a-f]{40}$/.test(String(localSha || ''))) throw new Error('localSha must be a full git commit SHA.');
147
+ if (typeof deployId !== 'string' || !deployId.trim() || /[\r\n]/.test(deployId)) {
148
+ throw new Error('deployId must be a non-empty single-line string.');
149
+ }
150
  return {
151
  schema_version: 1,
152
  local_sha: localSha,
153
+ created_at: createdAt.toISOString(),
154
+ deploy_id: deployId
155
  };
156
  }
157
 
158
+ export function assertDeployMarkerMatches(marker, expectedMarker) {
159
+ if (!marker || typeof marker !== 'object') throw new Error('deploy marker response was not an object.');
160
+ if (marker?.schema_version !== expectedMarker.schema_version) throw new Error('deploy marker schema_version mismatch.');
161
+ if (marker.local_sha !== expectedMarker.local_sha) {
162
+ throw new Error(`deploy marker local_sha mismatch: expected ${expectedMarker.local_sha}, received ${marker.local_sha || 'missing'}.`);
163
+ }
164
+ if (marker.created_at !== expectedMarker.created_at) {
165
+ throw new Error(`deploy marker created_at mismatch: expected ${expectedMarker.created_at}, received ${marker.created_at || 'missing'}.`);
166
+ }
167
+ if (marker.deploy_id !== expectedMarker.deploy_id) {
168
+ throw new Error(`deploy marker deploy_id mismatch: expected ${expectedMarker.deploy_id}, received ${marker.deploy_id || 'missing'}.`);
169
+ }
170
+ return marker;
171
+ }
172
+
173
  export function buildDeployMarkerRouteSource(marker) {
174
  const markerJson = JSON.stringify(marker);
175
  return `import { NextResponse } from 'next/server';
 
204
  return files;
205
  }
206
 
207
+ function uploadSourceTree(sourceDir, deployMarker) {
208
+ writeDeployMarker(sourceDir, deployMarker);
209
  const deletePaths = findRemoteDeletePaths(readLocalGitFilesWithDeployMarker(), readRemoteFilePaths());
210
+ const output = runText('hf', buildUploadArgs({ sourceDir, localSha: deployMarker.local_sha, repoSlug: readRepositorySlug(), deletePaths }));
211
  return extractUploadCommitSha(output);
212
  }
213
 
 
216
  return parseJsonPayload(output, 'hf spaces info');
217
  }
218
 
219
+ export async function waitForRunning(spaceCommitSha, deployMarker, options = {}) {
220
+ const attempts = options.attempts || STATUS_POLL_ATTEMPTS;
221
+ const intervalMs = options.intervalMs ?? STATUS_POLL_INTERVAL_MS;
222
+ const readInfo = options.readInfo || readSpaceInfo;
223
+ const verifyMarker = options.verifyMarker || verifyDeployMarker;
224
+ const sleep = options.sleep || delay;
225
+ const log = options.log || console.log;
226
  let lastStage = 'unknown';
227
  let lastSha = 'unknown';
228
+ let lastMarkerError = 'unknown';
229
+ for (let attempt = 1; attempt <= attempts; attempt += 1) {
230
+ const info = readInfo();
231
  lastStage = info.runtime?.stage || 'unknown';
232
  lastSha = info.sha || info.runtime?.raw?.sha || 'unknown';
233
+ log(`attempt=${attempt} stage=${lastStage} sha=${lastSha}`);
234
  if (lastStage === 'RUNNING' && lastSha === spaceCommitSha) {
235
+ try {
236
+ const marker = await verifyMarker(deployMarker);
237
+ return { stage: lastStage, sha: lastSha, service_marker_verified: true, marker };
238
+ } catch (error) {
239
+ lastMarkerError = error instanceof Error ? error.message : String(error);
240
+ log(`attempt=${attempt} marker_status=not_ready error=${lastMarkerError}`);
241
+ }
242
  }
243
+ await sleep(intervalMs);
244
  }
245
+ const marker = await verifyMarker(deployMarker);
246
  return {
247
  stage: lastStage,
248
  sha: lastSha,
249
  management_status: 'runtime_stage_not_running',
250
  service_marker_verified: true,
251
+ warning: `Space did not reach RUNNING with a matching service marker for ${spaceCommitSha}; last stage=${lastStage} sha=${lastSha} marker_error=${lastMarkerError}`,
252
  marker
253
  };
254
  }
 
257
  return fetchJsonWithTimeout(new URL(path, HF_SPACE_URL), { timeoutMs: PUBLIC_ENDPOINT_TIMEOUT_MS });
258
  }
259
 
260
+ async function verifyDeployMarker(expectedMarker) {
261
  const marker = await fetchJson(`${DEPLOY_MARKER_SERVICE_PATH}?t=${Date.now()}`);
262
+ return assertDeployMarkerMatches(marker, expectedMarker);
 
 
 
 
 
 
 
263
  }
264
 
265
  async function verifyPublicEndpoints() {
 
289
  assertCleanGitWorktree();
290
  runText('hf', ['auth', 'whoami']);
291
  const localSha = runText('git', ['rev-parse', 'HEAD']);
292
+ const deployMarker = buildDeployMarker(localSha);
293
  const sourceDir = prepareSourceTree();
294
  try {
295
+ const spaceCommitSha = uploadSourceTree(sourceDir, deployMarker);
296
+ const runtime = await waitForRunning(spaceCommitSha, deployMarker);
297
  const verification = await verifyPublicEndpoints();
298
  console.log(
299
  JSON.stringify(
scripts/deploy-hf-space.test.mjs CHANGED
@@ -3,12 +3,14 @@ import { describe, it } from 'node:test';
3
 
4
  import {
5
  GIT_ARCHIVE_MAX_BUFFER_BYTES,
 
6
  buildDeployMarker,
7
  buildDeployMarkerRouteSource,
8
  buildUploadArgs,
9
  extractUploadCommitSha,
10
  findRemoteDeletePaths,
11
- parseRepositorySlug
 
12
  } from './deploy-hf-space.mjs';
13
 
14
  describe('HF Space deploy script', () => {
@@ -129,28 +131,121 @@ describe('HF Space deploy script', () => {
129
  });
130
 
131
  it('builds a non-secret deploy marker for service-side deployment verification', () => {
132
- const marker = buildDeployMarker('3333333333333333333333333333333333333333', new Date('2026-06-20T10:00:00.000Z'));
 
 
 
 
133
 
134
  assert.deepEqual(marker, {
135
  schema_version: 1,
136
  local_sha: '3333333333333333333333333333333333333333',
137
- created_at: '2026-06-20T10:00:00.000Z'
 
138
  });
139
  assert.doesNotMatch(JSON.stringify(marker), /key|token|secret|password/i);
140
  });
141
 
 
 
 
 
 
 
 
 
142
  it('rejects short deploy marker commit shas before upload', () => {
143
  assert.throws(() => buildDeployMarker('3333333'), /full git commit SHA/);
144
  });
145
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
  it('builds a no-store API route for service-side deploy marker verification', () => {
147
- const marker = buildDeployMarker('4444444444444444444444444444444444444444', new Date('2026-06-20T11:00:00.000Z'));
 
 
 
 
148
  const source = buildDeployMarkerRouteSource(marker);
149
 
150
  assert.match(source, /NextResponse\.json/);
151
  assert.match(source, /Cache-Control/);
152
  assert.match(source, /no-store/);
153
  assert.match(source, /4444444444444444444444444444444444444444/);
 
154
  assert.doesNotMatch(source, /key|token|secret|password/i);
155
  });
156
 
 
3
 
4
  import {
5
  GIT_ARCHIVE_MAX_BUFFER_BYTES,
6
+ assertDeployMarkerMatches,
7
  buildDeployMarker,
8
  buildDeployMarkerRouteSource,
9
  buildUploadArgs,
10
  extractUploadCommitSha,
11
  findRemoteDeletePaths,
12
+ parseRepositorySlug,
13
+ waitForRunning
14
  } from './deploy-hf-space.mjs';
15
 
16
  describe('HF Space deploy script', () => {
 
131
  });
132
 
133
  it('builds a non-secret deploy marker for service-side deployment verification', () => {
134
+ const marker = buildDeployMarker(
135
+ '3333333333333333333333333333333333333333',
136
+ new Date('2026-06-20T10:00:00.000Z'),
137
+ 'deploy-333'
138
+ );
139
 
140
  assert.deepEqual(marker, {
141
  schema_version: 1,
142
  local_sha: '3333333333333333333333333333333333333333',
143
+ created_at: '2026-06-20T10:00:00.000Z',
144
+ deploy_id: 'deploy-333'
145
  });
146
  assert.doesNotMatch(JSON.stringify(marker), /key|token|secret|password/i);
147
  });
148
 
149
+ it('builds a unique deploy marker id when none is provided', () => {
150
+ const first = buildDeployMarker('3333333333333333333333333333333333333333', new Date('2026-06-20T10:00:00.000Z'));
151
+ const second = buildDeployMarker('3333333333333333333333333333333333333333', new Date('2026-06-20T10:00:00.000Z'));
152
+
153
+ assert.match(first.deploy_id, /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
154
+ assert.notEqual(first.deploy_id, second.deploy_id);
155
+ });
156
+
157
  it('rejects short deploy marker commit shas before upload', () => {
158
  assert.throws(() => buildDeployMarker('3333333'), /full git commit SHA/);
159
  });
160
 
161
+ it('rejects unsafe deploy marker ids before upload', () => {
162
+ assert.throws(
163
+ () =>
164
+ buildDeployMarker(
165
+ '3333333333333333333333333333333333333333',
166
+ new Date('2026-06-20T10:00:00.000Z'),
167
+ 'deploy\n333'
168
+ ),
169
+ /deployId must be/
170
+ );
171
+ });
172
+
173
+ it('rejects stale deploy markers from an older same-commit deployment', () => {
174
+ const expected = buildDeployMarker(
175
+ '3333333333333333333333333333333333333333',
176
+ new Date('2026-06-20T10:00:00.000Z'),
177
+ 'deploy-current'
178
+ );
179
+ const stale = buildDeployMarker(
180
+ '3333333333333333333333333333333333333333',
181
+ new Date('2026-06-20T09:00:00.000Z'),
182
+ 'deploy-stale'
183
+ );
184
+
185
+ assert.throws(() => assertDeployMarkerMatches(stale, expected), /created_at mismatch/);
186
+ assert.deepEqual(assertDeployMarkerMatches(expected, expected), expected);
187
+ });
188
+
189
+ it('rejects deploy markers with a stale deploy id even when the source commit and timestamp match', () => {
190
+ const expected = buildDeployMarker(
191
+ '3333333333333333333333333333333333333333',
192
+ new Date('2026-06-20T10:00:00.000Z'),
193
+ 'deploy-current'
194
+ );
195
+ const stale = {
196
+ ...expected,
197
+ deploy_id: 'deploy-stale'
198
+ };
199
+
200
+ assert.throws(() => assertDeployMarkerMatches(stale, expected), /deploy_id mismatch/);
201
+ });
202
+
203
+ it('waits for the public service marker after management reports the new Space commit running', async () => {
204
+ const marker = buildDeployMarker(
205
+ '5555555555555555555555555555555555555555',
206
+ new Date('2026-06-20T12:00:00.000Z'),
207
+ 'deploy-555'
208
+ );
209
+ let verifyCount = 0;
210
+
211
+ const runtime = await waitForRunning('aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', marker, {
212
+ attempts: 2,
213
+ intervalMs: 0,
214
+ readInfo: () => ({
215
+ runtime: { stage: 'RUNNING' },
216
+ sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
217
+ }),
218
+ verifyMarker: async () => {
219
+ verifyCount += 1;
220
+ if (verifyCount === 1) throw new Error('deploy marker deploy_id mismatch');
221
+ return marker;
222
+ },
223
+ sleep: async () => {},
224
+ log: () => {}
225
+ });
226
+
227
+ assert.equal(verifyCount, 2);
228
+ assert.deepEqual(runtime, {
229
+ stage: 'RUNNING',
230
+ sha: 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa',
231
+ service_marker_verified: true,
232
+ marker
233
+ });
234
+ });
235
+
236
  it('builds a no-store API route for service-side deploy marker verification', () => {
237
+ const marker = buildDeployMarker(
238
+ '4444444444444444444444444444444444444444',
239
+ new Date('2026-06-20T11:00:00.000Z'),
240
+ 'deploy-444'
241
+ );
242
  const source = buildDeployMarkerRouteSource(marker);
243
 
244
  assert.match(source, /NextResponse\.json/);
245
  assert.match(source, /Cache-Control/);
246
  assert.match(source, /no-store/);
247
  assert.match(source, /4444444444444444444444444444444444444444/);
248
+ assert.match(source, /deploy-444/);
249
  assert.doesNotMatch(source, /key|token|secret|password/i);
250
  });
251
 
src/app/api/deploy-marker/route.ts CHANGED
@@ -1,6 +1,6 @@
1
  import { NextResponse } from 'next/server';
2
 
3
- const deployMarker = {"schema_version":1,"local_sha":"9bbfa7b63748719962755d5bcd75a62ce2415c5a","created_at":"2026-06-20T13:57:39.833Z"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6
 
 
1
  import { NextResponse } from 'next/server';
2
 
3
+ const deployMarker = {"schema_version":1,"local_sha":"a623efcc28ea071e4a51a2753173829a0ee01002","created_at":"2026-06-21T06:16:41.993Z","deploy_id":"83a9c54d-c4df-43fc-8045-97fe4feadb16"} as const;
4
 
5
  export const dynamic = 'force-dynamic';
6