Skip to content
Open
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
30 changes: 0 additions & 30 deletions .github/workflows/docker-build-and-publish-linux-amd64.yml

This file was deleted.

30 changes: 0 additions & 30 deletions .github/workflows/docker-build-and-publish-linux-arm64.yml

This file was deleted.

117 changes: 101 additions & 16 deletions .github/workflows/docker-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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-<arch> tag.
# Then, the merge workflow combines the platform tags into `nightly`.
#
# If any platform fails, the others still push their own nightly-linux-<arch> 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 }}
16 changes: 7 additions & 9 deletions .github/workflows/docker-merge.yml
Original file line number Diff line number Diff line change
@@ -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 }}
Expand All @@ -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 \
Expand Down
2 changes: 2 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,15 @@ Some useful commands:
- `docker image ls` - list the images and IDs
- `docker run -it <image id>` - run a container and shell into it
- `docker tag <image id> devcontainer:nightly` - run to tag an image as `nightly`
- `docker build --build-arg NODE_REF=<commit sha> .` - 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-<arch>`, 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

Expand Down
20 changes: 18 additions & 2 deletions scripts/clone.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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.
)