mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:10:44 +00:00
fix(docker): replace curl|bash Bun install with pinned multi-stage COPY (#74359)
Merged via squash.
Prepared head SHA: 3b4a889467
Co-authored-by: fede-kamel <209537060+fede-kamel@users.noreply.github.com>
Co-authored-by: sallyom <11166065+sallyom@users.noreply.github.com>
Reviewed-by: @sallyom
This commit is contained in:
committed by
GitHub
parent
2b37b383ed
commit
10ebcbdb99
@@ -16,6 +16,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/sessions: keep `sessions.list` polling responsive on large session stores by reusing list-safe session cache/indexes and returning a lightweight compaction checkpoint preview instead of heavyweight summaries. Thanks @rolandrscheel.
|
||||
- CLI/update: treat inherited Gateway service markers as origin hints and only block package replacement when the managed Gateway is still live, so self-updates can stop the service and continue safely. (#75729) Thanks @hxy91819.
|
||||
- Agents/failover: exempt run-level timeouts that fire during tool execution from model fallback, timeout-triggered compaction, and generic timeout payload synthesis. Long `process(poll)`, browser, or `exec` tool calls that exceed `agents.defaults.timeoutSeconds` previously rotated auth profiles, switched to a fallback model, and surfaced a misleading "LLM request timed out" error even though the primary model had already responded. Mirrors the existing `timedOutDuringCompaction` precedent (#46889). Fixes #52147. (#75873) Thanks @simonusa.
|
||||
- Docker: copy Bun 1.3.13 from a digest-pinned image and keep CI on the same version. Fixes #74356. Thanks @fede-kamel and @sallyom.
|
||||
|
||||
## 2026.5.2
|
||||
|
||||
|
||||
19
Dockerfile
19
Dockerfile
@@ -15,6 +15,9 @@ ARG OPENCLAW_BUNDLED_PLUGIN_DIR=extensions
|
||||
ARG OPENCLAW_NODE_BOOKWORM_IMAGE="node:24-bookworm@sha256:3a09aa6354567619221ef6c45a5051b671f953f0a1924d1f819ffb236e520e6b"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_IMAGE="node:24-bookworm-slim@sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
|
||||
ARG OPENCLAW_NODE_BOOKWORM_SLIM_DIGEST="sha256:e8e2e91b1378f83c5b2dd15f0247f34110e2fe895f6ca7719dbb780f929368eb"
|
||||
# Keep in sync with .github/actions/setup-node-env/action.yml bun-version.
|
||||
# To update: docker buildx imagetools inspect oven/bun:<version> and use the manifest-list digest.
|
||||
ARG OPENCLAW_BUN_IMAGE="oven/bun:1.3.13@sha256:87416c977a612a204eb54ab9f3927023c2a3c971f4f345a01da08ea6262ae30e"
|
||||
|
||||
# Base images are pinned to SHA256 digests for reproducible builds.
|
||||
# Dependabot refreshes these blessed digests; release builds consume the
|
||||
@@ -37,22 +40,12 @@ RUN --mount=type=bind,source=${OPENCLAW_BUNDLED_PLUGIN_DIR},target=/tmp/${OPENCL
|
||||
done
|
||||
|
||||
# ── Stage 2: Build ──────────────────────────────────────────────
|
||||
FROM ${OPENCLAW_BUN_IMAGE} AS bun-binary
|
||||
FROM ${OPENCLAW_NODE_BOOKWORM_IMAGE} AS build
|
||||
ARG OPENCLAW_BUNDLED_PLUGIN_DIR
|
||||
|
||||
# Install Bun (required for build scripts). Retry the whole bootstrap flow to
|
||||
# tolerate transient 5xx failures from bun.sh/GitHub during CI image builds.
|
||||
RUN set -eux; \
|
||||
for attempt in 1 2 3 4 5; do \
|
||||
if curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL https://bun.sh/install | bash; then \
|
||||
break; \
|
||||
fi; \
|
||||
if [ "$attempt" -eq 5 ]; then \
|
||||
exit 1; \
|
||||
fi; \
|
||||
sleep $((attempt * 2)); \
|
||||
done
|
||||
ENV PATH="/root/.bun/bin:${PATH}"
|
||||
# Copy pinned Bun binary from the official image instead of fetching via curl.
|
||||
COPY --from=bun-binary /usr/local/bin/bun /usr/local/bin/bun
|
||||
|
||||
RUN corepack enable
|
||||
|
||||
|
||||
@@ -33,43 +33,67 @@ type DependabotConfig = {
|
||||
updates?: DependabotUpdate[];
|
||||
};
|
||||
|
||||
function resolveFirstFromReference(dockerfile: string): string | undefined {
|
||||
function resolveArgDefaults(dockerfile: string): Map<string, string> {
|
||||
const argDefaults = new Map<string, string>();
|
||||
|
||||
for (const line of dockerfile.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) {
|
||||
continue;
|
||||
}
|
||||
if (trimmed.startsWith("FROM ")) {
|
||||
break;
|
||||
}
|
||||
const argMatch = trimmed.match(/^ARG\s+([A-Z0-9_]+)=(.+)$/);
|
||||
if (!argMatch) {
|
||||
continue;
|
||||
}
|
||||
const [, name, rawValue] = argMatch;
|
||||
const value = rawValue.replace(/^["']|["']$/g, "");
|
||||
argDefaults.set(name, value);
|
||||
}
|
||||
|
||||
const fromLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("FROM "));
|
||||
if (!fromLine) {
|
||||
return undefined;
|
||||
argDefaults.set(name, rawValue.replace(/^["']|["']$/g, ""));
|
||||
}
|
||||
return argDefaults;
|
||||
}
|
||||
|
||||
function resolveFromImageRef(fromLine: string, argDefaults: Map<string, string>): string {
|
||||
const fromMatch = fromLine.trim().match(/^FROM\s+(\S+?)(?:\s+AS\s+\S+)?$/);
|
||||
if (!fromMatch) {
|
||||
return undefined;
|
||||
return fromLine;
|
||||
}
|
||||
const imageRef = fromMatch[1];
|
||||
const argName =
|
||||
imageRef.match(/^\$\{([A-Z0-9_]+)\}$/)?.[1] ?? imageRef.match(/^\$([A-Z0-9_]+)$/)?.[1];
|
||||
|
||||
if (!argName) {
|
||||
return imageRef;
|
||||
}
|
||||
return argDefaults.get(argName);
|
||||
return argDefaults.get(argName) ?? imageRef;
|
||||
}
|
||||
|
||||
function resolveAllArgBackedFromReferences(
|
||||
dockerfile: string,
|
||||
): { stage: string; imageRef: string }[] {
|
||||
const argDefaults = resolveArgDefaults(dockerfile);
|
||||
const results: { stage: string; imageRef: string }[] = [];
|
||||
let stageIndex = 0;
|
||||
for (const line of dockerfile.split(/\r?\n/)) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed.startsWith("FROM ")) {
|
||||
continue;
|
||||
}
|
||||
const imageRef = resolveFromImageRef(trimmed, argDefaults);
|
||||
// Only check FROM lines that use an ARG — literal `FROM scratch` etc. are intentionally unpinned.
|
||||
const usesArg =
|
||||
trimmed.match(/FROM\s+\$\{[A-Z0-9_]+\}/) !== null ||
|
||||
trimmed.match(/FROM\s+\$[A-Z0-9_]+/) !== null;
|
||||
if (usesArg) {
|
||||
const stageMatch = trimmed.match(/AS\s+(\S+)/i);
|
||||
const stageName = stageMatch ? stageMatch[1] : `stage-${stageIndex}`;
|
||||
results.push({ stage: stageName, imageRef });
|
||||
}
|
||||
stageIndex += 1;
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
function resolveFirstFromReference(dockerfile: string): string | undefined {
|
||||
const argDefaults = resolveArgDefaults(dockerfile);
|
||||
const fromLine = dockerfile.split(/\r?\n/).find((line) => line.trimStart().startsWith("FROM "));
|
||||
if (!fromLine) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveFromImageRef(fromLine, argDefaults);
|
||||
}
|
||||
|
||||
describe("docker base image pinning", () => {
|
||||
@@ -84,6 +108,18 @@ describe("docker base image pinning", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("pins all ARG-backed FROM stages in selected Dockerfiles to sha256 digests", async () => {
|
||||
for (const dockerfilePath of DIGEST_PINNED_DOCKERFILES) {
|
||||
const dockerfile = await readFile(resolve(repoRoot, dockerfilePath), "utf8");
|
||||
const stages = resolveAllArgBackedFromReferences(dockerfile);
|
||||
for (const { stage, imageRef } of stages) {
|
||||
expect(imageRef, `${dockerfilePath} stage "${stage}" must be digest-pinned`).toMatch(
|
||||
/^\S+@sha256:[a-f0-9]{64}$/,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps Dependabot Docker updates enabled for root Dockerfiles", async () => {
|
||||
const raw = await readFile(resolve(repoRoot, ".github/dependabot.yml"), "utf8");
|
||||
const config = parse(raw) as DependabotConfig;
|
||||
|
||||
Reference in New Issue
Block a user