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
115 changes: 14 additions & 101 deletions .github/workflows/docker-dependency-updater.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: udx-automation / worker-nodejs dependency upgrade

"on":
schedule:
- cron: "0 5 * * 1"
- cron: "0 5 * * *"
workflow_dispatch:

concurrency:
Expand Down Expand Up @@ -149,9 +149,7 @@ jobs:

outputs:
apt_count: ${{ steps.probe.outputs.apt_count }}
base_image: ${{ steps.probe.outputs.base_image }}
changed: ${{ steps.changes.outputs.changed }}
node_version: ${{ steps.probe.outputs.node_version }}
pr_url: ${{ steps.create-pr.outputs.pull-request-url }}

permissions:
Expand All @@ -169,7 +167,6 @@ jobs:
timeout-minutes: 8
env:
DOCKERFILE: ${{ needs.config.outputs.dockerfile }}
GITHUB_TOKEN: ${{ github.token }}
PROBE_DOCKERFILE: ${{ needs.config.outputs.probe_dockerfile }}
PROBE_IMAGE: ${{ needs.config.outputs.image }}
PROBE_REPORT: ${{ needs.config.outputs.report_path }}
Expand All @@ -195,30 +192,6 @@ jobs:
}
' "${DOCKERFILE}" > "${PROBE_DOCKERFILE}"

current_node_version="$(awk -F= '/^ARG NODE_VERSION=/ { print $2; exit }' "${DOCKERFILE}")"
current_node_major="${current_node_version%%.*}"
test -n "${current_node_major}"

node_version="$(curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL https://nodejs.org/dist/index.json \
| jq -r --arg major "${current_node_major}" \
'[.[] | select(.lts != false) | .version | ltrimstr("v") | select(startswith($major + "."))][0]')"
test -n "${node_version}"

base_image="$(curl --retry 5 --retry-all-errors --retry-delay 2 -fsSL \
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
-H "X-GitHub-Api-Version: 2022-11-28" \
"https://api.github.com/repos/udx/worker/releases/latest" \
| jq -r '.tag_name | ltrimstr("v")')"
test -n "${base_image}"
base_image_ref="usabilitydynamics/udx-worker:${base_image}"

perl -0pi -e \
"s#^FROM usabilitydynamics/udx-worker:.*#FROM ${base_image_ref}#m" \
"${PROBE_DOCKERFILE}"
perl -0pi -e \
"s/^ARG NODE_VERSION=.*/ARG NODE_VERSION=${node_version}/m" \
"${PROBE_DOCKERFILE}"

apt_packages=(xz-utils)

{
Expand All @@ -238,9 +211,8 @@ jobs:

dpkg-query -W -f='${binary:Package}\t${Version}\n' "$@"
EOF
strategy_summary="Use the latest udx-worker release, latest Node.js "
strategy_summary="${strategy_summary}LTS release within the current major, and a no-pin apt "
strategy_summary="${strategy_summary}probe for xz-utils."
base_image="$(awk '$1 == "FROM" { print $2; exit }' "${DOCKERFILE}")"
strategy_summary="Render a temporary Dockerfile with apt package pins removed, build it against the configured base image, then read installed versions with dpkg-query."

non_apt_path="${probe_dir}/non-apt.json"
node <<'NODE' > "${non_apt_path}"
Expand Down Expand Up @@ -275,16 +247,14 @@ jobs:
--arg generated_at "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--arg base_image "${base_image}" \
--arg dockerfile "${DOCKERFILE}" \
--arg node_version "${node_version}" \
--arg probe_dockerfile "${PROBE_DOCKERFILE}" \
--arg strategy_summary "${strategy_summary}" \
--slurpfile non_apt "${non_apt_path}" \
--rawfile apt_versions "${apt_versions_path}" \
'{
generated_at: $generated_at,
method: "worker-nodejs dependency probe",
base_image: "usabilitydynamics/udx-worker:" + $base_image,
node_version: $node_version,
method: "no-pin apt probe",
base_image: $base_image,
strategy: {
summary: $strategy_summary,
dockerfile: $dockerfile,
Expand All @@ -304,8 +274,6 @@ jobs:

apt_count="$(jq '.dependencies.apt | length' "${PROBE_REPORT}")"
echo "apt_count=${apt_count}" >> "${GITHUB_OUTPUT}"
echo "base_image=${base_image}" >> "${GITHUB_OUTPUT}"
echo "node_version=${node_version}" >> "${GITHUB_OUTPUT}"
echo "Upgrade probe: wrote ${PROBE_REPORT}."
- name: Upload dependency report
uses: actions/upload-artifact@v6
Expand All @@ -316,73 +284,21 @@ jobs:
${{ needs.config.outputs.report_path }}
${{ needs.config.outputs.probe_dockerfile }}

- name: Check Copilot availability
id: copilot-availability
timeout-minutes: 1
run: |
set -euo pipefail

if [ -n "${COPILOT_GITHUB_TOKEN:-}" ]; then
echo "available=true" >> "${GITHUB_OUTPUT}"
echo "Copilot CLI review is available."
else
echo "available=false" >> "${GITHUB_OUTPUT}"
echo "Copilot CLI review skipped; COPILOT_GITHUB_TOKEN is not set."
fi
env:
COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GITHUB_TOKEN }}

