@faable/faable 1.20.0 → 1.22.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.
|
@@ -9,6 +9,7 @@ import { git_context } from './git_context.js';
|
|
|
9
9
|
import { deploy_remote } from './remote/index.js';
|
|
10
10
|
import { resolve_app_id } from './resolve_app_id.js';
|
|
11
11
|
import { secrets } from './secrets/index.js';
|
|
12
|
+
import { is_superseded } from './superseded.js';
|
|
12
13
|
import { mark_build_failure, start_log_sync, upload_logs } from './upload_logs.js';
|
|
13
14
|
import { upload_tag } from './upload_tag.js';
|
|
14
15
|
|
|
@@ -58,6 +59,13 @@ const deploy = {
|
|
|
58
59
|
const config = Configuration.instance().deployConfig();
|
|
59
60
|
const app_id = await resolve_app_id(args.app_id, ctx.appId, api, workdir);
|
|
60
61
|
const app = await api.getApp(app_id);
|
|
62
|
+
// Monorepo Root Directory: the server is the source of truth
|
|
63
|
+
// (App.root_dir), so no repo config file is needed. A local
|
|
64
|
+
// faable.json rootDir (dev override) still wins if set.
|
|
65
|
+
if (!config.rootDir && app.root_dir) {
|
|
66
|
+
config.rootDir = app.root_dir;
|
|
67
|
+
log.info(`📁 Monorepo root directory (from app): ${app.root_dir}`);
|
|
68
|
+
}
|
|
61
69
|
// Capture the commit/ref/actor so the deployment records which commit
|
|
62
70
|
// it came from and who pushed it (env in CI, git fallback locally).
|
|
63
71
|
const git = await git_context({ workdir });
|
|
@@ -179,6 +187,10 @@ const deploy = {
|
|
|
179
187
|
log.info(`⏳ Waiting for deployment to be promoted...`);
|
|
180
188
|
let promoted = false;
|
|
181
189
|
let failure = null;
|
|
190
|
+
let superseded = null;
|
|
191
|
+
// The pointer at wait-start is the PREVIOUS deployment (older than ours)
|
|
192
|
+
// — only a pointer CHANGE to something that isn't us needs a look.
|
|
193
|
+
const initialActiveId = app.status?.deployment;
|
|
182
194
|
while (Date.now() - start < timeoutMs) {
|
|
183
195
|
await wait(intervalMs);
|
|
184
196
|
try {
|
|
@@ -187,6 +199,18 @@ const deploy = {
|
|
|
187
199
|
promoted = true;
|
|
188
200
|
break;
|
|
189
201
|
}
|
|
202
|
+
// Superseded: the pointer moved to a deployment at least as new as
|
|
203
|
+
// ours (twin workflows on the same push, or a rapid follow-up
|
|
204
|
+
// deploy). It only moves forward — waiting longer can never succeed.
|
|
205
|
+
const activeId = current.status?.deployment;
|
|
206
|
+
if (activeId && activeId !== initialActiveId) {
|
|
207
|
+
const active = await api.getDeployment(activeId).catch(() => null);
|
|
208
|
+
if (active &&
|
|
209
|
+
is_superseded(deployment, active)) {
|
|
210
|
+
superseded = activeId;
|
|
211
|
+
break;
|
|
212
|
+
}
|
|
213
|
+
}
|
|
190
214
|
// Watch for a terminal runtime failure so we fail fast (and red)
|
|
191
215
|
// instead of timing out green. The controller marks a crash-looping
|
|
192
216
|
// deployment ERROR — with a reason (e.g. a missing module) — within
|
|
@@ -197,6 +221,12 @@ const deploy = {
|
|
|
197
221
|
failure = { phase, reason: dep.status?.reason };
|
|
198
222
|
break;
|
|
199
223
|
}
|
|
224
|
+
// CANCELED: the platform superseded this build before it produced a
|
|
225
|
+
// runnable (a sibling deployment of the app won the promotion).
|
|
226
|
+
if (phase === "CANCELED") {
|
|
227
|
+
superseded = dep.status?.reason ?? "canceled";
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
200
230
|
}
|
|
201
231
|
catch (_error) {
|
|
202
232
|
// Ignore transient errors while polling and keep waiting
|
|
@@ -206,6 +236,12 @@ const deploy = {
|
|
|
206
236
|
if (promoted) {
|
|
207
237
|
log.info(`🌍 Deployment promoted and live, visit: https://${app.url}`);
|
|
208
238
|
}
|
|
239
|
+
else if (superseded) {
|
|
240
|
+
// Not a failure: the app IS live, just on a sibling deployment (e.g.
|
|
241
|
+
// duplicated workflows firing on the same push). Green exit — there is
|
|
242
|
+
// nothing for the user to fix in THIS run.
|
|
243
|
+
log.warn(`⚠️ Deployment superseded — a newer deployment of this app was promoted (${superseded}). The app is live: https://${app.url}`);
|
|
244
|
+
}
|
|
209
245
|
else if (failure) {
|
|
210
246
|
// Fail the command (non-zero exit → red GitHub Action) and surface the
|
|
211
247
|
// reason the controller captured so the user sees WHY without digging.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Superseded detection for the promotion wait (deploy v3, twin-workflow
|
|
3
|
+
* case): the app's promoted pointer only ever moves FORWARD (the controller
|
|
4
|
+
* refuses to promote a deployment older than the active one), so once a
|
|
5
|
+
* deployment at-least-as-new as ours is promoted, waiting longer can never
|
|
6
|
+
* succeed. Twins from duplicated workflows land here — one wins the pointer,
|
|
7
|
+
* the other must exit cleanly instead of hanging to the timeout.
|
|
8
|
+
*
|
|
9
|
+
* `>=` on purpose: same-instant twins (created ms apart) supersede each
|
|
10
|
+
* other in whichever order the promoter resolved them.
|
|
11
|
+
*/
|
|
12
|
+
const is_superseded = (mine, active) => Boolean(active &&
|
|
13
|
+
active.id !== mine.id &&
|
|
14
|
+
new Date(active.createdAt).getTime() >=
|
|
15
|
+
new Date(mine.createdAt).getTime());
|
|
16
|
+
|
|
17
|
+
export { is_superseded };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faable/faable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.22.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Pomar <marc@faable.com>",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@actions/core": "^3.0.0",
|
|
30
|
-
"@faabletools/buildpacks": "^1.
|
|
30
|
+
"@faabletools/buildpacks": "^1.7.0",
|
|
31
31
|
"axios": "^1.13.2",
|
|
32
32
|
"fs-extra": "^11.3.2",
|
|
33
33
|
"handlebars": "^4.7.8",
|