diff --git a/.github/workflows/docker-dependency-updater.yml b/.github/workflows/docker-dependency-updater.yml index a8a19c5..bc61f2d 100644 --- a/.github/workflows/docker-dependency-updater.yml +++ b/.github/workflows/docker-dependency-updater.yml @@ -3,7 +3,7 @@ name: udx-automation / worker-nodejs dependency upgrade "on": schedule: - - cron: "0 5 * * 1" + - cron: "0 5 * * *" workflow_dispatch: concurrency: @@ -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: @@ -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 }} @@ -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) { @@ -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}" @@ -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, @@ -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 @@ -316,64 +284,13 @@ 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}" @@ -381,8 +298,7 @@ jobs: 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 @@ -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}" @@ -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 @@ -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" @@ -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 }} @@ -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 diff --git a/Dockerfile b/Dockerfile index c30e3f2..1204fe6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -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 diff --git a/ci/prompts/docker-dependency-nonapt.md b/ci/prompts/docker-dependency-nonapt.md index f7c355c..f34576e 100644 --- a/ci/prompts/docker-dependency-nonapt.md +++ b/ci/prompts/docker-dependency-nonapt.md @@ -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. diff --git a/src/tests/10_validate_environment.sh b/src/tests/10_validate_environment.sh index e2f462e..4792c7b 100644 --- a/src/tests/10_validate_environment.sh +++ b/src/tests/10_validate_environment.sh @@ -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 \ No newline at end of file +# 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