@faable/faable 1.15.2 → 1.15.4
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.
|
@@ -8,7 +8,7 @@ import { check_environment } from './check_environment.js';
|
|
|
8
8
|
import { git_context } from './git_context.js';
|
|
9
9
|
import { resolve_app_id } from './resolve_app_id.js';
|
|
10
10
|
import { secrets } from './secrets/index.js';
|
|
11
|
-
import {
|
|
11
|
+
import { mark_build_failure, start_log_sync, upload_logs } from './upload_logs.js';
|
|
12
12
|
import { upload_tag } from './upload_tag.js';
|
|
13
13
|
|
|
14
14
|
const deploy = {
|
|
@@ -41,20 +41,43 @@ const deploy = {
|
|
|
41
41
|
const workdir = args.workdir || process.cwd();
|
|
42
42
|
const ctx = await requireApi();
|
|
43
43
|
const { api } = ctx;
|
|
44
|
-
// Resolve the buildpack plan (detection or forced override). All the
|
|
45
|
-
// build thinking happens here; build() below just executes the plan.
|
|
46
44
|
const config = Configuration.instance().deployConfig();
|
|
47
|
-
const plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
|
|
48
45
|
const app_id = await resolve_app_id(args.app_id, ctx.appId, api, workdir);
|
|
49
46
|
const app = await api.getApp(app_id);
|
|
50
47
|
// Capture the commit/ref/actor so the deployment records which commit
|
|
51
48
|
// it came from and who pushed it (env in CI, git fallback locally).
|
|
52
49
|
const git = await git_context({ workdir });
|
|
50
|
+
// Resolve the buildpack plan (detection or forced override). All the build
|
|
51
|
+
// thinking happens here; build() below just executes the plan. Detection can
|
|
52
|
+
// fail (e.g. an app whose start command can't be inferred) — and it runs
|
|
53
|
+
// before the create-first deployment exists, so without this a detection
|
|
54
|
+
// failure would leave NO deployment row: no deploy-failed email, nothing in
|
|
55
|
+
// the dashboard, just a red X in the CI logs the user may never see. Record
|
|
56
|
+
// it as a failed deployment instead.
|
|
57
|
+
let plan;
|
|
58
|
+
try {
|
|
59
|
+
plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
// Log the reason into the build buffer first (so it rides along in the
|
|
63
|
+
// attached logs), then record a typeless BUILD_ERROR row — typeless so it
|
|
64
|
+
// can't rewrite the app's runtime_strategy. Best-effort: a create that
|
|
65
|
+
// itself fails (e.g. the free-plan quota gate) must not mask the original
|
|
66
|
+
// detection error.
|
|
67
|
+
log.error(error.message);
|
|
68
|
+
const failed = await api
|
|
69
|
+
.createDeployment({ app_id: app.id, ...git })
|
|
70
|
+
.catch(() => null);
|
|
71
|
+
if (failed) {
|
|
72
|
+
await mark_build_failure(api, { deployment_id: failed.id, app });
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
53
76
|
// Create-first: register the deployment BEFORE building. Gate rejections
|
|
54
77
|
// (free-plan quota 429, disabled app 409) surface here, at second 0 —
|
|
55
78
|
// before any build minutes are spent. The row is born QUEUED; the CLI
|
|
56
79
|
// owns it until the built image lands (or the build fails). `type` rides
|
|
57
|
-
// on the create
|
|
80
|
+
// on the create — the buildpack plan is already resolved.
|
|
58
81
|
const deployment = await api.createDeployment({
|
|
59
82
|
app_id: app.id,
|
|
60
83
|
type: plan.type,
|
|
@@ -82,11 +105,15 @@ const deploy = {
|
|
|
82
105
|
const stop_log_sync = start_log_sync(api, deployment.id);
|
|
83
106
|
try {
|
|
84
107
|
await buildpack.build({ workdir, config, app, env_vars }, plan);
|
|
85
|
-
// Upload to Faable registry
|
|
86
|
-
const {
|
|
108
|
+
// Upload to Faable registry, tagged by version and pinned to digest
|
|
109
|
+
const { image_ref } = await upload_tag({
|
|
110
|
+
app,
|
|
111
|
+
api,
|
|
112
|
+
deployment_id: deployment.id
|
|
113
|
+
});
|
|
87
114
|
// Complete the deployment with the built image — this is the handoff:
|
|
88
115
|
// the controller claims it and materializes the workload.
|
|
89
|
-
await api.completeDeployment(deployment.id,
|
|
116
|
+
await api.completeDeployment(deployment.id, image_ref);
|
|
90
117
|
}
|
|
91
118
|
finally {
|
|
92
119
|
stop_log_sync();
|
|
@@ -1,22 +1,73 @@
|
|
|
1
1
|
import { log } from '../../log.js';
|
|
2
2
|
import { cmd } from '../../lib/cmd.js';
|
|
3
3
|
|
|
4
|
+
/**
|
|
5
|
+
* `<hostname>/<image>:<deployment_id>` — one tag per build, never
|
|
6
|
+
* overwritten. Without a tag docker resolves `:latest`, which every deploy
|
|
7
|
+
* rewrites: any rescheduled pod (node drain, eviction) would silently pull
|
|
8
|
+
* whatever build was pushed last, breaking the deployment ↔ code identity.
|
|
9
|
+
*/
|
|
10
|
+
const build_upload_tagname = (hostname, image, deployment_id) => `${hostname}/${image}:${deployment_id}`;
|
|
11
|
+
/**
|
|
12
|
+
* Strips the tag from an image ref, keeping registry ports intact
|
|
13
|
+
* (`host:5000/img:tag` → `host:5000/img`). A colon only denotes a tag when
|
|
14
|
+
* it appears after the last path segment separator.
|
|
15
|
+
*/
|
|
16
|
+
const strip_image_tag = (ref) => {
|
|
17
|
+
const last_slash = ref.lastIndexOf("/");
|
|
18
|
+
const last_colon = ref.lastIndexOf(":");
|
|
19
|
+
return last_colon > last_slash ? ref.slice(0, last_colon) : ref;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Pins a pushed tag to its content digest: `repo:tag@sha256:…`. Kubernetes
|
|
23
|
+
* pulls by digest (immutable even if the tag were re-pushed); the tag stays
|
|
24
|
+
* as the human-readable label. `RepoDigests` entries come as `repo@sha256:…`
|
|
25
|
+
* (no tag) and may reference other registries — only the entry for this
|
|
26
|
+
* repo counts. Returns null when none matches (caller falls back to the tag).
|
|
27
|
+
*/
|
|
28
|
+
const pin_tag_to_digest = (tagname, repo_digests) => {
|
|
29
|
+
const repo = strip_image_tag(tagname);
|
|
30
|
+
const match = repo_digests.find((d) => d.startsWith(`${repo}@sha256:`));
|
|
31
|
+
if (!match)
|
|
32
|
+
return null;
|
|
33
|
+
return `${tagname}@${match.slice(repo.length + 1)}`;
|
|
34
|
+
};
|
|
4
35
|
const upload_tag = async (args) => {
|
|
5
|
-
const { api, app } = args;
|
|
36
|
+
const { api, app, deployment_id } = args;
|
|
6
37
|
log.info(`🔁 Uploading...`);
|
|
7
38
|
const registry = await api.getRegistry(app.id);
|
|
8
39
|
// Registry login
|
|
9
40
|
const { user, password, hostname, image } = registry;
|
|
10
41
|
await cmd(`echo "${password}" | docker login --username "${user}" --password-stdin ${hostname}`);
|
|
11
|
-
// Tag
|
|
12
|
-
const upload_tagname =
|
|
42
|
+
// Tag the local build (tagged `app.id` by the buildpack) for this version
|
|
43
|
+
const upload_tagname = build_upload_tagname(hostname, image, deployment_id);
|
|
13
44
|
await cmd(`docker tag ${app.id} ${upload_tagname}`);
|
|
14
45
|
// Upload the image to faable registry
|
|
15
46
|
await cmd(`docker push ${upload_tagname}`);
|
|
47
|
+
// Pin to the digest the registry just assigned. Best-effort: the tagged
|
|
48
|
+
// ref alone is already unique per build, the digest just makes it
|
|
49
|
+
// tamper-proof.
|
|
50
|
+
let image_ref = upload_tagname;
|
|
51
|
+
try {
|
|
52
|
+
const inspect = await cmd(`docker inspect --format '{{json .RepoDigests}}' ${upload_tagname}`);
|
|
53
|
+
const repo_digests = JSON.parse(String(inspect.stdout).trim());
|
|
54
|
+
const pinned = pin_tag_to_digest(upload_tagname, repo_digests);
|
|
55
|
+
if (pinned) {
|
|
56
|
+
image_ref = pinned;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
log.warn(`Could not resolve the pushed digest; deploying by tag only.`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
log.warn(`Could not inspect the pushed image (${error.message}); deploying by tag only.`);
|
|
64
|
+
}
|
|
16
65
|
log.info(`✅ Upload completed.`);
|
|
17
66
|
return {
|
|
18
67
|
upload_tagname,
|
|
68
|
+
/** Immutable ref recorded on the deployment: `repo:tag@sha256:…` */
|
|
69
|
+
image_ref,
|
|
19
70
|
};
|
|
20
71
|
};
|
|
21
72
|
|
|
22
|
-
export { upload_tag };
|
|
73
|
+
export { build_upload_tagname, pin_tag_to_digest, strip_image_tag, upload_tag };
|