@faable/faable 1.23.0 → 1.24.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.
- package/dist/commands/deploy/index.js +8 -131
- package/dist/commands/deploy/remote/index.js +20 -33
- package/dist/commands/deploy/remote/manifest.js +4 -4
- package/package.json +1 -2
- package/dist/commands/deploy/check_environment.js +0 -12
- package/dist/commands/deploy/upload_logs.js +0 -67
- package/dist/commands/deploy/upload_tag.js +0 -73
|
@@ -2,16 +2,11 @@ import { requireApi } from '../../api/context.js';
|
|
|
2
2
|
import { Configuration } from '../../lib/Configuration.js';
|
|
3
3
|
import { log } from '../../log.js';
|
|
4
4
|
import { link } from '../link/index.js';
|
|
5
|
-
import { configure_buildpacks, detect_buildpack, plan_summary, get_buildpack, buildpack_names } from '@faabletools/buildpacks';
|
|
6
|
-
import { cmd } from '../../lib/cmd.js';
|
|
7
|
-
import { check_environment } from './check_environment.js';
|
|
8
5
|
import { git_context } from './git_context.js';
|
|
9
6
|
import { deploy_remote } from './remote/index.js';
|
|
10
7
|
import { resolve_app_id } from './resolve_app_id.js';
|
|
11
8
|
import { secrets } from './secrets/index.js';
|
|
12
9
|
import { is_superseded } from './superseded.js';
|
|
13
|
-
import { mark_build_failure, start_log_sync, upload_logs } from './upload_logs.js';
|
|
14
|
-
import { upload_tag } from './upload_tag.js';
|
|
15
10
|
|
|
16
11
|
const deploy = {
|
|
17
12
|
command: 'deploy [app_id]',
|
|
@@ -31,29 +26,10 @@ const deploy = {
|
|
|
31
26
|
type: 'string',
|
|
32
27
|
description: 'Working directory'
|
|
33
28
|
})
|
|
34
|
-
.option('buildpack', {
|
|
35
|
-
alias: 'b',
|
|
36
|
-
type: 'string',
|
|
37
|
-
choices: buildpack_names(),
|
|
38
|
-
description: 'Force a specific buildpack (overrides auto-detection and faable.json)'
|
|
39
|
-
})
|
|
40
|
-
.option('remote', {
|
|
41
|
-
type: 'boolean',
|
|
42
|
-
description: 'Build server-side (remote build), regardless of the app build_mode'
|
|
43
|
-
})
|
|
44
|
-
.option('local', {
|
|
45
|
-
type: 'boolean',
|
|
46
|
-
description: 'Build locally with Docker, even if the app is set to remote builds'
|
|
47
|
-
})
|
|
48
|
-
.conflicts('remote', 'local')
|
|
49
29
|
.showHelpOnFail(false);
|
|
50
30
|
},
|
|
51
31
|
handler: async (args) => {
|
|
52
32
|
const workdir = args.workdir || process.cwd();
|
|
53
|
-
// Wire the shared buildpacks package to the CLI's sinks: pino (which tees
|
|
54
|
-
// into the build-log buffer) and cmd() (which captures subprocess output
|
|
55
|
-
// into the same buffer). Must happen before any detect/build call.
|
|
56
|
-
configure_buildpacks({ log, exec: cmd });
|
|
57
33
|
// Pass the explicit app target to the OIDC exchange so a monorepo (several
|
|
58
34
|
// apps, one repo) can be disambiguated in CI.
|
|
59
35
|
const ctx = await requireApi(args.app_id);
|
|
@@ -71,113 +47,14 @@ const deploy = {
|
|
|
71
47
|
// Capture the commit/ref/actor so the deployment records which commit
|
|
72
48
|
// it came from and who pushed it (env in CI, git fallback locally).
|
|
73
49
|
const git = await git_context({ workdir });
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
//
|
|
77
|
-
//
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
try {
|
|
83
|
-
plan = await detect_buildpack({ workdir, config }, args.buildpack || config.buildpack);
|
|
84
|
-
}
|
|
85
|
-
catch (error) {
|
|
86
|
-
// Log the reason into the build buffer first (so it rides along in the
|
|
87
|
-
// attached logs), then record a typeless BUILD_ERROR row — typeless so it
|
|
88
|
-
// can't rewrite the app's runtime_strategy. Best-effort: a create that
|
|
89
|
-
// itself fails (e.g. the free-plan quota gate) must not mask the original
|
|
90
|
-
// detection error.
|
|
91
|
-
log.error(error.message);
|
|
92
|
-
const failed = await api
|
|
93
|
-
.createDeployment({ app_id: app.id, ...git })
|
|
94
|
-
.catch(() => null);
|
|
95
|
-
if (failed) {
|
|
96
|
-
await mark_build_failure(api, { deployment_id: failed.id, app });
|
|
97
|
-
}
|
|
98
|
-
throw error;
|
|
99
|
-
}
|
|
100
|
-
// Remote build path (v2, arch/deploy/deploy-v2-remote-build.md): the
|
|
101
|
-
// server decides via the app's build_mode; --remote/--local override for
|
|
102
|
-
// testing. Pre-build failures with a server-decided mode fall back to the
|
|
103
|
-
// local build below (deploy_remote returns null); with --remote they
|
|
104
|
-
// fail hard.
|
|
105
|
-
let deployment;
|
|
106
|
-
const want_remote = !args.local && (Boolean(args.remote) || app.build_mode === 'remote');
|
|
107
|
-
const remote_deployment = want_remote
|
|
108
|
-
? await deploy_remote({
|
|
109
|
-
api,
|
|
110
|
-
app,
|
|
111
|
-
plan,
|
|
112
|
-
git,
|
|
113
|
-
workdir,
|
|
114
|
-
explicit: Boolean(args.remote)
|
|
115
|
-
})
|
|
116
|
-
: null;
|
|
117
|
-
if (remote_deployment) {
|
|
118
|
-
deployment = remote_deployment;
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
if (want_remote) {
|
|
122
|
-
log.warn('↩️ Falling back to a local build');
|
|
123
|
-
}
|
|
124
|
-
// Create-first: register the deployment BEFORE building. Gate rejections
|
|
125
|
-
// (free-plan quota 429, disabled app 409) surface here, at second 0 —
|
|
126
|
-
// before any build minutes are spent. The row is born QUEUED; the CLI
|
|
127
|
-
// owns it until the built image lands (or the build fails). `type` rides
|
|
128
|
-
// on the create — the buildpack plan is already resolved.
|
|
129
|
-
deployment = await api.createDeployment({
|
|
130
|
-
app_id: app.id,
|
|
131
|
-
type: plan.type,
|
|
132
|
-
...git
|
|
133
|
-
});
|
|
134
|
-
try {
|
|
135
|
-
// Check if we can build docker images
|
|
136
|
-
await check_environment();
|
|
137
|
-
const runtime_label = plan.runtime.version
|
|
138
|
-
? `${plan.runtime.name}-${plan.runtime.version}`
|
|
139
|
-
: plan.runtime.name;
|
|
140
|
-
log.info(`🚀 Deploying "${app.name}" (${app.id}) runtime=${runtime_label}`);
|
|
141
|
-
log.info(`🧩 Build plan ${plan_summary(plan)}`);
|
|
142
|
-
// get environment variables
|
|
143
|
-
const env_vars = await api.getAppSecrets(app.id);
|
|
144
|
-
const buildpack = get_buildpack(plan.buildpack);
|
|
145
|
-
if (!buildpack) {
|
|
146
|
-
throw new Error(`No buildpack registered for plan=${plan.buildpack}`);
|
|
147
|
-
}
|
|
148
|
-
// The build starts now: declare it (QUEUED → BUILDING) and stream the
|
|
149
|
-
// captured output to the deployment while it runs (best-effort).
|
|
150
|
-
await api
|
|
151
|
-
.updateDeploymentStatus(deployment.id, { phase: 'BUILDING' })
|
|
152
|
-
.catch((error) => log.warn(`Could not mark deployment BUILDING: ${error.message}`));
|
|
153
|
-
const stop_log_sync = start_log_sync(api, deployment.id);
|
|
154
|
-
try {
|
|
155
|
-
await buildpack.build({ workdir, config, app, env_vars, deployment }, plan);
|
|
156
|
-
// Upload to Faable registry, tagged by version and pinned to digest
|
|
157
|
-
const { image_ref } = await upload_tag({
|
|
158
|
-
app,
|
|
159
|
-
api,
|
|
160
|
-
deployment_id: deployment.id
|
|
161
|
-
});
|
|
162
|
-
// Complete the deployment with the built image — this is the handoff:
|
|
163
|
-
// the controller claims it and materializes the workload.
|
|
164
|
-
await api.completeDeployment(deployment.id, image_ref);
|
|
165
|
-
}
|
|
166
|
-
finally {
|
|
167
|
-
stop_log_sync();
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch (error) {
|
|
171
|
-
// The deployment already exists (created pre-build), so a failed build
|
|
172
|
-
// marks THAT row BUILD_ERROR with the captured logs — no extra row, no
|
|
173
|
-
// second quota hit. Gate rejections can't reach here anymore: the
|
|
174
|
-
// create happens before any building.
|
|
175
|
-
await mark_build_failure(api, { deployment_id: deployment.id, app });
|
|
176
|
-
throw error;
|
|
177
|
-
}
|
|
178
|
-
// Attach the final build output to the deployment (best-effort).
|
|
179
|
-
await upload_logs(api, deployment.id);
|
|
180
|
-
} // end local build path (remote builds upload their own logs server-side)
|
|
50
|
+
// Remote build only (arch/deploy/remote-artifact-default-cutover.md): the
|
|
51
|
+
// CLI no longer builds — it uploads the source and the platform builds
|
|
52
|
+
// server-side (framework detection, buildpacks, artifact/image output all
|
|
53
|
+
// live in the builder). No Docker, no local fallback: a rejected admission
|
|
54
|
+
// (build_mode=local opt-out, or the global kill-switch off) or a build
|
|
55
|
+
// error throws and exits red.
|
|
56
|
+
log.info(`🚀 Deploying "${app.name}" (${app.id})`);
|
|
57
|
+
const deployment = await deploy_remote({ api, app, git, workdir });
|
|
181
58
|
const dashboard_url = `https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`;
|
|
182
59
|
log.info(`Preparing to deploy in faable cloud · ${deployment.id}`);
|
|
183
60
|
log.info(`📊 View it in the dashboard -> ${dashboard_url}`);
|
|
@@ -4,42 +4,29 @@ import { collect_manifest } from './manifest.js';
|
|
|
4
4
|
import { upload_missing_blobs } from './upload.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
|
-
* Remote build
|
|
8
|
-
* source delta to the CAS, create the deployment with `source` (manifest
|
|
9
|
-
*
|
|
10
|
-
* handoff. No Docker daemon
|
|
7
|
+
* Remote build (arch/deploy/remote-artifact-default-cutover.md): upload the
|
|
8
|
+
* source delta to the CAS, create the deployment with `source` (manifest only —
|
|
9
|
+
* the builder re-detects the framework server-side) and follow the server-side
|
|
10
|
+
* build until the handoff. No Docker daemon, no local buildpacks.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* fail hard (testing wants to see the errors).
|
|
17
|
-
* - Failures AFTER the deployment exists never fall back: a BUILD_ERROR
|
|
18
|
-
* would fail locally too (it's the user's build), and a timeout may still
|
|
19
|
-
* be building — a local fallback would double-deploy.
|
|
12
|
+
* There is no local fallback anymore: the CLI no longer builds. Any failure —
|
|
13
|
+
* a rejected admission (`remote_build_disabled` when the app opted out with
|
|
14
|
+
* build_mode=local, or the global kill-switch is off), an upload error, or a
|
|
15
|
+
* BUILD_ERROR — throws so the command exits red.
|
|
20
16
|
*/
|
|
21
17
|
const deploy_remote = async (props) => {
|
|
22
|
-
const { api, app,
|
|
23
|
-
log.info(`☁️ Remote build
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
});
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (explicit)
|
|
38
|
-
throw error;
|
|
39
|
-
const code = error?.response?.data?.code;
|
|
40
|
-
log.warn(`☁️ Remote build unavailable${code ? ` (${code})` : `: ${error.message}`}`);
|
|
41
|
-
return null;
|
|
42
|
-
}
|
|
18
|
+
const { api, app, git, workdir } = props;
|
|
19
|
+
log.info(`☁️ Remote build`);
|
|
20
|
+
const manifest = await collect_manifest(workdir);
|
|
21
|
+
log.info(`🗂️ ${manifest.length} files in the source manifest`);
|
|
22
|
+
await upload_missing_blobs(api, app.id, workdir, manifest);
|
|
23
|
+
// No `plan`: the builder re-detects on the assembled tree and syncs the app's
|
|
24
|
+
// runtime_strategy on completion.
|
|
25
|
+
const deployment = await api.createDeployment({
|
|
26
|
+
app_id: app.id,
|
|
27
|
+
source: { manifest },
|
|
28
|
+
...git,
|
|
29
|
+
});
|
|
43
30
|
log.info(`Preparing to build in faable cloud · ${deployment.id}`);
|
|
44
31
|
await follow_remote_build(api, deployment.id);
|
|
45
32
|
return deployment;
|
|
@@ -15,15 +15,15 @@ const MAX_FILE_BYTES = 100 * 1024 * 1024;
|
|
|
15
15
|
* them like a fresh clone would.
|
|
16
16
|
*
|
|
17
17
|
* Symlinks are rejected (no representation in the manifest by design — see
|
|
18
|
-
* deploy-v2-remote-build.md) and a non-git directory is unsupported
|
|
19
|
-
*
|
|
18
|
+
* deploy-v2-remote-build.md) and a non-git directory is unsupported (the source
|
|
19
|
+
* tree is collected via git).
|
|
20
20
|
*/
|
|
21
21
|
const collect_manifest = async (workdir) => {
|
|
22
22
|
const inside = await cmd(`git rev-parse --is-inside-work-tree`, {
|
|
23
23
|
cwd: workdir,
|
|
24
24
|
}).catch(() => null);
|
|
25
25
|
if (!inside || String(inside.stdout).trim() !== "true") {
|
|
26
|
-
throw new Error("
|
|
26
|
+
throw new Error("Deploys need a git repository (the source tree is collected via git). Initialize one with `git init` and commit your files.");
|
|
27
27
|
}
|
|
28
28
|
// -c/-o: tracked + untracked; --exclude-standard: .gitignore/.git/info.
|
|
29
29
|
// -z: NUL-separated (paths with spaces/UTF-8 survive).
|
|
@@ -57,7 +57,7 @@ const collect_manifest = async (workdir) => {
|
|
|
57
57
|
if (stat.isDirectory())
|
|
58
58
|
continue;
|
|
59
59
|
if (stat.isSymbolicLink()) {
|
|
60
|
-
throw new Error(`Symlinks are not supported
|
|
60
|
+
throw new Error(`Symlinks are not supported: ${rel}. Replace the symlink with the real file.`);
|
|
61
61
|
}
|
|
62
62
|
if (stat.size > MAX_FILE_BYTES) {
|
|
63
63
|
throw new Error(`${rel} is too large for a remote build (${stat.size} bytes > ${MAX_FILE_BYTES}).`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@faable/faable",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.24.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Marc Pomar <marc@faable.com>",
|
|
@@ -27,7 +27,6 @@
|
|
|
27
27
|
],
|
|
28
28
|
"dependencies": {
|
|
29
29
|
"@actions/core": "^3.0.0",
|
|
30
|
-
"@faabletools/buildpacks": "^1.7.0",
|
|
31
30
|
"axios": "^1.13.2",
|
|
32
31
|
"fs-extra": "^11.3.2",
|
|
33
32
|
"handlebars": "^4.7.8",
|
|
@@ -1,67 +0,0 @@
|
|
|
1
|
-
import { buildLog } from '../../lib/log_buffer.js';
|
|
2
|
-
import { log } from '../../log.js';
|
|
3
|
-
|
|
4
|
-
// Best-effort by design: attaching logs (or recording a failed build) must
|
|
5
|
-
// never break or fail a deploy — an older API without these endpoints just
|
|
6
|
-
// produces a warn.
|
|
7
|
-
const upload_logs = async (api, deployment_id) => {
|
|
8
|
-
try {
|
|
9
|
-
const { content, truncated } = buildLog.contents();
|
|
10
|
-
if (!content)
|
|
11
|
-
return;
|
|
12
|
-
await api.uploadDeploymentLogs(deployment_id, { content, truncated });
|
|
13
|
-
log.info(`📝 Build logs attached to deployment ${deployment_id}`);
|
|
14
|
-
}
|
|
15
|
-
catch (error) {
|
|
16
|
-
log.warn(`Could not upload build logs (non-fatal): ${error.message}`);
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
// Live log sync during the build: re-upload the whole buffer every interval
|
|
20
|
-
// while it grows. The API endpoint is an idempotent replace (one log row per
|
|
21
|
-
// deployment), so partial uploads are simply superseded — the dashboard sees
|
|
22
|
-
// the build progressing instead of only the final dump. Rate-limit friendly:
|
|
23
|
-
// 10s interval = 6 req/min against the endpoint's 30/min cap.
|
|
24
|
-
const start_log_sync = (api, deployment_id, intervalMs = 10_000) => {
|
|
25
|
-
let lastSize = 0;
|
|
26
|
-
let inFlight = false;
|
|
27
|
-
const timer = setInterval(async () => {
|
|
28
|
-
if (inFlight)
|
|
29
|
-
return;
|
|
30
|
-
const { content, truncated, size } = buildLog.contents();
|
|
31
|
-
if (!content || size === lastSize)
|
|
32
|
-
return;
|
|
33
|
-
inFlight = true;
|
|
34
|
-
try {
|
|
35
|
-
await api.uploadDeploymentLogs(deployment_id, { content, truncated });
|
|
36
|
-
lastSize = size;
|
|
37
|
-
}
|
|
38
|
-
catch (error) {
|
|
39
|
-
// Quiet: the final upload_logs still runs at the end of the deploy.
|
|
40
|
-
log.debug(`Log sync failed (non-fatal): ${error.message}`);
|
|
41
|
-
}
|
|
42
|
-
finally {
|
|
43
|
-
inFlight = false;
|
|
44
|
-
}
|
|
45
|
-
}, intervalMs);
|
|
46
|
-
// Never keep the process alive just for log syncing.
|
|
47
|
-
timer.unref?.();
|
|
48
|
-
return () => clearInterval(timer);
|
|
49
|
-
};
|
|
50
|
-
// Mark the create-first deployment as a failed build (phase BUILD_ERROR) and
|
|
51
|
-
// attach the captured logs. The deployment row already exists — created
|
|
52
|
-
// before the build started — so unlike the old flow no extra row is created
|
|
53
|
-
// (and no quota is consumed) to record the failure.
|
|
54
|
-
const mark_build_failure = async (api, { deployment_id, app, }) => {
|
|
55
|
-
try {
|
|
56
|
-
await api
|
|
57
|
-
.updateDeploymentStatus(deployment_id, { phase: "BUILD_ERROR" })
|
|
58
|
-
.catch((error) => log.warn(`Could not mark deployment BUILD_ERROR: ${error.message}`));
|
|
59
|
-
await upload_logs(api, deployment_id);
|
|
60
|
-
log.error(`❌ Build failed — logs attached to deployment ${deployment_id} · https://dashboard.faable.com/deploy/${app.team}/app/${app.id}`);
|
|
61
|
-
}
|
|
62
|
-
catch (error) {
|
|
63
|
-
log.warn(`Could not report the failed build (non-fatal): ${error.message}`);
|
|
64
|
-
}
|
|
65
|
-
};
|
|
66
|
-
|
|
67
|
-
export { mark_build_failure, start_log_sync, upload_logs };
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { log } from '../../log.js';
|
|
2
|
-
import { cmd } from '../../lib/cmd.js';
|
|
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
|
-
};
|
|
35
|
-
const upload_tag = async (args) => {
|
|
36
|
-
const { api, app, deployment_id } = args;
|
|
37
|
-
log.info(`🔁 Uploading...`);
|
|
38
|
-
const registry = await api.getRegistry(app.id);
|
|
39
|
-
// Registry login
|
|
40
|
-
const { user, password, hostname, image } = registry;
|
|
41
|
-
await cmd(`echo "${password}" | docker login --username "${user}" --password-stdin ${hostname}`);
|
|
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);
|
|
44
|
-
await cmd(`docker tag ${app.id} ${upload_tagname}`);
|
|
45
|
-
// Upload the image to faable registry
|
|
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
|
-
}
|
|
65
|
-
log.info(`✅ Upload completed.`);
|
|
66
|
-
return {
|
|
67
|
-
upload_tagname,
|
|
68
|
-
/** Immutable ref recorded on the deployment: `repo:tag@sha256:…` */
|
|
69
|
-
image_ref,
|
|
70
|
-
};
|
|
71
|
-
};
|
|
72
|
-
|
|
73
|
-
export { build_upload_tagname, pin_tag_to_digest, strip_image_tag, upload_tag };
|