Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/patch-refresh-gh-aw-node.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 15 additions & 4 deletions .github/workflows/publish-safe-outputs-node.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,13 +41,20 @@ jobs:
metadata=$(curl -fsSL "$METADATA_URL")
echo "Metadata fetched successfully"

digest=$(printf '%s' "$metadata" | jq -r '.images[] | select(.os == "linux" and .architecture == "amd64") | .digest' | head -n 1)
digest=$(printf '%s' "$metadata" | jq -r '.digest')
amd64_digest=$(printf '%s' "$metadata" | jq -r '.images[] | select(.os == "linux" and .architecture == "amd64") | .digest' | head -n 1)
arm64_digest=$(printf '%s' "$metadata" | jq -r '.images[] | select(.os == "linux" and .architecture == "arm64") | .digest' | head -n 1)
last_updated=$(printf '%s' "$metadata" | jq -r '.last_updated')

echo "Resolved linux/amd64 digest: ${digest}"
echo "Resolved multi-platform digest: ${digest}"
echo "Resolved linux/amd64 digest: ${amd64_digest}"
echo "Resolved linux/arm64 digest: ${arm64_digest}"
echo "Upstream last_updated: ${last_updated}"

if [ -z "$digest" ] || [ "$digest" = "null" ] || [ -z "$last_updated" ] || [ "$last_updated" = "null" ]; then
if [ -z "$digest" ] || [ "$digest" = "null" ] \
|| [ -z "$amd64_digest" ] || [ "$amd64_digest" = "null" ] \
|| [ -z "$arm64_digest" ] || [ "$arm64_digest" = "null" ] \
|| [ -z "$last_updated" ] || [ "$last_updated" = "null" ]; then
echo "Failed to resolve node:lts-alpine metadata"
exit 1
fi
Expand Down Expand Up @@ -184,6 +191,10 @@ jobs:
echo "should_push=$should_push" >> "$GITHUB_OUTPUT"
echo "reason=$reason" >> "$GITHUB_OUTPUT"

- name: Set up QEMU
if: steps.decision.outputs.should_push == 'true'
uses: docker/setup-qemu-action@96fe6ef7f33517b61c61be40b68a1882f3264fb8 # v4.2.0

- name: Set up Docker Buildx
if: steps.decision.outputs.should_push == 'true'
uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0
Expand All @@ -194,7 +205,7 @@ jobs:
with:
context: .
file: actions/setup/js/Dockerfile.safe-outputs-mcp
platforms: linux/amd64
platforms: linux/amd64,linux/arm64

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 11618d1 — added a pinned docker/setup-qemu-action@v4.2.0 step with the same should_push condition immediately before setup-buildx-action.

pull: true
push: true
build-args: |
Expand Down
9 changes: 8 additions & 1 deletion actions/setup/js/Dockerfile.safe-outputs-mcp
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,21 @@ ARG NODE_IMAGE
ARG NODE_IMAGE_DIGEST=""
ARG NODE_IMAGE_UPDATED_AT=""
ARG DOCKERFILE_HASH=""
ARG NPM_VERSION=11.18.0

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-deterministic image builds per architecture: apk upgrade --no-cache pulls current-at-build-time packages with no version pins, so the amd64 and arm64 layers may differ if built at different times.

💡 Details

On multi-platform builds the two arch layers are built concurrently from the same manifest but each apk upgrade fetches independently. If the Alpine mirror advances between layers, the two architectures will have different effective package versions in the same image tag. This silently defeats the digest-pinning security model the rest of the workflow is built around.

Mitigate by logging installed packages so diffs are visible in CI output:

RUN apk upgrade --no-cache \
    && apk add --no-cache git \
    && apk info -v | sort \
    && npm install --global "npm@${NPM_VERSION}" \
    && npm cache clean --force

Longer term, consider pinning specific Alpine packages or using a custom base image built from a locked apk world.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 65cb3f4 — added apk info -v | sort after the apk upgrade so the full package manifest (with versions) is printed to CI logs for each architecture. Any divergence between amd64 and arm64 layers will be visible in the build output.


Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[/codebase-design] apk upgrade --no-cache runs arbitrary network fetches at build time, which could produce different package sets on each build and undermine reproducibility.

💡 Suggestion

The resulting image digest is pinned downstream, so this is partially mitigated. Still worth documenting the trade-off explicitly:

# Intentional: upgrade all packages to pick up security fixes; downstream digest pins the result.
RUN apk upgrade --no-cache \
    && apk add --no-cache git \
    && npm install --global "npm@${NPM_VERSION}" \
    && npm cache clean --force

Alternatively, consider adding a scheduled rebuild workflow that re-pins the digest on a cadence, making the upgrade step explicit and bounded.

@copilot please address this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 65cb3f4 — added the suggested comment documenting the intentional apk upgrade and added apk info -v | sort to log all installed packages in CI output, making per-architecture differences visible.

RUN apk add --no-cache git
# Intentional: upgrade all packages to pick up security fixes; downstream digest pins the result.
RUN apk upgrade --no-cache \
&& apk add --no-cache git \
&& apk info -v | sort \
&& npm install --global "npm@${NPM_VERSION}" \
&& npm cache clean --force

LABEL org.opencontainers.image.source="https://github.com/github/gh-aw" \
org.opencontainers.image.description="Node LTS Alpine with git for the local safe-outputs MCP runtime" \
org.opencontainers.image.base.name="${NODE_IMAGE}" \
org.opencontainers.image.base.digest="${NODE_IMAGE_DIGEST}" \
io.github.gh-aw.safe-outputs.node.updated-at="${NODE_IMAGE_UPDATED_AT}" \
io.github.gh-aw.safe-outputs.npm.version="${NPM_VERSION}" \
io.github.gh-aw.safe-outputs.dockerfile-hash="${DOCKERFILE_HASH}"

ENTRYPOINT ["sh", "-lc", "node \"${GITHUB_WORKSPACE:-$PWD}/actions/setup/js/safe_outputs_mcp_server.cjs\""]
Loading