Preview: Dev-Registrierung nutzt gespeicherten Branch und Stand
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf2841adf5
commit
3316b562af
5 files changed
+76
-36
No files matched your search
@@ -15,6 +15,7 @@ export const PREVIEW_RUNNER_ATTRIBUTES = [
|
|||||||
{ key: 'previewState', type: 'string', size: 20, required: false },
|
{ key: 'previewState', type: 'string', size: 20, required: false },
|
||||||
{ key: 'previewDetected', type: 'string', size: 40, required: false },
|
{ key: 'previewDetected', type: 'string', size: 40, required: false },
|
||||||
{ key: 'previewBuiltSha', type: 'string', size: 40, required: false },
|
{ key: 'previewBuiltSha', type: 'string', size: 40, required: false },
|
||||||
|
{ key: 'previewBranch', type: 'string', size: 100, required: false },
|
||||||
{ key: 'previewBuiltAt', type: 'string', size: 40, required: false },
|
{ key: 'previewBuiltAt', type: 'string', size: 40, required: false },
|
||||||
{ key: 'previewError', type: 'string', size: 500, required: false },
|
{ key: 'previewError', type: 'string', size: 500, required: false },
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -461,27 +461,27 @@ export async function waitForDevRegistration() {
|
|||||||
while (registering) await registering
|
while (registering) await registering
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerDevSpecs(projects) {
|
let pendingProjects = null
|
||||||
if (registering) return
|
|
||||||
registering = (async () => {
|
async function registerDevRound(projects) {
|
||||||
const key = envKey()
|
const key = envKey()
|
||||||
await Promise.allSettled(
|
await Promise.allSettled(
|
||||||
projects.map(async (project) => {
|
projects.map(async (project) => {
|
||||||
const sub = project.subdomain
|
const sub = project.subdomain
|
||||||
try {
|
try {
|
||||||
const repo = project.repoFullName
|
const repo = project.repoFullName
|
||||||
if (!repo) return
|
const { previewBranch: branch, previewBuiltSha: sha } = project
|
||||||
const branch = project.previewBranch || 'main'
|
// Kein Raten (R17): ohne gespeicherten Branch/Stand wird uebersprungen
|
||||||
let sha = project.previewBuiltSha
|
if (!repo || !branch || !SHA_RE.test(String(sha || ''))) {
|
||||||
if (!SHA_RE.test(String(sha || ''))) {
|
console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen: Branch oder Stand unbekannt`)
|
||||||
try { sha = await resolveSha(repo, branch, null) } catch (err) {
|
|
||||||
console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen:`, err.message)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let env = {}
|
let env = {}
|
||||||
if (project.previewEnv && key) {
|
if (project.previewEnv) {
|
||||||
try { env = decryptEnv(project.previewEnv, key) } catch { env = {} }
|
try { env = decryptEnv(project.previewEnv, key) } catch {
|
||||||
|
console.error(`[previewDeploy] Dev-Registrierung ${sub} uebersprungen: Variablen nicht entschluesselbar`)
|
||||||
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await runnerClient.startDev(sub, { sub, repo, sha, branch, mode: 'dev', env })
|
await runnerClient.startDev(sub, { sub, repo, sha, branch, mode: 'dev', env })
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -489,6 +489,18 @@ function registerDevSpecs(projects) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerDevSpecs(projects) {
|
||||||
|
// Laeuft schon eine Runde: genau eine Wiederholung danach (mit dem neuesten Stand)
|
||||||
|
if (registering) { pendingProjects = projects; return }
|
||||||
|
registering = (async () => {
|
||||||
|
let next = projects
|
||||||
|
while (next) {
|
||||||
|
pendingProjects = null
|
||||||
|
await registerDevRound(next)
|
||||||
|
next = pendingProjects
|
||||||
|
}
|
||||||
})().finally(() => { registering = null })
|
})().finally(() => { registering = null })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -613,7 +625,12 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job,
|
|||||||
const now = new Date().toISOString()
|
const now = new Date().toISOString()
|
||||||
if (!failure && run?.state === 'done') {
|
if (!failure && run?.state === 'done') {
|
||||||
const detected = detectedLabel(run.detected, run.result)
|
const detected = detectedLabel(run.detected, run.result)
|
||||||
const base = { previewDetected: detected, previewError: warning || '' }
|
const base = {
|
||||||
|
previewDetected: detected,
|
||||||
|
previewError: warning || '',
|
||||||
|
previewBranch: job.branch,
|
||||||
|
previewBuiltSha: run.sha || job.sha,
|
||||||
|
}
|
||||||
if (run.result === 'dev') {
|
if (run.result === 'dev') {
|
||||||
await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' })
|
await patchProject(repoFullName, project, { ...base, previewState: 'sleeping' })
|
||||||
try { await runnerClient.startDev(subdomain, job) } catch (err) {
|
try { await runnerClient.startDev(subdomain, job) } catch (err) {
|
||||||
@@ -621,7 +638,7 @@ async function followRun({ repoFullName, project, token, runId, subdomain, job,
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
await patchProject(repoFullName, project, {
|
await patchProject(repoFullName, project, {
|
||||||
...base, previewState: 'online', previewBuiltSha: run.sha || job.sha, previewBuiltAt: now,
|
...base, previewState: 'online', previewBuiltAt: now,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ before(async () => {
|
|||||||
await new Promise((r) => runner.listen(0, '127.0.0.1', r))
|
await new Promise((r) => runner.listen(0, '127.0.0.1', r))
|
||||||
gitea = http.createServer((req, res) => {
|
gitea = http.createServer((req, res) => {
|
||||||
res.setHeader('Content-Type', 'application/json')
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
giteaHits++
|
||||||
if (giteaDown) { res.statusCode = 500; return res.end('{}') }
|
if (giteaDown) { res.statusCode = 500; return res.end('{}') }
|
||||||
res.end(JSON.stringify({ commit: { id: HEAD } }))
|
res.end(JSON.stringify({ commit: { id: HEAD } }))
|
||||||
})
|
})
|
||||||
@@ -62,7 +63,7 @@ before(async () => {
|
|||||||
await createDocument('websiteProjects', { ...base, subdomain: 'statisch', previewState: 'online', previewDetected: 'build:npm' }, 'p2')
|
await createDocument('websiteProjects', { ...base, subdomain: 'statisch', previewState: 'online', previewDetected: 'build:npm' }, 'p2')
|
||||||
await createDocument('websiteProjects', {
|
await createDocument('websiteProjects', {
|
||||||
...base, subdomain: 'devapp', repoFullName: 'JUSN/devapp', previewState: 'sleeping', previewDetected: 'dev:npm',
|
...base, subdomain: 'devapp', repoFullName: 'JUSN/devapp', previewState: 'sleeping', previewDetected: 'dev:npm',
|
||||||
previewBuiltSha: BUILT, previewEnv: envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key),
|
previewBuiltSha: BUILT, previewBranch: 'feature-x', previewEnv: envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key),
|
||||||
}, 'p3')
|
}, 'p3')
|
||||||
await createDocument('websiteProjects', {
|
await createDocument('websiteProjects', {
|
||||||
...base, subdomain: 'devoff', repoFullName: 'JUSN/devoff', previewState: 'failed', previewDetected: 'dev:npm',
|
...base, subdomain: 'devoff', repoFullName: 'JUSN/devoff', previewState: 'failed', previewDetected: 'dev:npm',
|
||||||
@@ -182,18 +183,21 @@ test('Registrierung: Runner bekommt Spec mit entschluesselten Variablen', async
|
|||||||
assert.equal(puts.length, 1)
|
assert.equal(puts.length, 1)
|
||||||
assert.equal(puts[0].url, '/dev/devapp')
|
assert.equal(puts[0].url, '/dev/devapp')
|
||||||
assert.deepEqual(puts[0].body, {
|
assert.deepEqual(puts[0].body, {
|
||||||
sub: 'devapp', repo: 'JUSN/devapp', sha: BUILT, branch: 'main', mode: 'dev', env: { API_KEY: 'geheim-wert' },
|
sub: 'devapp', repo: 'JUSN/devapp', sha: BUILT, branch: 'feature-x', mode: 'dev', env: { API_KEY: 'geheim-wert' },
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Registrierung: ohne PREVIEW_ENV_KEY env = {}', async () => {
|
test('Registrierung: nicht entschluesselbare Variablen (Schluessel fehlt) => uebersprungen', async () => {
|
||||||
const saved = process.env.PREVIEW_ENV_KEY
|
const saved = process.env.PREVIEW_ENV_KEY
|
||||||
delete process.env.PREVIEW_ENV_KEY
|
delete process.env.PREVIEW_ENV_KEY
|
||||||
|
const errors = console.error
|
||||||
|
console.error = () => {}
|
||||||
try {
|
try {
|
||||||
await pd.syncPreviewTls()
|
await pd.syncPreviewTls()
|
||||||
await pd.waitForDevRegistration()
|
await pd.waitForDevRegistration()
|
||||||
} finally { process.env.PREVIEW_ENV_KEY = saved }
|
} finally { process.env.PREVIEW_ENV_KEY = saved; console.error = errors }
|
||||||
assert.deepEqual(puts[0].body.env, {})
|
assert.equal(puts.length, 0)
|
||||||
|
assert.match(await read(), /preview-runner@docker/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Registrierung: Runner-Fehler bricht den Abgleich nicht, Datei ist geschrieben', async () => {
|
test('Registrierung: Runner-Fehler bricht den Abgleich nicht, Datei ist geschrieben', async () => {
|
||||||
@@ -210,30 +214,45 @@ test('Registrierung: Runner-Fehler bricht den Abgleich nicht, Datei ist geschrie
|
|||||||
assert.match(await read(), /preview-runner@docker/)
|
assert.match(await read(), /preview-runner@docker/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Registrierung: ohne gebauten Stand wird der Branch-Kopf genutzt, bei Fehlschlag uebersprungen', async () => {
|
let giteaHits = 0
|
||||||
|
|
||||||
|
test('Registrierung: fehlender Stand oder Branch => uebersprungen, kein Gitea-Aufruf', async () => {
|
||||||
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||||
await updateDocument('websiteProjects', 'p3', { previewBuiltSha: '' })
|
const errors = console.error
|
||||||
|
console.error = () => {}
|
||||||
try {
|
try {
|
||||||
|
for (const patch of [{ previewBuiltSha: '' }, { previewBranch: '' }]) {
|
||||||
|
await updateDocument('websiteProjects', 'p3', patch)
|
||||||
|
giteaHits = 0
|
||||||
|
puts.length = 0
|
||||||
await pd.syncPreviewTls()
|
await pd.syncPreviewTls()
|
||||||
await pd.waitForDevRegistration()
|
await pd.waitForDevRegistration()
|
||||||
assert.equal(puts[0].body.sha, HEAD)
|
assert.equal(puts.length, 0)
|
||||||
puts.length = 0
|
assert.equal(giteaHits, 0)
|
||||||
giteaDown = true
|
assert.match(await read(), /preview-runner@docker/)
|
||||||
|
await updateDocument('websiteProjects', 'p3', { previewBuiltSha: BUILT, previewBranch: 'feature-x' })
|
||||||
|
}
|
||||||
|
} finally { console.error = errors }
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Registrierung: kaputte verschluesselte Variablen => uebersprungen', async () => {
|
||||||
|
const { updateDocument } = await import('../server/services/appwriteAdmin.js')
|
||||||
|
const good = envLib.encryptEnv({ API_KEY: 'geheim-wert' }, key)
|
||||||
|
await updateDocument('websiteProjects', 'p3', { previewEnv: 'v1:xx:yy:zz' })
|
||||||
const errors = console.error
|
const errors = console.error
|
||||||
console.error = () => {}
|
console.error = () => {}
|
||||||
try {
|
try {
|
||||||
await pd.syncPreviewTls()
|
await pd.syncPreviewTls()
|
||||||
await pd.waitForDevRegistration()
|
await pd.waitForDevRegistration()
|
||||||
} finally { console.error = errors }
|
|
||||||
assert.equal(puts.length, 0)
|
|
||||||
assert.match(await read(), /preview-runner@docker/) // Router trotzdem da
|
|
||||||
} finally {
|
} finally {
|
||||||
await updateDocument('websiteProjects', 'p3', { previewBuiltSha: BUILT })
|
console.error = errors
|
||||||
|
await updateDocument('websiteProjects', 'p3', { previewEnv: good })
|
||||||
}
|
}
|
||||||
|
assert.equal(puts.length, 0)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Registrierung: keine zwei Runden gleichzeitig', async () => {
|
test('Registrierung: laeuft eine Runde, folgt genau eine Wiederholung', async () => {
|
||||||
await Promise.all([pd.syncPreviewTls(), pd.syncPreviewTls()])
|
await Promise.all([pd.syncPreviewTls(), pd.syncPreviewTls(), pd.syncPreviewTls()])
|
||||||
await pd.waitForDevRegistration()
|
await pd.waitForDevRegistration()
|
||||||
assert.equal(puts.length, 1)
|
assert.equal(puts.length, 2)
|
||||||
})
|
})
|
||||||
@@ -157,6 +157,8 @@ test('dev: sleeping + Registrierung per PUT /dev/<sub>', async () => {
|
|||||||
await pd.waitForRunFollowers()
|
await pd.waitForRunFollowers()
|
||||||
const d = aw.get('websiteProjects', id)
|
const d = aw.get('websiteProjects', id)
|
||||||
assert.equal(d.previewState, 'sleeping')
|
assert.equal(d.previewState, 'sleeping')
|
||||||
|
assert.equal(d.previewBranch, 'main')
|
||||||
|
assert.equal(d.previewBuiltSha, SHA)
|
||||||
assert.equal(d.previewDetected, 'dev:pnpm')
|
assert.equal(d.previewDetected, 'dev:pnpm')
|
||||||
const put = rs.calls.find((c) => c.method === 'PUT')
|
const put = rs.calls.find((c) => c.method === 'PUT')
|
||||||
assert.equal(put.url, `/dev/${repo.split('/')[1]}`)
|
assert.equal(put.url, `/dev/${repo.split('/')[1]}`)
|
||||||
|
|||||||
@@ -17,10 +17,10 @@ function mockClient(existing = []) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => {
|
test('Schema: genau die 8 Attribute mit Groessen, alle optional', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
PREVIEW_RUNNER_ATTRIBUTES.map((a) => a.key),
|
PREVIEW_RUNNER_ATTRIBUTES.map((a) => a.key),
|
||||||
['previewMode', 'previewEnv', 'previewState', 'previewDetected', 'previewBuiltSha', 'previewBuiltAt', 'previewError']
|
['previewMode', 'previewEnv', 'previewState', 'previewDetected', 'previewBuiltSha', 'previewBranch', 'previewBuiltAt', 'previewError']
|
||||||
)
|
)
|
||||||
assert.ok(PREVIEW_RUNNER_ATTRIBUTES.every((a) => a.required === false && a.type === 'string'))
|
assert.ok(PREVIEW_RUNNER_ATTRIBUTES.every((a) => a.required === false && a.type === 'string'))
|
||||||
const sizes = Object.fromEntries(PREVIEW_RUNNER_ATTRIBUTES.map((a) => [a.key, a.size]))
|
const sizes = Object.fromEntries(PREVIEW_RUNNER_ATTRIBUTES.map((a) => [a.key, a.size]))
|
||||||
@@ -30,6 +30,7 @@ test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => {
|
|||||||
previewState: 20,
|
previewState: 20,
|
||||||
previewDetected: 40,
|
previewDetected: 40,
|
||||||
previewBuiltSha: 40,
|
previewBuiltSha: 40,
|
||||||
|
previewBranch: 100,
|
||||||
previewBuiltAt: 40,
|
previewBuiltAt: 40,
|
||||||
previewError: 500,
|
previewError: 500,
|
||||||
})
|
})
|
||||||
@@ -38,16 +39,16 @@ test('Schema: genau die 7 Attribute mit Groessen, alle optional', () => {
|
|||||||
test('main legt nur fehlende Attribute an', async () => {
|
test('main legt nur fehlende Attribute an', async () => {
|
||||||
const client = mockClient(['previewMode'])
|
const client = mockClient(['previewMode'])
|
||||||
const result = await main({ check: false, client, log: () => {} })
|
const result = await main({ check: false, client, log: () => {} })
|
||||||
assert.equal(client.created.length, 6)
|
assert.equal(client.created.length, 7)
|
||||||
assert.ok(!client.created.includes('previewMode'))
|
assert.ok(!client.created.includes('previewMode'))
|
||||||
assert.equal(result.missing.length, 6)
|
assert.equal(result.missing.length, 7)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('main --check legt nichts an und meldet Fehlende', async () => {
|
test('main --check legt nichts an und meldet Fehlende', async () => {
|
||||||
const client = mockClient(['previewMode'])
|
const client = mockClient(['previewMode'])
|
||||||
const result = await main({ check: true, client, log: () => {} })
|
const result = await main({ check: true, client, log: () => {} })
|
||||||
assert.equal(client.created.length, 0)
|
assert.equal(client.created.length, 0)
|
||||||
assert.equal(result.missing.length, 6)
|
assert.equal(result.missing.length, 7)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('Konfiguration: previewRunner ist standardmaessig aus', () => {
|
test('Konfiguration: previewRunner ist standardmaessig aus', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user