ci(docker): install @studio-saelix/sencho-pro in production builds (#881)

Phase 2b of the open-core hybrid extraction. After Phase 2a (PR #880)
wired the public-side loader to dynamic-import the private package,
this PR makes production Docker builds actually install the package
so the runtime path uses it. Single image; saelix/sencho remains the
only published image (the original ADR's dual-image plan was rejected
because customers buying paid tiers would otherwise need GitHub auth
to pull a second image, breaking the purchase flow).

Dockerfile (prod-deps stage): a new RUN block after npm ci installs
@studio-saelix/sencho-pro using a BuildKit secret-mounted github_token
for npm.pkg.github.com auth. The .npmrc carrying the token is written
and removed inside the same RUN, plus /root/.npm is wiped to scrub
any verbose-log artifacts that npm might otherwise stash. The token
never enters an image layer (BuildKit excludes secret content from
both layer filesystems and cache keys; docker history shows the
literal $(cat /run/secrets/...) command, not the substituted value).

PRO_PACKAGE_VERSION is a build arg pinned by CI to a literal SemVer
(0.1.0 today) so the scan build and the publish build resolve to the
same package version. Default of `latest` keeps local builds
convenient. When the pro package ships a new version, bump the value
in docker-publish.yml in the same PR that ships the matching public
Sencho release; release-please does not coordinate the two cadences.

Empty-secret branch (no github_token provided, e.g. local dev or
fork PRs) skips the install and prints a notice. The resulting image
runs through the loader's in-tree LicenseService fallback, so PR
validation builds and contributor builds work without any GitHub
auth setup.

docker-publish.yml: both build-push-action invocations (the
pre-publish scan and the multi-arch publish) pass the github_token
secret and PRO_PACKAGE_VERSION build arg. The auto-provisioned
GITHUB_TOKEN's packages:read scope is sufficient because the public
Sencho repo and the private package live in the same Studio-Saelix
GitHub org. Moving the package to a different org would silently
break this contract; the Dockerfile comment block records the
invariant.

ci.yml is intentionally not changed. The PR-time Docker validation
job builds without the secret and exercises the loader's in-tree
fallback path, which is correct for fork PRs (no token access) and
useful for catching fallback-path regressions.

Test plan: tsc clean (no TS changes). The dockerfile install path is
exercised by the next release's pre-publish scan + smoke test, both
of which boot the actual image and call /api/health. Failed dynamic
import or constructor throw would block bootstrap before the listener
binds, so the existing smoke test covers the runtime contract.
This commit is contained in:
Anso
2026-05-02 13:24:33 -04:00
committed by GitHub
parent cffb481106
commit 638bd808f8
2 changed files with 63 additions and 0 deletions
+24
View File
@@ -118,8 +118,22 @@ jobs:
platforms: linux/amd64
tags: localhost/sencho:release-scan
cache-from: type=registry,ref=saelix/sencho:buildcache
# PRO_PACKAGE_VERSION pins @studio-saelix/sencho-pro to a
# specific SemVer so the scan build and the publish build
# below resolve identically. Bumping the pro package: bump
# this string in the same PR that ships the matching public
# Sencho release; release-please does not coordinate the two.
build-args: |
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
PRO_PACKAGE_VERSION=0.1.0
# GITHUB_TOKEN carries packages:read scope by default, which
# is enough to install @studio-saelix/sencho-pro from
# GitHub Packages during the prod-deps stage. Passing it via
# BuildKit secret keeps it out of image layers; the
# Dockerfile reads /run/secrets/github_token only inside the
# one RUN that authenticates to npm.pkg.github.com.
secrets: |
github_token=${{ secrets.GITHUB_TOKEN }}
- name: Re-scan release image for vulnerabilities (Trivy)
# Gates the release on the same HIGH/CRITICAL policy as the PR scan.
@@ -179,8 +193,18 @@ jobs:
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=saelix/sencho:buildcache
cache-to: type=registry,ref=saelix/sencho:buildcache,mode=max
# PRO_PACKAGE_VERSION must match the value passed to the
# scan build above so Trivy and the smoke test exercise the
# same package version that ships.
build-args: |
APK_CACHE_BUST=${{ steps.apk-bust.outputs.date }}
PRO_PACKAGE_VERSION=0.1.0
# Same secret as the pre-publish scan above so the published
# image carries @studio-saelix/sencho-pro identical to what
# Trivy and the smoke test exercised. BuildKit keeps the
# token out of the published layers.
secrets: |
github_token=${{ secrets.GITHUB_TOKEN }}
# SBOM + provenance attestations are embedded as OCI referrers on the
# published image. Inspect with: docker buildx imagetools inspect <img>
sbom: true