@wtfalch/foundry 0.64.0 → 0.65.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1765,7 +1765,7 @@ export interface FoundryStepExecutorPorts {
1765
1765
  /**
1766
1766
  * The stage copy (`copy_prod_to_stage`, run by the release tick). The host supplies the dump download and the
1767
1767
  * backup storage; foundry builds the restore port itself and reads production's address live from Coolify.
1768
- * Without it a copy request is refused with `no-copy-port`. The host needs `pg_restore` (Postgres 16) on PATH.
1768
+ * Without it a copy request is refused with `no-copy-port`. The host needs `pg_restore` on PATH, of the production database's Postgres version or newer (platform-archon-db runs 18).
1769
1769
  */
1770
1770
  stageCopy?: StageCopyHostConfig;
1771
1771
  /**
@@ -6237,7 +6237,7 @@ function assertSameDatabaseHost(key, liveUrl, dbInternalUrl) {
6237
6237
  /**
6238
6238
  * Throws when `liveUrl`, kept as it is on the app, names another database than
6239
6239
  * {@link ARCHON_POSTGRES_DB}: a hand-set URL on the wrong path, or a platform
6240
- * still on the retired `archon_v2`. Names the two databases only, never the URL.
6240
+ * still on a retired database. Names the two databases only, never the URL.
6241
6241
  */
6242
6242
  function assertArchonDatabasePath(key, liveUrl) {
6243
6243
  let database;
@@ -1,5 +1,5 @@
1
1
  import { decide, healthRange, } from './decide.js';
2
- import { formatWeights, formatWindow, recordedFrom, releaseKeys, } from './state.js';
2
+ import { formatRefusals, formatWeights, formatWindow, recordedFrom, releaseKeys, } from './state.js';
3
3
  import { isStageCopyAction } from './types.js';
4
4
  const REFUSAL_TEXT = {
5
5
  'no-copy-port': 'copy refused: this host cannot restore',
@@ -79,6 +79,10 @@ export async function runRelease(input) {
79
79
  [k.window]: formatWindow(next.window, next.release),
80
80
  [k.signal]: decision.signal ? JSON.stringify(decision.signal) : null,
81
81
  };
82
+ // Part of the run's writes: persisted with them, so a failed persist leaves neither. A run with none keeps it.
83
+ const lines = [...refused, ...decision.blocked];
84
+ if (lines.length > 0)
85
+ writes[k.refused] = formatRefusals(stamp, lines);
82
86
  for (const d of recorded.held)
83
87
  if (!next.held.includes(d))
84
88
  writes[k.held(d)] = null;
@@ -132,6 +136,7 @@ export function settleStageCopy(input) {
132
136
  writes[k.stageCopy] = `${stamp}|refused|${result.reason}`;
133
137
  writes[k.stageCopyRetry] = null;
134
138
  refused.push(REFUSAL_TEXT[result.reason] ?? `copy refused: ${result.reason}`);
139
+ writes[k.refused] = formatRefusals(stamp, refused);
135
140
  return { writes, refused, consume: true };
136
141
  }
137
142
  writes[k.stageCopy] = `${stamp}|failed|${result.step}`;
@@ -48,6 +48,8 @@ export declare function releaseKeys(entry: string): {
48
48
  stageCopyHandled: string;
49
49
  /** The id of a request whose copy failed once; a second failure consumes it. */
50
50
  stageCopyRetry: string;
51
+ /** JSON `{ at, lines }`: the latest run that refused or blocked something (see {@link formatRefusals}). */
52
+ refused: string;
51
53
  };
52
54
  export declare function formatWeights(w: Record<Color, number>): string;
53
55
  export declare function parseWeights(value: string | undefined): Record<Color, number> | null;
@@ -60,6 +62,13 @@ export declare function formatWindow(window: WindowState, release: Recorded['rel
60
62
  export declare function recordedFrom(ids: Record<string, string>, entryKey: string): Recorded | null;
61
63
  /** Reads `bg.<id>.stagecopy`; a damaged value reads as absent. */
62
64
  export declare function parseStageCopy(value: string | undefined): StageCopyState | undefined;
65
+ /**
66
+ * The value of `bg.<id>.refused`: the run time and its refused lines, then its blocked lines. Capped at 20 lines
67
+ * of 300 characters so a bad run cannot grow the row. Callers write it only when `lines` is not empty.
68
+ */
69
+ export declare function formatRefusals(at: string, lines: readonly string[]): string;
70
+ /** Reads `bg.<id>.refused`; a damaged value reads as absent. */
71
+ export declare function parseRefusals(value: string | undefined): ReleaseState['refused'];
63
72
  export declare function readSignal(ids: Record<string, string>, entryKey: string): ReleaseSignal | null;
64
73
  /** C2: the release state of one hosting entry, or null before blue/green recorded anything. */
65
74
  export declare function readReleaseState(store: ReleaseStore, input: {
@@ -25,6 +25,8 @@ export function releaseKeys(entry) {
25
25
  stageCopyHandled: `${p}.stagecopyhandled`,
26
26
  /** The id of a request whose copy failed once; a second failure consumes it. */
27
27
  stageCopyRetry: `${p}.stagecopyretry`,
28
+ /** JSON `{ at, lines }`: the latest run that refused or blocked something (see {@link formatRefusals}). */
29
+ refused: `${p}.refused`,
28
30
  };
29
31
  }
30
32
  export function formatWeights(w) {
@@ -122,6 +124,26 @@ export function parseStageCopy(value) {
122
124
  }
123
125
  return undefined;
124
126
  }
127
+ const REFUSED_MAX_LINES = 20;
128
+ const REFUSED_MAX_LINE = 300;
129
+ const refusedSchema = z.object({ at: z.string(), lines: z.array(z.string()) });
130
+ /**
131
+ * The value of `bg.<id>.refused`: the run time and its refused lines, then its blocked lines. Capped at 20 lines
132
+ * of 300 characters so a bad run cannot grow the row. Callers write it only when `lines` is not empty.
133
+ */
134
+ export function formatRefusals(at, lines) {
135
+ return JSON.stringify({
136
+ at,
137
+ lines: lines.slice(0, REFUSED_MAX_LINES).map((l) => l.slice(0, REFUSED_MAX_LINE)),
138
+ });
139
+ }
140
+ /** Reads `bg.<id>.refused`; a damaged value reads as absent. */
141
+ export function parseRefusals(value) {
142
+ const parsed = refusedSchema.safeParse(parseJson(value));
143
+ if (!parsed.success || Number.isNaN(Date.parse(parsed.data.at)))
144
+ return undefined;
145
+ return parsed.data;
146
+ }
125
147
  export function readSignal(ids, entryKey) {
126
148
  const parsed = signalSchema.safeParse(parseJson(ids[releaseKeys(entryKey).signal]));
127
149
  return parsed.success ? parsed.data : null;
@@ -133,6 +155,7 @@ export async function readReleaseState(store, input) {
133
155
  if (!r)
134
156
  return null;
135
157
  const stageCopy = parseStageCopy(ids[releaseKeys(input.entryKey).stageCopy]);
158
+ const refused = parseRefusals(ids[releaseKeys(input.entryKey).refused]);
136
159
  return {
137
160
  entryKey: input.entryKey,
138
161
  live: r.live,
@@ -144,6 +167,7 @@ export async function readReleaseState(store, input) {
144
167
  held: r.held,
145
168
  config: resolveReleaseConfig(input.entry),
146
169
  ...(stageCopy ? { stageCopy } : {}),
170
+ ...(refused ? { refused } : {}),
147
171
  ...stageDeployOf(ids[releaseKeys(input.entryKey).stageDeploy]),
148
172
  };
149
173
  }
@@ -59,6 +59,14 @@ export interface ReleaseState {
59
59
  outcome: 'ok' | 'failed';
60
60
  detail: string;
61
61
  };
62
+ /**
63
+ * The latest run that refused or blocked something (`bg.<id>.refused`): its time and its refused lines then its
64
+ * blocked lines, as decide's fixed texts (at most 20 lines of 300 characters). A run with none leaves it as is.
65
+ */
66
+ refused?: {
67
+ at: string;
68
+ lines: string[];
69
+ };
62
70
  }
63
71
  /** C3: what the last `copy_prod_to_stage` or `undo_stage_copy` request did. `detail` is fixed text or an id. */
64
72
  export interface StageCopyState {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wtfalch/foundry",
3
- "version": "0.64.0",
3
+ "version": "0.65.0",
4
4
  "description": "Foundry's SDK: the declared org's types, the plan and apply contracts, and the state store's interface.",
5
5
  "repository": {
6
6
  "type": "git",