@scalemule/news-publication 0.0.79 → 0.0.81
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.
- package/README.md +73 -0
- package/bin/scalemule-article.mjs +161 -6
- package/dist/{article-presentation-BrNCcF-5.d.cts → article-presentation-0d1FiDgA.d.cts} +1 -1
- package/dist/{article-presentation-BrNCcF-5.d.ts → article-presentation-0d1FiDgA.d.ts} +1 -1
- package/dist/article-workflow-client.cjs +677 -8
- package/dist/article-workflow-client.d.cts +111 -3
- package/dist/article-workflow-client.d.ts +111 -3
- package/dist/article-workflow-client.js +674 -9
- package/dist/index.d.cts +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/story-review.d.cts +1 -1
- package/dist/story-review.d.ts +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -338,6 +338,79 @@ The platform contract and a complete example live in
|
|
|
338
338
|
`docs/news-platform/ARTICLE-WORKFLOW-V1.md` in the ScaleMule meta-repo.
|
|
339
339
|
Do not use direct SQL, S3 paths, base64 bodies or invented media readiness.
|
|
340
340
|
|
|
341
|
+
### Edit article prose, cards and citations
|
|
342
|
+
|
|
343
|
+
```sh
|
|
344
|
+
npx --yes --package @scalemule/news-publication@0.0.81 scalemule-article edit \
|
|
345
|
+
--url "$ARTICLE_URL" --patch ./approved-edit.json --operation-key correction-v1
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
The patch may contain title, subtitle, content_markdown, excerpt, section,
|
|
349
|
+
community, format, layout, cards and citations. It preserves omitted fields and
|
|
350
|
+
media and saves one immutable revision. Default: save for review. Add
|
|
351
|
+
`--release` only to request release through the existing policy; the command
|
|
352
|
+
never issues APPROVE or downgrades review. On a legacy article, explicitly choose
|
|
353
|
+
`--enable-review-policy` only as a legitimate editorial onboarding decision.
|
|
354
|
+
Scheduled legacy articles stop before onboarding. Normal API-key/member grants
|
|
355
|
+
remain authoritative; the deployed server must advertise
|
|
356
|
+
`versioned_editorial_fields`.
|
|
357
|
+
|
|
358
|
+
The 10-second default deadline bounds the operation; `--timeout-ms` may raise it
|
|
359
|
+
to at most 120 seconds. Keep the same patch/key and private checkpoint
|
|
360
|
+
(`--state` or `~/.scalemule/newsroom/operations`) on retries. The checkpoint
|
|
361
|
+
contains the submitted document and private provenance: never publish it.
|
|
362
|
+
Unknown save/release responses reconcile against the original revision; another
|
|
363
|
+
current revision stops rather than being overwritten. Completed replay and no-op
|
|
364
|
+
edits add no revision. `RELEASE_HELD`/exit 2 preserves the saved revision while
|
|
365
|
+
review is pending. With `--url --release`, success requires the rendered
|
|
366
|
+
headline, prose and actual card components; public visibility pending is also
|
|
367
|
+
exit 2. A saved revision and a rendered published correction are separate states.
|
|
368
|
+
|
|
369
|
+
Use the same client method, `client.editArticle`, for a caller-supplied durable
|
|
370
|
+
checkpoint store. This is local resumability over the existing server save
|
|
371
|
+
receipt, not a distributed transaction covering entities, syndication, media
|
|
372
|
+
commissioning or human review. The notices-only `/editorial` endpoint is not
|
|
373
|
+
an article-body/card editor. Routine content corrections require no backend PR.
|
|
374
|
+
|
|
375
|
+
### Replace a published article photo from a local file
|
|
376
|
+
|
|
377
|
+
```sh
|
|
378
|
+
npx --yes --package @scalemule/news-publication@0.0.80 scalemule-article replace-photo \
|
|
379
|
+
--url "$ARTICLE_URL" --file ./venue-exterior.jpg \
|
|
380
|
+
--metadata ./photo.json --operation-key venue-exterior-v1
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
Use the normal authorized member's `SCALEMULE_API_KEY` and
|
|
384
|
+
`SCALEMULE_ACCESS_TOKEN`. The caller needs developer-level asset access and
|
|
385
|
+
publication approval rights. Supply caption, credit, factual alt, confirmed rights,
|
|
386
|
+
explicit `aiGenerated` and `label` (null for an ordinary photo) in `photo.json`.
|
|
387
|
+
The URL resolves the authorized publication and article; no UUID search or
|
|
388
|
+
publication build is needed. This does not find a photo or establish rights.
|
|
389
|
+
|
|
390
|
+
`uploadAndReplacePublishedCover` implements the same operation for applications.
|
|
391
|
+
It transfers bytes through managed Storage, preserves private capture evidence,
|
|
392
|
+
waits for a clean scan, registers Photo, prepares the publication's lead,
|
|
393
|
+
thumbnail and social renditions, reads measured delivery receipts and performs
|
|
394
|
+
one article-version-fenced media write. It verifies unchanged text/audio and,
|
|
395
|
+
when given `--url`, checks the selected lead and social URLs in the public HTML.
|
|
396
|
+
It requires the deployed content-first document and member Photo endpoints.
|
|
397
|
+
|
|
398
|
+
A private atomic checkpoint lives under `~/.scalemule/newsroom/operations`
|
|
399
|
+
(or `--state PATH`). Keep it: it contains a temporary upload credential until
|
|
400
|
+
completion. Reuse the same file, metadata and operation key after interruption.
|
|
401
|
+
A completed replay verifies the same attachment without uploading or writing
|
|
402
|
+
again. A concurrent article change stops instead of overwriting the editor.
|
|
403
|
+
The default per-run deadline is 30 seconds, adjustable with `--timeout-ms` up to
|
|
404
|
+
120 seconds; timeouts name the blocked stage and preserve progress. Exit 2 means
|
|
405
|
+
API attachment succeeded but public HTML visibility is still pending. A timeout
|
|
406
|
+
is not a success receipt. This is local resumability, not a distributed
|
|
407
|
+
exactly-once service or a guaranteed image-processing SLA.
|
|
408
|
+
|
|
409
|
+
This published-cover convenience lane stops before upload for collaboration
|
|
410
|
+
articles. Their immutable revision, approval and release gates still apply.
|
|
411
|
+
Animated GIF/APNG/WebP and videos use the Video workflow; this lane accepts
|
|
412
|
+
still JPEG, PNG and WebP up to 25 MiB.
|
|
413
|
+
|
|
341
414
|
### Replace an existing, prepared cover
|
|
342
415
|
|
|
343
416
|
For a legacy published article and an already public, scanned Photo asset,
|
|
@@ -1,16 +1,41 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { readFile, mkdir, writeFile, rename, open, unlink } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, basename } from 'node:path';
|
|
5
|
+
import { createHash } from 'node:crypto';
|
|
3
6
|
import { parseArgs } from 'node:util';
|
|
4
|
-
import { PublicationArticleClient } from '../dist/article-workflow-client.js';
|
|
7
|
+
import { PublicationArticleClient, articlePhotoHtmlMatches,articleEditHtmlMatches } from '../dist/article-workflow-client.js';
|
|
5
8
|
|
|
6
9
|
const help = `Usage:
|
|
7
10
|
scalemule-article inspect --publication ID --article ID
|
|
8
11
|
scalemule-article replace-cover --publication ID --article ID --file-id ID --expected-version N --metadata photo.json
|
|
12
|
+
scalemule-article replace-photo --url ARTICLE_URL --file PHOTO --metadata photo.json --operation-key KEY
|
|
13
|
+
scalemule-article edit --url ARTICLE_URL --patch edit.json --operation-key KEY [--release]
|
|
9
14
|
|
|
10
15
|
Credentials: SCALEMULE_API_KEY and SCALEMULE_ACCESS_TOKEN (normal authorized member).
|
|
11
16
|
Platform admins may use their normal access token with --application-id ID.
|
|
12
17
|
Optional --api-url URL defaults to https://api.scalemule.com.
|
|
13
18
|
|
|
19
|
+
edit saves prose, excerpt, cards and citations in one immutable revision.
|
|
20
|
+
Unspecified fields and media are retained. Default: save for review.
|
|
21
|
+
--release explicitly requests release under the existing policy; it never issues
|
|
22
|
+
APPROVE or changes a policy. Legacy onboarding requires an explicit
|
|
23
|
+
--enable-review-policy value. Scheduled legacy articles stop before onboarding.
|
|
24
|
+
Use --expected-generation when coordinating an edit with another editor.
|
|
25
|
+
Default edit deadline: 10000 ms. Reuse the same patch/key/checkpoint on retry.
|
|
26
|
+
RELEASE_HELD means the revision is saved and existing review gates remain.
|
|
27
|
+
|
|
28
|
+
replace-photo accepts a local file and uses normal member/API-key credentials.
|
|
29
|
+
It requires developer-level asset access and publication approval rights.
|
|
30
|
+
It resolves the publication from its authorized API inventory, saves a private
|
|
31
|
+
upload checkpoint, resumes the same multipart part, waits for scan and measured
|
|
32
|
+
publication renditions, then attaches with the original article version fence.
|
|
33
|
+
It verifies article invariants and, with --url, the public page. The default
|
|
34
|
+
deadline is 30 seconds per run; a failure names its stage and preserves progress.
|
|
35
|
+
Reuse the same --operation-key, metadata and file when resuming. Optional
|
|
36
|
+
--state PATH and --timeout-ms N (maximum 120000). Review-managed articles stop
|
|
37
|
+
before uploading and require their existing exact-revision editorial workflow.
|
|
38
|
+
|
|
14
39
|
replace-cover selects an already public, scanned Photo asset. It reads measured
|
|
15
40
|
hero and social renditions, preserves the article text, and changes media with
|
|
16
41
|
the supplied version fence. It never uploads raw bytes, republishes the story,
|
|
@@ -29,13 +54,35 @@ that every website/CDN cache already displays the new image.
|
|
|
29
54
|
try {
|
|
30
55
|
const { values, positionals } = parseArgs({
|
|
31
56
|
allowPositionals: true, strict: true,
|
|
32
|
-
options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url'].map(k => [k, { type: 'string' }]).concat([['help', { type: 'boolean' }]])),
|
|
57
|
+
options: Object.fromEntries(['publication', 'article', 'file-id', 'expected-version', 'metadata', 'application-id', 'api-url', 'url', 'file', 'operation-key', 'state', 'timeout-ms','patch','expected-generation','enable-review-policy'].map(k => [k, { type: 'string' }]).concat([['help', { type: 'boolean' }],['release',{type:'boolean'}]])),
|
|
33
58
|
});
|
|
34
59
|
if (values.help || !positionals.length) {
|
|
35
60
|
console.log(help);
|
|
36
61
|
} else {
|
|
37
62
|
const [command, ...extra] = positionals;
|
|
38
|
-
|
|
63
|
+
const commandStarted = performance.now();
|
|
64
|
+
const budget = Number(values['timeout-ms'] ?? (command==='edit'?10000:30000));
|
|
65
|
+
if (!Number.isFinite(budget) || budget < 100 || budget > 120000) throw Error('--timeout-ms must be between 100 and 120000');
|
|
66
|
+
const remaining = () => { const ms = Math.floor(budget - (performance.now()-commandStarted)); if (ms < 1) throw Error('Operation deadline reached; resume the same operation'); return ms; };
|
|
67
|
+
if (extra.length || !['inspect', 'replace-cover', 'replace-photo','edit'].includes(command)) throw Error('Use inspect, edit, replace-cover or replace-photo; see --help');
|
|
68
|
+
if (values.url) {
|
|
69
|
+
if (!process.env.SCALEMULE_API_KEY || !process.env.SCALEMULE_ACCESS_TOKEN || values['application-id']) throw Error('URL resolution requires normal publication member/API-key credentials');
|
|
70
|
+
const url = new URL(values.url);
|
|
71
|
+
if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw Error('Use the public HTTPS article URL, without a private review key');
|
|
72
|
+
const api = values['api-url'] ?? 'https://api.scalemule.com';
|
|
73
|
+
const apiOrigin = new URL(api); if (apiOrigin.protocol !== 'https:' || apiOrigin.username || apiOrigin.password) throw Error('Use an HTTPS platform API endpoint');
|
|
74
|
+
const headers = { 'x-api-key': process.env.SCALEMULE_API_KEY, authorization: 'Bearer '+process.env.SCALEMULE_ACCESS_TOKEN };
|
|
75
|
+
const get = async route => { const r = await fetch(api.replace(/\/$/,'')+route,{headers,redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(10000,remaining()))}); const v = await r.json(); if (!r.ok || !v.success) throw Error(`Article resolution HTTP ${r.status}`); return v.data; };
|
|
76
|
+
const pubs = await get('/v1/blog/publications');
|
|
77
|
+
const pub = pubs.items?.find(p=>new URL(p.canonical_url).origin===url.origin);
|
|
78
|
+
if (!pub) throw Error('The caller has no publication grant for that article origin');
|
|
79
|
+
const match = /^\/news\/([^/]+)\/?$/.exec(url.pathname);
|
|
80
|
+
if (!match) throw Error('Use the publication article slug URL');
|
|
81
|
+
const resolved = await get(`/v1/blog/publications/${encodeURIComponent(pub.id)}/article-content/${encodeURIComponent(decodeURIComponent(match[1]))}`);
|
|
82
|
+
if (!resolved.article?.id) throw Error('No published article at this URL');
|
|
83
|
+
if ((values.publication && values.publication !== pub.id) || (values.article && values.article !== resolved.article.id)) throw Error('Article URL and supplied IDs disagree');
|
|
84
|
+
values.publication = pub.id; values.article = resolved.article.id;
|
|
85
|
+
}
|
|
39
86
|
for (const key of ['publication', 'article']) if (!values[key]) throw Error(`Missing --${key}`);
|
|
40
87
|
if (!process.env.SCALEMULE_ACCESS_TOKEN) throw Error('SCALEMULE_ACCESS_TOKEN is required; use normal member or platform admin authorization');
|
|
41
88
|
if (!values['application-id'] && !process.env.SCALEMULE_API_KEY) throw Error('Member calls also require SCALEMULE_API_KEY');
|
|
@@ -62,6 +109,114 @@ try {
|
|
|
62
109
|
capabilities: options.capabilities, narration: processing.narration,
|
|
63
110
|
assets: processing.assets, collaboration, elapsed_ms: Math.round(performance.now() - started),
|
|
64
111
|
}, null, 2));
|
|
112
|
+
} else if (command==='edit') {
|
|
113
|
+
for(const key of ['patch','operation-key'])if(!values[key])throw Error('Missing --'+key);
|
|
114
|
+
const patch=JSON.parse(await readFile(values.patch,'utf8'));
|
|
115
|
+
const key=createHash('sha256').update(JSON.stringify(['edit',values['api-url']??'https://api.scalemule.com',values.publication,values.article,values['operation-key']])).digest('hex');
|
|
116
|
+
const statePath=values.state??join(homedir(),'.scalemule','newsroom','operations',key+'.json');
|
|
117
|
+
await mkdir(dirname(statePath),{recursive:true,mode:0o700});
|
|
118
|
+
const lockPath=statePath+'.lock';let lock;
|
|
119
|
+
try{lock=await open(lockPath,'wx',0o600);}catch(error){
|
|
120
|
+
if(error.code!=='EEXIST')throw error;
|
|
121
|
+
const pid=Number(await readFile(lockPath,'utf8'));if(!Number.isSafeInteger(pid)||pid<1)throw Error('Invalid operation lock; inspect '+lockPath);
|
|
122
|
+
let alive=true;try{process.kill(pid,0);}catch(e){if(e.code==='ESRCH')alive=false;else throw e;}
|
|
123
|
+
if(alive)throw Error('This edit operation is already running');
|
|
124
|
+
await unlink(lockPath);lock=await open(lockPath,'wx',0o600);
|
|
125
|
+
}
|
|
126
|
+
await lock.writeFile(String(process.pid));await lock.close();
|
|
127
|
+
try{
|
|
128
|
+
let checkpoint;try{checkpoint=JSON.parse(await readFile(statePath,'utf8'));}catch(error){if(error.code!=='ENOENT')throw error;}
|
|
129
|
+
const result=await client.editArticle({articleId:values.article,operationKey:values['operation-key'],patch,
|
|
130
|
+
expectedGeneration:values['expected-generation']===undefined?undefined:Number(values['expected-generation']),
|
|
131
|
+
enableReviewPolicy:values['enable-review-policy'],release:values.release,checkpoint,timeoutMs:remaining(),
|
|
132
|
+
saveCheckpoint:async state=>{const temp=statePath+'.'+process.pid+'.tmp';await writeFile(temp,JSON.stringify(state),{mode:0o600});await rename(temp,statePath);checkpoint=state;},
|
|
133
|
+
});
|
|
134
|
+
if(values.url&&values.release&&['published','already_published'].includes(result.state)){
|
|
135
|
+
let verified=false,reason;
|
|
136
|
+
try{
|
|
137
|
+
const snapshot=await client.publicArticle(checkpoint.sourceIdentity.slug,AbortSignal.timeout(remaining()));
|
|
138
|
+
const article=snapshot.article;
|
|
139
|
+
if(!article||!article.content_html||article.id!==values.article)throw Error('Published reader snapshot is missing');
|
|
140
|
+
while(!verified){
|
|
141
|
+
const response=await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
|
|
142
|
+
const html=await response.text();
|
|
143
|
+
verified=response.ok&&articleEditHtmlMatches(html,{title:article.title,content_html:article.content_html,card_titles:(article.metadata?.cards??[]).map(c=>c.title)});
|
|
144
|
+
if(!verified)await new Promise(r=>setTimeout(r,Math.min(300,remaining())));
|
|
145
|
+
}
|
|
146
|
+
}catch(error){reason=error.message;}
|
|
147
|
+
if(!verified){console.error(JSON.stringify({...result,state:'published_public_verification_pending',stage:'public_page',message:'The exact revision is published; rendered prose/cards remain unverified. Resume the same edit.',reason}));process.exitCode=2;}
|
|
148
|
+
else console.log(JSON.stringify({...result,public_page_verified:true,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
149
|
+
}else{
|
|
150
|
+
console.log(JSON.stringify({...result,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
151
|
+
}
|
|
152
|
+
}finally{await unlink(lockPath);}
|
|
153
|
+
} else if (command === 'replace-photo') {
|
|
154
|
+
for (const key of ['file','metadata','operation-key']) if (!values[key]) throw Error(`Missing --${key}`);
|
|
155
|
+
if (values['application-id']) throw Error('Local-photo intake uses normal publication member credentials');
|
|
156
|
+
const metadata = JSON.parse(await readFile(values.metadata,'utf8'));
|
|
157
|
+
for (const key of ['caption','credit','alt']) if (typeof metadata[key] !== 'string') throw Error(`photo.json requires a string ${key}`);
|
|
158
|
+
const key = createHash('sha256').update(JSON.stringify([values['api-url']??'https://api.scalemule.com',values.publication,values.article,values['operation-key']])).digest('hex');
|
|
159
|
+
const statePath = values.state ?? join(homedir(),'.scalemule','newsroom','operations',key+'.json');
|
|
160
|
+
await mkdir(dirname(statePath),{recursive:true,mode:0o700});
|
|
161
|
+
const lockPath = statePath+'.lock';
|
|
162
|
+
let lock;
|
|
163
|
+
try { lock = await open(lockPath,'wx',0o600); }
|
|
164
|
+
catch (error) {
|
|
165
|
+
if (error.code !== 'EEXIST') throw error;
|
|
166
|
+
const pid = Number(await readFile(lockPath,'utf8'));
|
|
167
|
+
if (!Number.isSafeInteger(pid) || pid < 1) throw Error('Operation lock is invalid; inspect '+lockPath);
|
|
168
|
+
let alive = true; try { process.kill(pid,0); } catch (e) { if (e.code === 'ESRCH') alive = false; else throw e; }
|
|
169
|
+
if (alive) throw Error('This operation is already running; use its existing result');
|
|
170
|
+
await unlink(lockPath); lock = await open(lockPath,'wx',0o600);
|
|
171
|
+
}
|
|
172
|
+
await lock.writeFile(String(process.pid)); await lock.close();
|
|
173
|
+
try {
|
|
174
|
+
let checkpoint;
|
|
175
|
+
try { checkpoint = JSON.parse(await readFile(statePath,'utf8')); } catch(error) { if (error.code !== 'ENOENT') throw error; }
|
|
176
|
+
const expectedVersion = Number(values['expected-version'] ?? checkpoint?.expectedVersion ?? (await client.processing(values.article,AbortSignal.timeout(remaining()))).version);
|
|
177
|
+
const bytes = await readFile(values.file);
|
|
178
|
+
const saveCheckpoint = async state => { const temp = statePath+'.'+process.pid+'.tmp'; await writeFile(temp,JSON.stringify(state),{mode:0o600}); await rename(temp,statePath); checkpoint = state; };
|
|
179
|
+
const result = await client.uploadAndReplacePublishedCover({
|
|
180
|
+
articleId:values.article, expectedVersion, operationKey:values['operation-key'],file:new Blob([bytes]),filename:basename(values.file),
|
|
181
|
+
caption:metadata.caption,credit:metadata.credit,alt:metadata.alt,rightsConfirmed:metadata.rightsConfirmed,aiGenerated:metadata.aiGenerated,label:metadata.label,focalPoint:metadata.focalPoint,
|
|
182
|
+
checkpoint,saveCheckpoint,timeoutMs:remaining(),
|
|
183
|
+
onProgress:event=>console.error(JSON.stringify(event)),
|
|
184
|
+
});
|
|
185
|
+
if (values.url) {
|
|
186
|
+
let visible = false;
|
|
187
|
+
let failure;
|
|
188
|
+
try {
|
|
189
|
+
const images = [checkpoint?.payload?.cover_image_url, checkpoint?.payload?.og_image_url];
|
|
190
|
+
const leads = new Set([images[0],...(checkpoint?.payload?.cover_image?.variants??[]).map(v=>v.url)].filter(Boolean));
|
|
191
|
+
while (!visible) {
|
|
192
|
+
const response = await fetch(values.url,{redirect:'error',cache:'no-store',signal:AbortSignal.timeout(Math.min(10000,remaining()))});
|
|
193
|
+
const html = await response.text();
|
|
194
|
+
visible = response.ok && images.every(Boolean) && articlePhotoHtmlMatches(html,[...leads],images[1]);
|
|
195
|
+
if (!visible) {
|
|
196
|
+
// Sites may select another measured responsive rendition of the
|
|
197
|
+
// same Photo. Read that evidence; never infer a CDN path or ID.
|
|
198
|
+
await Promise.all(['image/webp','image/jpeg','image/avif'].map(async accept=>{
|
|
199
|
+
const api=(values['api-url']??'https://api.scalemule.com').replace(/\/$/,'');
|
|
200
|
+
const r=await fetch(api+'/v1/photos/public/'+encodeURIComponent(result.file_id)+'/manifest?preset=hero',{headers:{accept},redirect:'error',signal:AbortSignal.timeout(Math.min(5000,remaining()))});
|
|
201
|
+
const value=await r.json();
|
|
202
|
+
if(!r.ok || !value.success || value.data?.file_id!==result.file_id) return;
|
|
203
|
+
for(const v of Object.values(value.data.variant_metadata??{})) {
|
|
204
|
+
if(!Number.isInteger(v.width)||v.width<1||!Number.isInteger(v.height)||v.height<1) continue;
|
|
205
|
+
const u=new URL(v.url);
|
|
206
|
+
if(u.protocol==='https:'&&!u.username&&!u.password&&!u.search&&!u.hash) leads.add(v.url);
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
visible = response.ok && images.every(Boolean) && articlePhotoHtmlMatches(html,[...leads],images[1]);
|
|
210
|
+
}
|
|
211
|
+
if (!visible) await new Promise(resolve=>setTimeout(resolve,Math.min(500,remaining())));
|
|
212
|
+
}
|
|
213
|
+
} catch(error) { failure = error.message; }
|
|
214
|
+
if (!visible) {
|
|
215
|
+
console.error(JSON.stringify({...result,state:'attached_public_verification_pending',stage:'public_page',message:'The API attachment is verified; public HTML visibility remains pending. Resume this same operation.',reason:failure}));
|
|
216
|
+
process.exitCode = 2;
|
|
217
|
+
} else console.log(JSON.stringify({...result,public_page_verified:true,total_elapsed_ms:Math.round(performance.now()-commandStarted)},null,2));
|
|
218
|
+
} else console.log(JSON.stringify(result,null,2));
|
|
219
|
+
} finally { await unlink(lockPath); }
|
|
65
220
|
} else {
|
|
66
221
|
for (const key of ['file-id', 'expected-version', 'metadata']) if (!values[key]) throw Error(`Missing --${key}`);
|
|
67
222
|
const metadata = JSON.parse(await readFile(values.metadata, 'utf8'));
|
|
@@ -78,6 +233,6 @@ try {
|
|
|
78
233
|
}
|
|
79
234
|
}
|
|
80
235
|
} catch (error) {
|
|
81
|
-
console.error(JSON.stringify({ error: error.message, status: error.status }));
|
|
82
|
-
process.exitCode = 1;
|
|
236
|
+
console.error(JSON.stringify({ error: error.message, status: error.status, code:error.code, stage:error.stage, file_id:error.fileId,revision_id:error.revisionId }));
|
|
237
|
+
process.exitCode = error.code==='RELEASE_HELD'?2:1;
|
|
83
238
|
}
|
|
@@ -18,4 +18,4 @@ declare function articlePresentation(value?: ArticlePresentation | null): Articl
|
|
|
18
18
|
declare function articleHeroRatio(layout: ArticleLayout): number;
|
|
19
19
|
declare function articleFormatLabel(format?: ArticleFormat | null): string | undefined;
|
|
20
20
|
|
|
21
|
-
export { type ArticleFormat as A, type
|
|
21
|
+
export { type ArticleFormat as A, type ArticlePresentation as a, type ArticleLayout as b, articleFormatLabel as c, articleHeroRatio as d, articlePresentation as e };
|
|
@@ -18,4 +18,4 @@ declare function articlePresentation(value?: ArticlePresentation | null): Articl
|
|
|
18
18
|
declare function articleHeroRatio(layout: ArticleLayout): number;
|
|
19
19
|
declare function articleFormatLabel(format?: ArticleFormat | null): string | undefined;
|
|
20
20
|
|
|
21
|
-
export { type ArticleFormat as A, type
|
|
21
|
+
export { type ArticleFormat as A, type ArticlePresentation as a, type ArticleLayout as b, articleFormatLabel as c, articleHeroRatio as d, articlePresentation as e };
|