- name: Update Dockerfile pins
timeout-minutes: 3
run: |
set -euo pipefail

base_image="$(jq -r '.base_image' "${REPORT_PATH}")"
node_version="$(jq -r '.node_version' "${REPORT_PATH}")"
xz_version="$(jq -r \
'.dependencies.apt[] | select(.name == "xz-utils") | .installed' \
"${REPORT_PATH}")"

test -n "${base_image}"
test -n "${node_version}"
test -n "${xz_version}"

perl -0pi -e \
"s#^FROM usabilitydynamics/udx-worker:.*#FROM ${base_image}#m" \
"${DOCKERFILE}"
perl -0pi -e \
"s/^ARG NODE_VERSION=.*/ARG NODE_VERSION=${node_version}/m" \
"${DOCKERFILE}"
perl -0pi -e \
"s/xz-utils=[^[:space:]\\]+/xz-utils=${xz_version}/" \
"${DOCKERFILE}"

echo "Upgrade pins:"
echo "- Base image: ${base_image}"
echo "- Node.js: ${node_version}"
echo "- xz-utils: ${xz_version}"
env:
DOCKERFILE: ${{ needs.config.outputs.dockerfile }}
REPORT_PATH: ${{ needs.config.outputs.report_path }}

- name: Set up Node.js for Copilot CLI
if: steps.copilot-availability.outputs.available == 'true'
uses: actions/setup-node@v6
timeout-minutes: 3
with:
node-version: "22"

- name: Install Copilot CLI
if: steps.copilot-availability.outputs.available == 'true'
timeout-minutes: 3
run: |
npm install -g "@github/copilot@${COPILOT_CLI_VERSION}"
echo "Upgrade setup: installed Copilot CLI ${COPILOT_CLI_VERSION}"
env:
COPILOT_CLI_VERSION: ${{ needs.config.outputs.copilot_cli_version }}

- name: Review Dockerfile with Copilot CLI
if: steps.copilot-availability.outputs.available == 'true'
- name: Update Dockerfile with Copilot CLI
timeout-minutes: 8
run: |
set -euo pipefail
Expand All @@ -395,6 +311,11 @@ jobs:
jq -e '.dependencies.apt and .dependencies.non_apt' \
"${REPORT_PATH}" >/dev/null

if [ -z "${COPILOT_GITHUB_TOKEN:-}" ]; then
echo "COPILOT_GITHUB_TOKEN secret is required for non-apt dependency updates." >&2
exit 1
fi

prompt_path=".tmp/dependency-upgrade/copilot-prompt.md"
mkdir -p "$(dirname "${prompt_path}")"
: > "${prompt_path}"
Expand All @@ -409,8 +330,7 @@ jobs:

