Repository navigation
ci(docker): publish multi-arch Docker images from a manual workflow - #138
Open
wayfarer3130 wants to merge 17 commits into
Open
wayfarer3130 wants to merge 17 commits into
wayfarer3130 wants to merge 17 commits into
Conversation
…h audit findings - @cornerstonejs/core 4.22.3 -> 5.11.4 (adds metadata/utils peers), dicom-codec 1.1.6, codec-openjph 2.4.11, codec-openjpeg 1.3.6 - dcmjs 0.50.1 -> 0.52.0 - Fix high/critical bun audit findings: aws-cdk-lib 2.272.0 (cli 2.1144.0), ws 8.22.0, adm-zip 0.6.1, overrides for axios, fast-uri, js-yaml, pacote, smol-toml, tar; re-resolve other transitive deps within their ranges - cs3d: compile with module node20 so it can import the ESM-only core 5 types - jest: transform gl-matrix, which core 5 installs nested - bunfig: pin linker = "hoisted" (bun.lock configVersion 1 defaults to isolated) and apply minimumReleaseAge to install:update-lockfile Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 5.11.5 Bun does not accept a scope wildcard in minimumReleaseAgeExcludes, so the excludes list each @cornerstonejs package in the dependency tree. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fixable audit advisories - Override http-cache-semantics to 4.3.0 to clear the lerna advisory. - Ignore four dev-only DoS advisories in the CI audit step: braces has no patched release, and nx pins brace-expansion 5.0.8 exactly. - Bump constructs to 10.8.1 to meet the aws-cdk-lib peer range ^10.5.0. - Note in both bunfig files that the Cornerstone3D exclude lists must match. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ed publishing - Replace bun and lerna with pnpm 12.9.1 for install, lockfile, scripts and audit. Bun stays the runtime for the CLIs and the Docker service image. - pnpm-workspace.yaml holds the overrides, the release-age rule (with a @cornerstonejs/* exclude), allowBuilds and auditConfig.ignoreGhsas. Range overrides fix brace-expansion, so only braces (no patched release) is ignored. - CI uses pnpm/action-setup and Node 24.15.0, as Cornerstone3D does. - Add publish.yml and scripts/release/*, adapted from the Cornerstone3D release flow: version from the last commit message, atomic push of the version commit and tag, then npm publish --provenance with OIDC. - Fix repository url/directory in each package.json so provenance matches. - Align healthlakestore to 1.7.6 with the other packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take #136 (squash of the branch this one builds on). Keep the pnpm side of the bun-to-pnpm conflicts: drop bun.lock and the bunfig files, keep the pnpm audit step. Carry over the review fixes: the scp bin scripts become 100755, and the braces ignore comment in pnpm-workspace.yaml says the advisory is unreachable, not dev-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ruleset can allow it Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark @radicalimaging/healthlakestore private so the release scripts skip it, and restore its version to 1.6.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge Dockerfile and arm-Dockerfile into one Dockerfile that builds for linux/amd64 and linux/arm64. arm64 compiles canvas from source, because canvas 3.1.0 has no linux-arm64 prebuilt binary. The image build skips s3-deploy, static-wado-deploy and healthlakestore. Add the "Publish Docker images" workflow (workflow_dispatch only). It builds a release tag on native amd64 and arm64 runners, pushes by digest to GHCR, and joins the digests into one multi-arch tag. The publish job uses the docker-hub environment, which holds the Docker Hub token behind a required reviewer. Without the token, the job publishes to GHCR only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Take master's tree (the squash merge of #137 and the v1.7.7 release) and keep only the Docker changes of this branch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…subject The build job runs canvas and the CLI commands in each image before the publish job tags it. The workflow header notes that the environment changes the OIDC subject for a later move to Docker Hub OIDC. The Dockerfile says why amd64 gets no canvas build packages. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blish workflow
The installer stage ran `npm install` on the packed packages, so npm
ignored pnpm-lock.yaml and the overrides of pnpm-workspace.yaml. The
1.7.7 image held 21 package versions that the lockfile does not have,
among them adm-zip 0.5.17 and 0.5.18 (5 high advisories) and dcmjs
0.38.3. `pnpm deploy --prod` of static-wado-webserver now installs the
image from the lockfile; the image holds no version outside it.
Dockerfile:
- Pin node:24-trixie and oven/bun:1.3.13 by digest. Both are trixie, so
the canvas that arm64 compiles finds the same libraries at run time.
- Drop the installer stage and the root *.tgz copies.
docker-publish.yml:
- Fail when DOCKERHUB_TOKEN is missing, unless the ghcr_only input is set.
- Fail a run that does not start on master, instead of a green skip.
- Choose and check the tag in scripts/release/docker-release.mjs: master
must contain it, and it must carry its version. Build the commit SHA.
- Pin actions by commit, restore no build cache, use permissions: {}.
- Keep the digests 30 days (the approval wait), with overwrite: true.
- Smoke test that the default command serves /dicomweb/studies.
- Describe the token scope and the GHCR limit of the gate correctly.
Keep platform: linux/amd64 in docker-compose.yml until arm64 images
exist, and say in the README which releases are multi-arch.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… amd64 pin A maintainer pushed 1.7.7 and latest as one linux/amd64 and linux/arm64 index, built from this branch. The README now says that the images are multi-arch from 1.7.7, and docker-compose.yml no longer forces platform: linux/amd64. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… only what the image needs Review round 2 of the Docker publish workflow. docker-publish.yml: - Fail when a registry already holds X.Y.Z, unless the new overwrite input is set. Only "not found" counts as a free tag. - The publish job has only packages: write. - Explain why the token check runs after the approval, the artifact retention, and what the concurrency group cancels. Dockerfile: - Pin the dockerfile:1.7-labs frontend by digest. - Build with the filter static-wado-webserver..., the packages that pnpm deploy copies (6 of 11 workspace projects). - Use --no-install-recommends, and install ca-certificates explicitly. - Keep the install scripts of the global pnpm: with --ignore-scripts, pnpm runs its Node launcher, and the arm64 canvas build fails with "node-gyp: not found". docker-release.mjs uses only the release tags that master contains, and quotes the input in its error messages. The README says that only Docker Hub has the approval gate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wayfarer3130
marked this pull request as ready for review
October 8, 2026 22:47
… the image in PRs Review round 3 of the Docker publish workflow. docker-publish.yml: - A version keeps one image. When GHCR holds the version (a run that stopped before Docker Hub, or a ghcr_only run), publish that image, not the new build. A tag with the same manifests counts as done, so a re-run continues; a different image needs overwrite. Check each new tag, latest included, against the expected manifests. - Pin the BuildKit daemon of the build job by digest; the publish job runs no BuildKit daemon. - Add the OCI labels source, revision and version. docker-release.mjs refuses a tag whose Dockerfile does not install from pnpm-lock.yaml (v1.7.7 and older), and resolves refs/tags/<tag>. scripts/docker-smoke-test.sh holds the smoke test of both workflows. It runs createdicomweb --help and mkdicomweb --help, uses a free host port, and waits up to 180 s, for arm64 under QEMU. New docker-ci.yml builds and tests the amd64 image in a pull request that changes the Docker files or the lockfile. Dockerfile: correct the canvas comment (node:24-trixie has the canvas build libraries, so a failed amd64 download compiles canvas), and check in the final stage that canvas loads. docker-compose.yml drops port 11115: no image since at least 1.7.6 has the DIMSE SCP of the old v1 tag. The docker:build scripts no longer run cleanTgz. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review round 4 of the Docker publish workflow. - Move the publish logic to scripts/docker-publish-tags.sh, with scripts/docker-publish-tags.test.sh: 13 checks against a local registry, in docker-ci.yml with shellcheck. A copy of the script without the revision check and the latest guard fails exactly those checks. - latest moves only forward: a re-run of an older release leaves a newer latest alone. - A GHCR version that the run reuses must carry the release commit as its revision label, because GHCR is not behind the approval. - Refuse ghcr_only with overwrite. - The Dockerfile replaces the labels of oven/bun with the labels of this project; the workflow passes version, revision and the commit time, and the tag script adds index annotations. - The final stage runs apt-get upgrade: the pinned base image predates the trixie security updates (docker scout: 5 critical and 16 high findings before, 0 critical and 3 unfixed high after). - docker-ci.yml builds amd64 and arm64, and also starts for package changes. - The smoke test sets its trap first, limits each request to 5 s, and takes SMOKE_TIMEOUT_SECONDS for QEMU runs. - The Dockerfile marker must be on a RUN line; publish runs on ubuntu-24.04; the README drops the DIMSE port. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ten the tag test Review round 5 of the Docker publish workflow. - The test disables SC2317 and SC2329: the shellcheck 0.9.0 of the ubuntu-24.04 runner uses the older code, so the CI job failed. - docker-publish-tags.sh trusts only an X.Y.Z version label on latest. sort -V puts `dev` (the label of a local build) above every number, so one local push to latest would have kept latest there for good. - The local docker scripts use static-dicomweb:dev, not the Docker Hub name. - docker-publish-tags.sh uses inherit_errexit, and refuses a digest file name that is not 64 hex characters. - The test now has 18 checks: Docker Hub stays free after ghcr_only, 1.8.2 has its own images, latest is checked after each move, a dev label, and a bad digest name. The registry binds 127.0.0.1, and the cleanup removes its volume. - The publish job gets contents: read for its checkout. - docker-ci.yml also starts for tsconfig.json, tsconfig.base.json and babel.config.js. - startStaticDicomweb.sh runs only monitordicomwebserver: the image has no dicomwebscp. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…script executable Review round 6 of the Docker publish workflow. - docker-publish-tags.sh no longer takes LATEST. Each run moves latest, unless the version label of the current latest is a newer X.Y.Z. Before, only the newest tag on master could move latest, so an older release could not move latest forward when a newer tag had no image, and an overwrite run left latest on the replaced image. docker-release.mjs no longer writes a latest output. - git marks docker/startStaticDicomweb.sh executable, so an image built on Linux can run it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
rleisti
approved these changes
Oct 9, 2026
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR adds a manual workflow that publishes the Docker image for
linux/amd64andlinux/arm64. The PR also mergesDockerfileandarm-Dockerfileinto oneDockerfile, and makes the image install its packages frompnpm-lock.yaml. A new CI workflow builds and tests the image for both architectures in a pull request that changes the files of the image.The publish workflow builds release tags from the publish workflow of #137. The first tag that it can build is the first release after the merge of this PR.
Security fix: the image did not use the lockfile
The old installer stage ran
npm installon the packed packages. npm ignoredpnpm-lock.yamland theoverridesofpnpm-workspace.yaml, and resolved each dependency again.adm-zip0.5.17 and 0.5.18, with 5 high advisories (for example GHSA-8238-w5pm-2374). The override pinsadm-zipto 0.6.1.dcmjs-dimsealso broughtdcmjs0.38.3. The override pinsdcmjsto 0.52.0.@radicalimaging/static-wado-pluginsfrom the registry with the range>=1.7.7. A later build of the same tag could get a newer version.The builder now runs
pnpm deploy --prodfor@radicalimaging/static-wado-webserver. On amd64 and on arm64, the image now holds no package version that is not inpnpm-lock.yaml.Manual release of 1.7.7
A maintainer built 1.7.7 by hand from this branch (the code of
v1.7.7and the Docker changes of this PR at that time), and pushed it to Docker Hub:braveheartsoftware/static-dicomweb:1.7.7and:latestare one multi-arch index (sha256:c932b0bf9d27…) forlinux/amd64andlinux/arm64, with attestations.adm-zip0.6.1.oven/bun(for exampleorg.opencontainers.image.source=https://github.com/oven-sh/bunandversion=1.3.13), because the Dockerfile set no labels then. The current Dockerfile sets the labels of this project. The 1.7.7 image keeps the old labels; the next release gets the correct ones.Changes for users
1.7.7orlatest) holdslinux/amd64andlinux/arm64, and Docker selects the correct architecture for the host. Before, Docker Hub had amd64 images only. An arm64 image existed only as a local build (pnpm run docker:build:arm), with the local tag:arm.v1(2024-11-06, amd64) started/app/startStaticDicomweb.sh, which randicomwebserveranddicomwebscp scp -p 11115. No image since at least 1.7.6 containsdicomwebscp, and the default command runs onlymonitordicomwebserver.docker/docker-compose.ymlmoves fromv1tolatest, and drops the mapping of port 11115. A compose user who used the SCP on 11115 loses it.docker runexample, and says that the image has no SCP.docker/docker-compose.ymldoes not forceplatform: linux/amd64now, so an arm64 host runs the image without emulation.X.Y.Zandlatest. It makes noXorX.Ytags.overwrite.latestmoves only forward. A run for a release moveslatestwhenlatestholds an older version, also when a newer tag onmasterhas no image yet. A re-run of an older release does not movelatestback.monitordicomwebserver. The oldarm-Dockerfileuseddicomwebserver, and had nocurland noEXPOSE 6499.ghcr.io/radicalimaging/static-dicomweb./appin the image now holds the deployedstatic-wado-webserverpackage (bin/,lib/,dist/,package.json,pnpm-lock.yaml), not an npm project with fivefile:dependencies and apackage-lock.json. The commandscreatedicomweb,mkdicomweb,dicomwebserverandmonitordicomwebserverstay onPATH.node_modules/.binno longer has the dev tools of the old image (for exampleacorn,terser,webpack).pnpm run docker:build:armnow builds a reallinux/arm64image. On an amd64 host, the build needs QEMU. Before, the script built an image for the architecture of the host.docker:build,docker:build:arm,docker:runanddocker:dicomwebservernow use the local tagsstatic-dicomweb:devandstatic-dicomweb:dev-arm64, notbraveheartsoftware/static-dicomweb:latest. A local build then cannot replace a release by accident./app/startStaticDicomweb.shnow runs onlymonitordicomwebserver, and git marks it executable (100755), so an image built on Linux can run it. Before, it also starteddicomwebscp, which the image does not contain.Implementation
Dockerfile
node:24-trixie, and the final stage usesoven/bun:1.3.13. Each image is pinned by digest. Both images use Debian trixie, so the canvas that arm64 compiles finds the same libraries at run time.docker/dockerfile:1.7-labs) is also pinned by digest.apt-get installuses--no-install-recommends, and the final stage installsca-certificatesexplicitly.apt-get upgrade, because the pinnedoven/bunimage predates the trixie security updates.docker scouton the amd64 image: before, 5 critical and 15 high findings (perl, glibc, pcre2, sqlite3 and others); after, 0 critical and 3 high. The 3 have no fixed version (zlib,cyrus-sasl2), or are the acceptedbracesadvisory ofpnpm-workspace.yaml. The published 1.7.7 image has the old packages.node:24-trixiehas all of them exceptlibgif-dev, but the list names each one.node:24-trixie, and the final stage does not have them. The final stage runsrequire('canvas').createCanvas(1, 1), so such an image fails the build, also in a local build.node-gypto the canvas build on arm64. A local arm64 build with--ignore-scriptsfailed withnode-gyp: not found. The pnpm version must stay equal topackageManagerinpackage.json; corepack is not an option, because Node removes it after version 24.@radicalimaging/static-wado-webserver..., so the build covers the server and the 5 workspace packages that it needs. These are the packages thatpnpm deploycopies.dicomwebserverandmonitordicomwebserverintonode_modules/.bin, becausepnpm deploylinks only the commands of the dependencies.oven/bun. The build argumentsIMAGE_VERSION,IMAGE_REVISIONandIMAGE_CREATEDset the release values; a local build getsdev,unknownandunknown..dockerignoreexcludes thetestdatasubmodule. Thedocker:buildscripts no longer runcleanTgz.Workflow
.github/workflows/docker-publish.ymlworkflow_dispatchis the only trigger. The inputs:tag: optional. An empty value selects the newestvX.Y.Ztag onmaster.ghcr_only: skip Docker Hub.overwrite: replace the image of a version in both registries. Theresolvejob refusesoverwritetogether withghcr_only, because that run gives the two registries two images for one version.masterfails. It does not show a green run that publishes nothing.resolvejob runsscripts/release/docker-release.mjs:mastercontains.packages/create-dicomweb/package.json.Dockerfileof the tag must have aRUNline withpnpm … deploy --prod. The script refusesv1.7.7and older tags with a clear error, so a run cannot build the old Dockerfile.buildjob checks out the commit SHA fromresolve, and builds on a native runner (ubuntu-24.04andubuntu-24.04-arm).moby/buildkit:v0.34.0) is pinned by digest, because it runs the build and holds the push credential.GITHUB_TOKEN, with SBOM and provenance attestations. The labelcreatedis the time of the release commit. A rebuild gives the same label, but afterapt-get upgradeit can hold newer packages.scripts/docker-smoke-test.sh. canvas must make a PNG,createdicomweb --helpandmkdicomweb --helpmust succeed, the other commands must exist, and the default command must answerGET /dicomweb/studieswithin 180 seconds (an arm64 image under QEMU starts slowly). Each request has a limit of 5 seconds.SMOKE_TIMEOUT_SECONDSraises the wait for a local QEMU run; under QEMU the monitor of the image can restart the slow server first. A failed test stops the run before the approval, so a broken image gets no tag.publishjob uses thedocker-hubenvironment, on the pinned runnerubuntu-24.04, withcontents: readandpackages: write. The run stops at this job until a reviewer approves the deployment.DOCKERHUB_TOKEN. When the secret is not set andghcr_onlyis not set, the job fails. Only a job of the environment can see the secret, so this check runs after the approval.scripts/docker-publish-tags.shfrommaster(the dispatch ref) and runs it. The job runs no other code of the repository, and no BuildKit daemon.scripts/docker-publish-tags.shwrites the tags:ghcr_onlyrun), the script publishes that image and not the new build. Therevisionlabel of each platform image must be the release commit, because GHCR is not behind the approval. Otherwise the run fails, unlessoverwriteis set.overwriteis set.latestmoves only forward: each run moveslatest, unless theversionlabel of the currentlatestis a newerX.Y.Z. The newest tag onmasterdoes not decide it, because an npm release does not publish an image, so an older release can still movelatestforward. Another label (for exampledevof a local build, whichsort -Vputs above every number) does not holdlatest.inherit_errexit, so a failed read inside$(...)stops the script.publish.yml: actions pinned by commit, no build cache, andpermissions: {}at the top level. The digest artifacts last 30 days, because an approval can wait 30 days. The repository allows 90 days.Workflow
.github/workflows/docker-ci.ymlDockerfile,.dockerignore,docker/,package.json,tsconfig.json,tsconfig.base.json,babel.config.js,pnpm-lock.yaml,pnpm-workspace.yaml,packages/, the Docker scripts or a Docker workflow starts this workflow. A manual start is also possible.imagejob builds the image for amd64 and arm64 on native runners without a push, and runsscripts/docker-smoke-test.sh. A broken image then shows up before a release, and not after npm holds the version.publish-tagsjob runsshellcheckon the Docker scripts, and runsscripts/docker-publish-tags.test.shagainst a local registry.Limits
GITHUB_TOKEN, so any workflow of this repository withpackages: writecan write to the GHCR package, from any branch. The revision check limits what can reach Docker Hub from GHCR, but a forged label can pass it.braveheartsoftware.braveheartsoftwareis a personal account. A later move to an organization changes only the Docker Hub login step. Because thepublishjob names an environment, the OIDC subject isrepo:RadicalImaging/Static-DICOMWeb:environment:docker-hub, and the Docker Hub ruleset must match that subject.Setup before the first run
braveheartsoftware. Create a personal access token with the access "Read & Write".docker-hub. Add a required reviewer, and keep "Prevent self-review" off.docker-hub, set "Deployment branches and tags" to "Selected branches and tags", with the branchmasteronly.docker-hub, add the secretDOCKERHUB_TOKEN. The variableDOCKERHUB_USERNAMEis optional, and the default isbraveheartsoftware.static-dicomwebon GitHub, and change the visibility to public. GHCR creates a new package as private. Do not push to GHCR by hand before the first run.Tests
release.test.mjshas two new tests, both in the category "the API must do this":chooseRelease: the selection of the newest tag, the numeric order, and the refusal of a tag that is not a release tag.findTagProblems: a wrong version, a missingpackage.json, a Dockerfile without the lockfile install, and a lockfile install in a comment only.scripts/docker-publish-tags.test.sh("the API must do this") runs the tag script against a local registry on127.0.0.1, with smallFROM scratchimages per architecture that carry attestations and labels. It has 18 checks:ghcr_onlypublish leaves Docker Hub alone;ghcr_onlywithoverwriteand a bad digest file name are refused;latesthold one image;overwritereplaces it in both registries;latestto that release, and a re-run of an older release leaveslatestalone;devlabel onlatestdoes not holdlatest.Each refusal check also matches the error message. Mutation tests: a copy of the script without the revision check and without the guard of
latestfails exactly those two checks, and a copy without theX.Y.Zfilter fails exactly thedevcheck.docker-release.mjson this repository refuses an empty input andv1.7.7(old Dockerfile),v1.7.6(masterdoes not contain that tag) andv1.5.0.scripts/docker-smoke-test.shpasses on the local images, and fails with exit code 127 on an image without the commands.Local builds of this branch for
linux/amd64andlinux/arm64(QEMU) succeed, with the labels of this project. The smoke test passes on amd64, and on arm64 under QEMU withSMOKE_TIMEOUT_SECONDS=600(302 seconds). In the images of the previous commit, HTTPS works and each package version is inpnpm-lock.yaml.actionlintreports no errors for both workflows.shellcheck0.9.0 (the version of theubuntu-24.04runner) and 0.11.0 report no errors for the three Docker scripts anddocker/startStaticDicomweb.sh. An earlier commit of this PR failed in CI, because it disabled only SC2329, the newer code of SC2317.docker-publish.ymldid not run. It runs only frommaster, and it needs a release tag that contains the newDockerfile.docker-ci.ymlruns in this PR.🤖 Generated with Claude Code