diff --git a/6/alpine3.23/Dockerfile b/6/alpine3.23/Dockerfile index e4e90805..768923db 100644 --- a/6/alpine3.23/Dockerfile +++ b/6/alpine3.23/Dockerfile @@ -12,7 +12,7 @@ RUN apk add --no-cache \ # grab gosu for easy step-down from root # https://github.com/tianon/gosu/releases -ENV GOSU_VERSION 1.19 +ENV GOSU_VERSION=1.19 RUN set -eux; \ \ apk add --no-cache --virtual .gosu-deps \ @@ -40,18 +40,53 @@ RUN set -eux; \ gosu --version; \ gosu nobody true -ENV NODE_ENV production +ENV NODE_ENV=production -ENV GHOST_CLI_VERSION 1.30.1 +# Ghost-CLI is installed from source rather than "npm install -g ghost-cli" so that +# pnpm can resolve the dependency tree from the lockfile committed alongside the tag +# ("npm install -g" ignores lockfiles entirely, so the tree it produces varies by build date) +ENV GHOST_CLI_VERSION=1.30.1 +ENV GHOST_CLI_SHA=6c711179d9669c1938aa506f92a82c602d6c202e +ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli RUN set -eux; \ + \ + apk add --no-cache --virtual .ghost-cli-deps git; \ + \ corepack enable; \ - npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \ - npm cache clean --force + \ +# corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location +# from these, so pointing them at /tmp keeps every throwaway byte in one place + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + mkdir -p "$GHOST_CLI_INSTALL"; \ + cd "$GHOST_CLI_INSTALL"; \ + git init --quiet .; \ + git remote add origin https://github.com/TryGhost/Ghost-CLI.git; \ +# fetching the commit by hash means git's own object verification pins the source +# (a moved tag or a re-rolled release tarball cannot change what we get) + git fetch --quiet --depth 1 origin "$GHOST_CLI_SHA"; \ + git checkout --quiet FETCH_HEAD; \ + [ "$(node -p 'require("./package.json").version')" = "$GHOST_CLI_VERSION" ]; \ + \ +# "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails + pnpm install --prod --frozen-lockfile; \ + \ + ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \ + \ +# drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json) + rm -rf .git test .github; \ + \ + apk del --no-network .ghost-cli-deps; \ + \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ + \ + ghost --version -ENV GHOST_INSTALL /var/lib/ghost -ENV GHOST_CONTENT /var/lib/ghost/content +ENV GHOST_INSTALL=/var/lib/ghost +ENV GHOST_CONTENT=/var/lib/ghost/content -ENV GHOST_VERSION 6.57.1 +ENV GHOST_VERSION=6.57.1 RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ @@ -81,6 +116,9 @@ RUN set -eux; \ gosu node pnpm store prune; \ gosu node npm cache clean --force; \ npm cache clean --force; \ +# none of these are needed to run Ghost: corepack re-downloads pnpm on demand and node-gyp +# only matters while compiling native modules, which is finished by this point + rm -rf /home/node/.cache/node/corepack /home/node/.cache/node-gyp /home/node/.cache/pnpm; \ \ # test that the optional dependencies are installed and loadable cd current; \ diff --git a/6/bookworm/Dockerfile b/6/bookworm/Dockerfile index de310b37..4ce4181a 100644 --- a/6/bookworm/Dockerfile +++ b/6/bookworm/Dockerfile @@ -8,7 +8,7 @@ FROM node:22-bookworm-slim # grab gosu for easy step-down from root # https://github.com/tianon/gosu/releases -ENV GOSU_VERSION 1.19 +ENV GOSU_VERSION=1.19 RUN set -eux; \ # save list of currently installed packages for later so we can clean up savedAptMark="$(apt-mark showmanual)"; \ @@ -37,18 +37,58 @@ RUN set -eux; \ gosu --version; \ gosu nobody true -ENV NODE_ENV production +ENV NODE_ENV=production -ENV GHOST_CLI_VERSION 1.30.1 +# Ghost-CLI is installed from source rather than "npm install -g ghost-cli" so that +# pnpm can resolve the dependency tree from the lockfile committed alongside the tag +# ("npm install -g" ignores lockfiles entirely, so the tree it produces varies by build date) +ENV GHOST_CLI_VERSION=1.30.1 +ENV GHOST_CLI_SHA=6c711179d9669c1938aa506f92a82c602d6c202e +ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli RUN set -eux; \ + \ + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ + \ corepack enable; \ - npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \ - npm cache clean --force + \ +# corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location +# from these, so pointing them at /tmp keeps every throwaway byte in one place + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + mkdir -p "$GHOST_CLI_INSTALL"; \ + cd "$GHOST_CLI_INSTALL"; \ + git init --quiet .; \ + git remote add origin https://github.com/TryGhost/Ghost-CLI.git; \ +# fetching the commit by hash means git's own object verification pins the source +# (a moved tag or a re-rolled release tarball cannot change what we get) + git fetch --quiet --depth 1 origin "$GHOST_CLI_SHA"; \ + git checkout --quiet FETCH_HEAD; \ + [ "$(node -p 'require("./package.json").version')" = "$GHOST_CLI_VERSION" ]; \ + \ +# "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails + pnpm install --prod --frozen-lockfile; \ + \ + ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \ + \ +# drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json) + rm -rf .git test .github; \ + \ + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + rm -rf /var/lib/apt/lists/*; \ + \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ + \ + ghost --version -ENV GHOST_INSTALL /var/lib/ghost -ENV GHOST_CONTENT /var/lib/ghost/content +ENV GHOST_INSTALL=/var/lib/ghost +ENV GHOST_CONTENT=/var/lib/ghost/content -ENV GHOST_VERSION 6.57.1 +ENV GHOST_VERSION=6.57.1 RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ @@ -83,6 +123,9 @@ RUN set -eux; \ gosu node pnpm store prune; \ gosu node npm cache clean --force; \ npm cache clean --force; \ +# none of these are needed to run Ghost: corepack re-downloads pnpm on demand and node-gyp +# only matters while compiling native modules, which is finished by this point + rm -rf /home/node/.cache/node/corepack /home/node/.cache/node-gyp /home/node/.cache/pnpm; \ \ # test that the optional dependencies are installed and loadable cd current; \ diff --git a/Dockerfile.template b/Dockerfile.template index 149e82bf..f717c17a 100644 --- a/Dockerfile.template +++ b/Dockerfile.template @@ -18,7 +18,7 @@ RUN apk add --no-cache \ {{ ) else "" end -}} # grab gosu for easy step-down from root # https://github.com/tianon/gosu/releases -ENV GOSU_VERSION 1.19 +ENV GOSU_VERSION=1.19 RUN set -eux; \ {{ if is_alpine then ( -}} \ @@ -60,18 +60,66 @@ RUN set -eux; \ gosu --version; \ gosu nobody true -ENV NODE_ENV production +ENV NODE_ENV=production -ENV GHOST_CLI_VERSION {{ .cli.version }} +# Ghost-CLI is installed from source rather than "npm install -g ghost-cli" so that +# pnpm can resolve the dependency tree from the lockfile committed alongside the tag +# ("npm install -g" ignores lockfiles entirely, so the tree it produces varies by build date) +ENV GHOST_CLI_VERSION={{ .cli.version }} +ENV GHOST_CLI_SHA={{ .cli.sha }} +ENV GHOST_CLI_INSTALL=/usr/local/lib/ghost-cli RUN set -eux; \ + \ +{{ if is_alpine then ( -}} + apk add --no-cache --virtual .ghost-cli-deps git; \ +{{ ) else ( -}} + savedAptMark="$(apt-mark showmanual)"; \ + apt-get update; \ + apt-get install -y --no-install-recommends ca-certificates git; \ +{{ ) end -}} + \ corepack enable; \ - npm install -g "ghost-cli@$GHOST_CLI_VERSION"; \ - npm cache clean --force + \ +# corepack's downloads, pnpm's metadata cache and pnpm's store all derive their location +# from these, so pointing them at /tmp keeps every throwaway byte in one place + export XDG_CACHE_HOME=/tmp/xdg-cache XDG_DATA_HOME=/tmp/xdg-data; \ + \ + mkdir -p "$GHOST_CLI_INSTALL"; \ + cd "$GHOST_CLI_INSTALL"; \ + git init --quiet .; \ + git remote add origin https://github.com/TryGhost/Ghost-CLI.git; \ +# fetching the commit by hash means git's own object verification pins the source +# (a moved tag or a re-rolled release tarball cannot change what we get) + git fetch --quiet --depth 1 origin "$GHOST_CLI_SHA"; \ + git checkout --quiet FETCH_HEAD; \ + [ "$(node -p 'require("./package.json").version')" = "$GHOST_CLI_VERSION" ]; \ + \ +# "--frozen-lockfile" is the point of all this: it installs pnpm-lock.yaml exactly, or fails + pnpm install --prod --frozen-lockfile; \ + \ + ln -s "$GHOST_CLI_INSTALL/bin/ghost" /usr/local/bin/ghost; \ + \ +# drop what "npm publish" would not have shipped (see "files" in Ghost-CLI's package.json) + rm -rf .git test .github; \ + \ +{{ if is_alpine then ( -}} + apk del --no-network .ghost-cli-deps; \ +{{ ) else ( -}} + apt-mark auto '.*' > /dev/null; \ + [ -z "$savedAptMark" ] || apt-mark manual $savedAptMark > /dev/null; \ + apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \ + {{ clean_apt }}; \ +{{ ) end -}} + \ + rm -rf /tmp/xdg-cache /tmp/xdg-data; \ + npm cache clean --force; \ + \ + ghost --version -ENV GHOST_INSTALL /var/lib/ghost -ENV GHOST_CONTENT /var/lib/ghost/content +ENV GHOST_INSTALL=/var/lib/ghost +ENV GHOST_CONTENT=/var/lib/ghost/content -ENV GHOST_VERSION {{ .version }} +ENV GHOST_VERSION={{ .version }} RUN set -eux; \ mkdir -p "$GHOST_INSTALL"; \ @@ -114,6 +162,9 @@ RUN set -eux; \ gosu node pnpm store prune; \ gosu node npm cache clean --force; \ npm cache clean --force; \ +# none of these are needed to run Ghost: corepack re-downloads pnpm on demand and node-gyp +# only matters while compiling native modules, which is finished by this point + rm -rf /home/node/.cache/node/corepack /home/node/.cache/node-gyp /home/node/.cache/pnpm; \ \ # test that the optional dependencies are installed and loadable cd current; \ diff --git a/versions.json b/versions.json index 5b2b4d8b..33a76d87 100644 --- a/versions.json +++ b/versions.json @@ -2,7 +2,8 @@ "6": { "version": "6.57.1", "cli": { - "version": "1.30.1" + "version": "1.30.1", + "sha": "6c711179d9669c1938aa506f92a82c602d6c202e" }, "node": { "version": "22" diff --git a/versions.sh b/versions.sh index 94f0a59a..2641485e 100755 --- a/versions.sh +++ b/versions.sh @@ -18,14 +18,30 @@ allVersions="$( | sort -ruV )" +cliTags="$(git ls-remote --tags https://github.com/TryGhost/Ghost-CLI.git)" + cliVersion="$( - git ls-remote --tags https://github.com/TryGhost/Ghost-CLI.git \ + echo "$cliTags" \ | sed -rne 's!^.*\trefs/tags/v?|\^\{\}$!!g; /^[0-9][.][0-9]+/p' \ | grep -vE -- '-(alpha|beta|rc)' \ | sort -ruV \ | head -n1 )" +# the Dockerfile clones Ghost-CLI by commit hash so that git's own object verification pins +# the source; prefer the peeled hash since these are annotated tags +cliSha="$( + awk -v tag="refs/tags/v$cliVersion" ' + $2 == tag "^{}" { peeled = $1 } + $2 == tag { direct = $1 } + END { print (peeled != "" ? peeled : direct) } + ' <<<"$cliTags" +)" +if [ -z "$cliSha" ]; then + echo >&2 "error: cannot determine commit for Ghost-CLI 'v$cliVersion'" + exit 1 +fi + for version in "${versions[@]}"; do rcVersion="${version%-rc}" rcGrepV='-v' @@ -83,7 +99,7 @@ for version in "${versions[@]}"; do ' )" - export fullVersion cliVersion + export fullVersion cliVersion cliSha json="$(jq <<<"$json" --compact-output --argjson doc "$doc" ' { # https://docs.ghost.org/faq/node-versions @@ -92,7 +108,7 @@ for version in "${versions[@]}"; do }[env.version] as $nodeVersion | .[env.version] = { version: env.fullVersion, - cli: { version: env.cliVersion }, + cli: { version: env.cliVersion, sha: env.cliSha }, node: { version: $nodeVersion }, variants: ( $doc