copilot_args=(
--prompt "$(cat "${prompt_path}")"
--allow-tool=write
--deny-tool=shell
--allow-all-tools
--allow-all-urls
--no-ask-user
--no-auto-update
Comment thread
fqjony marked this conversation as resolved.
Expand Down Expand Up @@ -475,13 +395,8 @@ jobs:
echo "## Evidence"
echo
echo "- Probe report artifact: \`${REPORT_PATH}\`"
if [ "${COPILOT_AVAILABLE}" = "true" ]; then
echo "- Copilot CLI review: enabled"
else
echo "- Copilot CLI review: skipped; token not configured"
fi
if [ "${COPILOT_AVAILABLE}" = "true" ] &&
[ "${UPLOAD_COPILOT_SESSION}" = "true" ]; then
echo "- Copilot CLI: updated non-apt dependencies"
if [ "${UPLOAD_COPILOT_SESSION}" = "true" ]; then
echo "- Copilot session artifact: \`copilot-docker-dependency-session.md\`"
fi
echo "## Diff"
Expand All @@ -492,7 +407,6 @@ jobs:
} > docker-dependency-pr-body.md
echo "Upgrade PR body: prepared docker-dependency-pr-body.md"
env:
COPILOT_AVAILABLE: ${{ steps.copilot-availability.outputs.available }}
REPORT_PATH: ${{ needs.config.outputs.report_path }}
UPLOAD_COPILOT_SESSION: ${{ needs.config.outputs.upload_copilot_session }}

Expand All @@ -508,7 +422,6 @@ jobs:
- name: Upload Copilot session
if: >-
always() &&
steps.copilot-availability.outputs.available == 'true' &&
needs.config.outputs.upload_copilot_session == 'true'
uses: actions/upload-artifact@v6
timeout-minutes: 3
Expand Down
7 changes: 6 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ FROM usabilitydynamics/udx-worker:0.47.0
# Add metadata labels
LABEL version="0.32.0"

# Set build arguments for Node.js version and application port
# Set build arguments for Node.js, npm, and application port
ARG NODE_VERSION=24.18.0
ARG NPM_VERSION=12.0.1
ARG APP_PORT=8080

# Add Node.js to PATH
Expand Down Expand Up @@ -48,6 +49,10 @@ RUN set -ex && \
# Verify installation and resolution path
node --version && \
command -v npm && head -n 1 "$(command -v npm)" && npm --version && \
npm install --global "npm@${NPM_VERSION}" && \
npm --version && \
npm cache clean --force && \
rm -rf /root/.npm && \
rm -rf /tmp/*

# Remove xz-utils as it's no longer needed
Expand Down
1 change: 1 addition & 0 deletions ci/prompts/docker-dependency-nonapt.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
Non-apt dependency rules:
- Use `dependencies.non_apt.pins` and `dependencies.non_apt.urls` from the dependency report as the starting inventory of Dockerfile-owned non-apt candidates.
- For `NODE_VERSION`, check the official Node.js release index and keep the latest stable LTS release from the same active LTS line unless the Dockerfile or repo docs explicitly require a different major version.
- For `NPM_VERSION`, use the latest npm release only when its Node.js engine range supports the Dockerfile `NODE_VERSION`; retain the current npm version when compatibility is not explicit.
- Preserve the UDX Worker base image tag already selected by the deterministic updater unless clear upstream evidence shows a newer stable `udx/worker` release.
- Include the upstream source URL for every non-apt update in the changelog.
- Leave pins unchanged when the upstream source cannot be identified, cannot be checked, is ambiguous, or does not clearly show a newer stable release/version.
Expand Down
29 changes: 27 additions & 2 deletions src/tests/10_validate_environment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,30 @@ if ! command -v worker >/dev/null 2>&1; then
exit 3
fi

echo "Node.js version is correct and Worker PM is available."
exit 0
# Verify that npm's bundled tar includes the security fix. Resolve npm's global
# installation dynamically so the test follows the installed Node.js layout.
if ! TAR_VERSION=$(node -e '
const { execFileSync } = require("node:child_process");
const path = require("node:path");
const npmRoot = execFileSync("npm", ["root", "--global"], { encoding: "utf8" }).trim();
process.stdout.write(require(path.join(npmRoot, "npm/node_modules/tar/package.json")).version);
'); then
echo "Error: unable to determine npm bundled tar version."
exit 4
fi

if ! node -e '
const match = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/.exec(process.argv[1]);
if (!match) process.exit(1);
const version = match.slice(1, 4).map(Number);
const minimum = [7, 5, 19];
const comparison = version.findIndex((part, index) => part !== minimum[index]);
const hasPrerelease = process.argv[1].includes("-");
process.exit(comparison === -1 ? (hasPrerelease ? 1 : 0) : (version[comparison] > minimum[comparison] ? 0 : 1));
' "$TAR_VERSION"; then
echo "Error: npm bundled tar must be at least 7.5.19. Current: $TAR_VERSION"
exit 4
fi

echo "Node.js version is correct, npm bundled tar is patched, and Worker PM is available."
exit 0
Loading