From a966e8bbaefd8c7a2d9ff207286cbdf0e4fae3b9 Mon Sep 17 00:00:00 2001 From: Ethan Arrowood Date: Fri, 30 Oct 2026 17:43:23 -0600 Subject: [PATCH] Add building by specific commit SHA The core change is the Dockerfile and clone.sh now respect a `NODE_REF` value for building by the specified commit SHA When not specified, it defaults to the head commit of the main branch of `nodejs/node`. But the main intent for this feature is so that the `nodejs/node` repo can use `repository_dispatch` to automatically trigger new builds (with a specific commit hash) after successfully executing its own daily build workflow. Signed-off-by: Ethan Arrowood --- .../docker-build-and-publish-linux-amd64.yml | 30 ----- .../docker-build-and-publish-linux-arm64.yml | 30 ----- .github/workflows/docker-build.yml | 117 +++++++++++++++--- .github/workflows/docker-merge.yml | 16 ++- Dockerfile | 2 + README.md | 8 ++ scripts/clone.sh | 20 ++- 7 files changed, 136 insertions(+), 87 deletions(-) delete mode 100644 .github/workflows/docker-build-and-publish-linux-amd64.yml delete mode 100644 .github/workflows/docker-build-and-publish-linux-arm64.yml diff --git a/.github/workflows/docker-build-and-publish-linux-amd64.yml b/.github/workflows/docker-build-and-publish-linux-amd64.yml deleted file mode 100644 index ec93f3e..0000000 --- a/.github/workflows/docker-build-and-publish-linux-amd64.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Build and Publish Linux AMD64 Image - -on: - workflow_dispatch: # allows us to trigger runs manually in the GitHub UI - workflow_call: # allows this workflow to be called by another workflow - -jobs: - docker: - runs-on: ubuntu-24.04 - steps: # See https://docs.docker.com/build/ci/github-actions/multi-platform/ - - name: Login to DockerHub - uses: docker/login-action@v3 - with: # these are defined in the GitHub Secrets UI - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/amd64 - push: true - tags: ${{ vars.DOCKERHUB_IMAGE }}:nightly-linux-amd64 - - merge: - needs: [docker] - uses: ./.github/workflows/docker-merge.yml - secrets: inherit diff --git a/.github/workflows/docker-build-and-publish-linux-arm64.yml b/.github/workflows/docker-build-and-publish-linux-arm64.yml deleted file mode 100644 index 7f6d900..0000000 --- a/.github/workflows/docker-build-and-publish-linux-arm64.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Build and Publish Linux ARM64 Image - -on: - workflow_dispatch: # allows us to trigger runs manually in the GitHub UI - workflow_call: # allows this workflow to be called by another workflow - -jobs: - docker: - runs-on: ubuntu-24.04-arm - steps: # See https://docs.docker.com/build/ci/github-actions/multi-platform/ - - name: Login to DockerHub - uses: docker/login-action@v3 - with: # these are defined in the GitHub Secrets UI - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build and push - uses: docker/build-push-action@v6 - with: - platforms: linux/arm64 - push: true - tags: ${{ vars.DOCKERHUB_IMAGE }}:nightly-linux-arm64 - - merge: - needs: [docker] - uses: ./.github/workflows/docker-merge.yml - secrets: inherit diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml index e0c9b7f..dc6cf74 100644 --- a/.github/workflows/docker-build.yml +++ b/.github/workflows/docker-build.yml @@ -2,24 +2,109 @@ name: Nightly Build Orchestrator on: workflow_dispatch: # allows us to trigger runs manually in the GitHub UI - repository_dispatch: # allows us to trigger runs from an external repository + inputs: + node_ref: + description: 'nodejs/node commit SHA to build (omit for head of main)' + type: string + required: false + default: '' + platform: + description: 'Platform to build (the other keeps its current image)' + type: choice + options: + - all + - linux/amd64 + - linux/arm64 + default: all + repository_dispatch: # sent by nodejs/node's daily workflow once it has built main + types: [node-daily] schedule: - - cron: '0 0 * * *' # runs nightly - -# When any of the build workflows complete successfully on the nightly branch, -# the merge workflow will be triggered by those workflows directly to update -# the build for that platform. These are run independently and concurrently, -# if any of them fail, at worst the image for that specific platform won't be -# updated, but at least one usable image for a platform is always available, -# which is better than dragging the updates for all platforms to wait -# for one platform to be fixed. + # Fallback in case the dispatches from nodejs/node stop arriving. + - cron: '0 6 * * 0' # runs weekly + +# nodejs/node's daily workflow dispatches a `node-daily` event with the commit it builds successfully. +# +# Each platform builds in its own job on a native runner and pushes its own nightly-linux- tag. +# Then, the merge workflow combines the platform tags into `nightly`. +# +# If any platform fails, the others still push their own nightly-linux- tags, but `nightly` is not updated. +# The failed run flags the broken platform in an issue. jobs: - call_build_arm64: + docker: if: ${{ github.ref == format('refs/heads/{0}', vars.NIGHTLY_BRANCH) }} - uses: ./.github/workflows/docker-build-and-publish-linux-arm64.yml - secrets: inherit + strategy: + fail-fast: false + matrix: + arch: ${{ fromJSON(inputs.platform == 'linux/amd64' && '["amd64"]' || inputs.platform == 'linux/arm64' && '["arm64"]' || '["amd64", "arm64"]') }} + runs-on: ${{ matrix.arch == 'arm64' && 'ubuntu-24.04-arm' || 'ubuntu-24.04' }} + steps: # See https://docs.docker.com/build/ci/github-actions/multi-platform/ + - name: Login to DockerHub + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 + with: # these are defined in the GitHub Secrets UI + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} - call_build_amd64: - if: ${{ github.ref == format('refs/heads/{0}', vars.NIGHTLY_BRANCH) }} - uses: ./.github/workflows/docker-build-and-publish-linux-amd64.yml + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 + + - name: Build and push + uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0 + with: + platforms: linux/${{ matrix.arch }} + push: true + build-args: NODE_REF=${{ github.event.client_payload.sha || inputs.node_ref }} + tags: ${{ vars.DOCKERHUB_IMAGE }}:nightly-linux-${{ matrix.arch }} + + # Only runs when every platform in this run built successfully (the default for `needs`). + merge: + needs: [docker] + uses: ./.github/workflows/docker-merge.yml secrets: inherit + + # GitHub only notifies whoever triggered a run (for repository_dispatches it is the bot account) + # So when an automatic run fails, open a tracking issue (or comment on the open one), and close it once a run succeeds. + # Manual runs are skipped: GitHub already notifies whoever started them. + report: + needs: [docker, merge] + if: ${{ !cancelled() && github.event_name != 'workflow_dispatch' && needs.docker.result != 'skipped' }} + runs-on: ubuntu-latest + permissions: + actions: read # to list the failed jobs + issues: write + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + FAILED: ${{ contains(needs.*.result, 'failure') }} + NODE_REF: ${{ github.event.client_payload.sha || 'head of main' }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + steps: + - name: Open, update, or close the tracking issue + run: | + issue=$(gh issue list --label "nightly-build-failure" --state open --json number --jq '.[0].number // empty') + + if [ "$FAILED" = true ]; then + failed_jobs=$(gh run view "$GITHUB_RUN_ID" --json jobs --jq '.jobs[] | select(.conclusion == "failure") | "- " + .name') + details=$(printf 'Run: %s\nTrigger: %s\nnodejs/node commit: %s\n\nFailed jobs:\n%s' "$RUN_URL" "$GITHUB_EVENT_NAME" "$NODE_REF" "$failed_jobs") + if [ -n "$issue" ]; then + gh issue comment "$issue" --body "$(printf 'Still failing.\n\n%s' "$details")" + else + gh issue create --label "nightly-build-failure" --title 'Nightly image build is failing' \ + --body "$(printf '@nodejs/devcontainer the nightly tag was not updated because a build failed.\n\n%s' "$details")" + fi + elif [ -n "$issue" ]; then + gh issue close "$issue" --comment "Fixed: $RUN_URL built successfully and tagged nightly." + fi + + # GitHub disables scheduled workflows in public repositories after 60 days without repository activity. + # This repo often goes a long time without reasonable updates, yet we want the images to continue building. + # Re-enabling the workflow on every automatic run resets that timer, so the weekly fallback keeps running even when the repo is quiet. + keepalive: + if: ${{ github.event_name != 'workflow_dispatch' }} + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Keep the scheduled trigger enabled + run: gh api -X PUT "repos/${GITHUB_REPOSITORY}/actions/workflows/docker-build.yml/enable" + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/docker-merge.yml b/.github/workflows/docker-merge.yml index 96cc1c9..d9d5efb 100644 --- a/.github/workflows/docker-merge.yml +++ b/.github/workflows/docker-merge.yml @@ -1,8 +1,8 @@ name: Merge images on: - workflow_dispatch: # allow explicit dispatches from arch-specific builds - workflow_call: # allow this workflow to be called by another workflow + workflow_dispatch: # allow re-merging the platform tags by hand + workflow_call: # allow this workflow to be called by another workflow env: IMAGE: ${{ vars.DOCKERHUB_IMAGE }} @@ -17,21 +17,19 @@ jobs: runs-on: ubuntu-latest steps: - name: Login to DockerHub - uses: docker/login-action@v3 - with: # these are defined in the GitHub Secrets UI + uses: docker/login-action@81c6f93d4bc991e510b342a00b419b3bb4efc5c6 # v4.7.0 + with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1 # Merge the multiple architecture-specific images into a single multi-arch image. # If images for other architectures do not exist in the registry yet, this step will fail. # It will only succeed when at least one image for each architecture has been built and pushed. - # If images for other architectures are being built and the ones in the registry - # are old, it only updates the nightly tag for the architecture that triggered this - # workflow. When images for other architectures are built successfully later, they will - # trigger this workflow again and merge the updated images. + # When a manual run rebuilt only one platform, the other platform's existing image is + # merged in unchanged. - name: Create multi-arch manifest run: | docker buildx imagetools create \ diff --git a/Dockerfile b/Dockerfile index b577351..9cfad4c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,8 @@ ENV DEBIAN_FRONTEND=1 COPY --chown=root:developer --chmod=0755 ./scripts/ /home/developer/scripts/ USER developer +# The nodejs/node commit to build; empty builds the head of main. +ARG NODE_REF= RUN /home/developer/scripts/clone.sh # Installing Nix using the same script test-shared workflow uses upstream. diff --git a/README.md b/README.md index 993247c..740516c 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,15 @@ Some useful commands: - `docker image ls` - list the images and IDs - `docker run -it ` - run a container and shell into it - `docker tag devcontainer:nightly` - run to tag an image as `nightly` +- `docker build --build-arg NODE_REF= .` - build the image from a specific nodejs/node commit +### How the `nightly` images are published + +nodejs/node's [daily workflow](https://github.com/nodejs/node/blob/main/.github/workflows/daily.yml) builds `main` and, when that succeeds, sends a `node-daily` `repository_dispatch` event to this repository with the commit SHA it built successfully. + +The [build workflow](./.github/workflows/docker-build.yml) then builds and publishes the images from that commit across both platforms (`arm64` and `amd64`) and then tags them both as `nightly`. If either platform fails, the other is still published as `nightly-linux-`, but `nightly` stays on the previous build until both succeed. A failed automatic run opens an issue labeled `nightly-build-failure` (or comments on the open one), and the next successful run closes it. + +In case the `repository_dispatch` events stop arriving, a weekly scheduled run builds the head of `main` as a fallback. Builds can also be manually triggered using `workflow_dispatch` from the Actions tab. You can specify a commit SHA (defaults to head of main) and which platform to build (defaults to both of them). ### Tips and Tricks for Debugging Failed Builds and Otherwise Developing in This Repo diff --git a/scripts/clone.sh b/scripts/clone.sh index 53d59dc..bf3160d 100755 --- a/scripts/clone.sh +++ b/scripts/clone.sh @@ -2,10 +2,26 @@ set -xe -mkdir -p /home/developer/nodejs -git clone https://github.com/nodejs/node.git --depth 1 /home/developer/nodejs/node +# NODE_REF pins the image to one nodejs/node commit. +# This is specified by the repository_dispatch event from nodejs/node (the commit associated with the successful daily build) or the manual `node_ref` input from workflow_dispatch. +# Otherwise, it defaults to the latest commit on `main` branch. +# This script is responsible for loosely validating the specified value as at least being a valid commit SHA. If a SHA not available in the nodejs/node repo is provided, the `git fetch` step will fail loudly. +if [ -n "${NODE_REF:-}" ] && [[ ! "$NODE_REF" =~ ^[0-9a-f]{40}$ ]]; then + echo "NODE_REF must be a full commit SHA, got: $NODE_REF" >&2 + exit 1 +fi + +# Equivalent to `git clone --depth 1`, except it also respects NODE_REF. +# Fetching only that commit keeps its history connected to main, so a later `git pull` fast-forwards. +mkdir -p /home/developer/nodejs/node ( cd /home/developer/nodejs/node + git init --initial-branch=main + git remote add --track main origin https://github.com/nodejs/node.git + git fetch --depth 1 origin "${NODE_REF:-main}" + git reset --hard FETCH_HEAD + git update-ref refs/remotes/origin/main FETCH_HEAD + git branch --set-upstream-to=origin/main main git remote add upstream https://github.com/nodejs/node.git git restore-mtime # Restore file modification times to commit times for build cache to match. )