From 1d11384d3185f109f6b11078c2672e3ffa903e95 Mon Sep 17 00:00:00 2001 From: Codex Date: Mon, 15 Jun 2026 05:33:18 +0000 Subject: [PATCH] fix: skip unchanged HWLAB test account sync --- scripts/src/hwlab-test-accounts.ts | 87 ++++++++++++++++++++++++++++-- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/scripts/src/hwlab-test-accounts.ts b/scripts/src/hwlab-test-accounts.ts index 9d3f52a1..77837b06 100644 --- a/scripts/src/hwlab-test-accounts.ts +++ b/scripts/src/hwlab-test-accounts.ts @@ -149,7 +149,7 @@ async function sync(config: LoadedConfig, target: Target, options: Options): Pro for (const account of target.accounts.filter((item) => item.target.kind === "kubernetes-secret")) { const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey); if (!source?.value) throw new Error(`${account.logicalId} source material missing after preflight`); - synced.push(syncKubernetesSecret(target, account, source, options)); + synced.push(syncKubernetesSecretIfNeeded(target, account, source, options)); } const userBillingAccounts = target.accounts.filter((item) => item.target.kind === "user-billing-api-key"); if (userBillingAccounts.length === 0) { @@ -163,6 +163,11 @@ async function sync(config: LoadedConfig, target: Target, options: Options): Pro for (const account of userBillingAccounts) { const source = sources.find((item) => item.sourceRef === account.sourceRef && item.sourceKey === account.sourceKey); if (!source?.value || !source.sha256Hex || !source.serviceKeyPrefix) throw new Error(`${account.logicalId} source material missing after preflight`); + const targetStatus = await userBillingApiKeyStatus(tx, account, source); + if (userBillingAccountMatches(account, targetStatus)) { + synced.push(skippedUserBillingAccountSync(account, source, targetStatus)); + continue; + } await syncUserBillingAccount(tx, account, source); synced.push({ logicalId: account.logicalId, targetKind: account.target.kind, mutation: true, userId: account.userId, keyId: account.target.keyId, keyPrefix: source.serviceKeyPrefix, fingerprint: source.fingerprint, valuesRedacted: true }); } @@ -296,6 +301,45 @@ VALUES (${account.target.keyId ?? ""}, ${account.userId}, ${account.target.keyNa ON CONFLICT (id) DO UPDATE SET user_id = EXCLUDED.user_id, name = EXCLUDED.name, key_prefix = EXCLUDED.key_prefix, key_hash = EXCLUDED.key_hash, scopes_json = EXCLUDED.scopes_json, status = 'active', revoked_at = NULL`; } +function syncKubernetesSecretIfNeeded(target: Target, account: Account, source: SourceMaterial, options: Options): Record { + const targetStatus = runtimeSecretStatus(target, account, source, options); + if (targetStatus.matchesSourceFingerprint === true) return skippedKubernetesSecretSync(target, account, source, targetStatus); + return syncKubernetesSecret(target, account, source, options); +} + +function skippedKubernetesSecretSync(target: Target, account: Account, source: SourceMaterial, targetStatus: Record): Record { + const namespace = account.target.namespace ?? target.namespace; + const secretName = account.target.secretName ?? ""; + return { + logicalId: account.logicalId, + targetKind: account.target.kind, + namespace, + secretName, + targetKey: account.target.targetKey, + rolloutDeployment: account.target.rolloutDeployment, + mutation: false, + skipped: "already-matches-source", + target: compactTargetStatus(targetStatus), + fingerprint: source.fingerprint, + valuesRedacted: true, + }; +} + +function skippedUserBillingAccountSync(account: Account, source: SourceMaterial, targetStatus: Record): Record { + return { + logicalId: account.logicalId, + targetKind: account.target.kind, + mutation: false, + skipped: "already-matches-source", + userId: account.userId, + keyId: account.target.keyId, + keyPrefix: source.serviceKeyPrefix, + fingerprint: source.fingerprint, + target: compactTargetStatus(targetStatus), + valuesRedacted: true, + }; +} + function syncKubernetesSecret(target: Target, account: Account, source: SourceMaterial, options: Options): Record { const namespace = account.target.namespace ?? target.namespace; const secretName = account.target.secretName ?? ""; @@ -359,7 +403,7 @@ function runtimeSecretStatus(target: Target, account: Account, source: SourceMat async function userBillingApiKeyStatus(sql: any, account: Account, source: SourceMaterial): Promise> { if (!sql) return { checked: false, kind: account.target.kind, reason: "database_url_source_missing", valuesRedacted: true }; const rows = await sql` -SELECT u.id AS user_id, u.username, u.role, u.status AS user_status, COALESCE(a.balance_credits, 0) AS balance_credits, COALESCE(a.reserved_credits, 0) AS reserved_credits, COALESCE(a.plan_id, '') AS plan_id, k.id AS key_id, k.key_prefix, k.key_hash, k.scopes_json::text AS scopes_json, k.status AS key_status +SELECT u.id AS user_id, u.email, u.username, u.display_name, u.role, u.status AS user_status, COALESCE(a.balance_credits, 0) AS balance_credits, COALESCE(a.reserved_credits, 0) AS reserved_credits, COALESCE(a.plan_id, '') AS plan_id, k.id AS key_id, k.key_prefix, k.key_hash, k.scopes_json::text AS scopes_json, k.status AS key_status FROM hwlab_users u LEFT JOIN hwlab_credit_accounts a ON a.user_id = u.id LEFT JOIN hwlab_api_keys k ON k.id = ${account.target.keyId ?? ""} AND k.user_id = u.id @@ -371,12 +415,41 @@ LIMIT 1`; checked: true, kind: account.target.kind, exists: true, - user: { id: row.user_id, username: row.username, role: row.role, status: row.user_status, planId: row.plan_id, balanceCredits: Number(row.balance_credits ?? 0), reservedCredits: Number(row.reserved_credits ?? 0) }, + user: { id: row.user_id, email: row.email, username: row.username, displayName: row.display_name, role: row.role, status: row.user_status, planId: row.plan_id, balanceCredits: Number(row.balance_credits ?? 0), reservedCredits: Number(row.reserved_credits ?? 0) }, apiKey: { exists: Boolean(row.key_id), keyId: account.target.keyId, keyPrefix: row.key_prefix || null, status: row.key_status || null, scopes: parseJsonArray(row.scopes_json), matchesSourceFingerprint: source.ok && source.sha256Hex && row.key_hash ? row.key_hash === source.sha256Hex : null }, valuesRedacted: true, }; } +function userBillingAccountMatches(account: Account, targetStatus: Record): boolean { + const user = plainObject(targetStatus.user); + const apiKey = plainObject(targetStatus.apiKey); + return targetStatus.exists === true + && user !== null + && apiKey !== null + && user.id === account.userId + && (user.email ?? "") === (account.email ?? "") + && user.username === account.username + && user.displayName === account.displayName + && user.role === account.role + && user.status === account.status + && user.planId === account.planId + && Number(user.balanceCredits ?? 0) >= account.initialCredits + && apiKey.exists === true + && apiKey.keyId === account.target.keyId + && apiKey.status === "active" + && apiKey.matchesSourceFingerprint === true + && stringArraysEqual(apiKey.scopes, account.target.scopes); +} + +function compactTargetStatus(status: Record): Record { + const target: Record = { valuesRedacted: true }; + for (const key of ["checked", "kind", "namespace", "secretName", "targetKey", "exists", "byteCount", "keyPrefix", "fingerprint", "matchesSourceFingerprint", "user", "apiKey"]) { + if (status[key] !== undefined) target[key] = status[key]; + } + return target; +} + function report(action: string, config: LoadedConfig, target: Target, options: Options, accounts: Record[], database: SourceMaterial, extra: Record = {}): Record { const blockers = [ ...(database.ok ? [] : [database.blocker]), @@ -513,6 +586,14 @@ function parseJsonArray(value: unknown): string[] { } } +function plainObject(value: unknown): Record | null { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : null; +} + +function stringArraysEqual(left: unknown, right: string[]): boolean { + return Array.isArray(left) && left.length === right.length && left.every((item, index) => item === right[index]); +} + function sha256(value: string): string { return createHash("sha256").update(value).digest("hex"); }