deploy-board-actions.mjs 2.9 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243
  1. // Only the two new public board action functions; no changes to existing functions, schemas or ACLs.
  2. import fs from 'node:fs';
  3. import path from 'node:path';
  4. import crypto from 'node:crypto';
  5. import { ROOT, request } from './platform-client.mjs';
  6. const sha = (s) => crypto.createHash('sha256').update(s).digest('hex');
  7. const apply = process.argv.includes('--apply');
  8. const manifest = JSON.parse(fs.readFileSync(path.join(ROOT, 'dist/manifest.json')));
  9. const names = ['ksfSetPublicOpportunityStatus', 'ksfRatePublicOpportunity'];
  10. const output = path.join(ROOT, 'evidence/board-actions-' + Date.now() + '.json');
  11. const journal = { ok: false, readOnly: !apply, at: new Date().toISOString(), businessWrites: 0, schemaChanges: 0, permissionChanges: 0, functions: [] };
  12. try {
  13. const schemas = await request('GET', '/schemas');
  14. for (const name of names) {
  15. const entry = manifest.functions.find((f) => f.name === name);
  16. if (!entry || entry.path !== '/' + name) throw Error('BAD_MANIFEST');
  17. const code = fs.readFileSync(path.join(ROOT, entry.file), 'utf8');
  18. if (sha(code) !== entry.sha256) throw Error('HASH_MISMATCH');
  19. new Function(code);
  20. const query = new URLSearchParams({ where: JSON.stringify({ $or: [{ name }, { path: '/' + name }] }), limit: '3' });
  21. const matches = (await request('GET', '/classes/Function?' + query)).results;
  22. if (matches.length > 1 || matches.some((f) => f.name !== name || f.path !== '/' + name || f.code !== code)) throw Error('UNKNOWN_FUNCTION_VERSION');
  23. journal.functions.push({ name, path: '/' + name, sha256: sha(code), objectId: matches[0]?.objectId || null, action: matches.length ? 'unchanged' : 'create' });
  24. }
  25. if (apply) for (const entry of journal.functions) {
  26. if (entry.action === 'unchanged') continue;
  27. const def = manifest.functions.find((f) => f.name === entry.name), code = fs.readFileSync(path.join(ROOT, def.file), 'utf8');
  28. const created = await request('POST', '/classes/Function', { name: entry.name, path: entry.path, code, ACL: {} });
  29. entry.objectId = created.objectId;
  30. fs.writeFileSync(output, JSON.stringify(journal, null, 2), { mode: 0o600 });
  31. const read = await request('GET', '/classes/Function/' + entry.objectId);
  32. if (read.code !== code || read.path !== entry.path || read.name !== entry.name) throw Error('READBACK_MISMATCH');
  33. entry.verified = true;
  34. }
  35. if (JSON.stringify((await request('GET', '/schemas')).results) !== JSON.stringify(schemas.results)) throw Error('SCHEMA_OR_PERMISSION_CHANGE');
  36. journal.ok = true; journal.schemaAndPermissionsPreserved = true;
  37. fs.writeFileSync(output, JSON.stringify(journal, null, 2), { mode: 0o600 });
  38. console.log(JSON.stringify(journal));
  39. } catch (error) {
  40. journal.error = { code: /^[A-Z_]+$/.test(error.message) ? error.message : 'DEPLOY_STOPPED' };
  41. fs.writeFileSync(output, JSON.stringify(journal, null, 2), { mode: 0o600 });
  42. console.error(JSON.stringify(journal)); process.exitCode = 1;
  43. }