diff --git a/scripts/installtests/build_release_assets_test.go b/scripts/installtests/build_release_assets_test.go index 09a4d7bc8..c019783c2 100644 --- a/scripts/installtests/build_release_assets_test.go +++ b/scripts/installtests/build_release_assets_test.go @@ -1478,9 +1478,10 @@ func TestDockerBuildUsesCanonicalReleaseLdflags(t *testing.T) { t.Fatalf("Dockerfile missing canonical release ldflags usage: %s", needle) } } - assertDigestPinnedDockerBase(t, dockerfile, `FROM --platform=linux/amd64 node:20-alpine@sha256:`) - assertDigestPinnedDockerBase(t, dockerfile, `FROM --platform=linux/amd64 golang:1.26.7-alpine@sha256:`) - assertDigestPinnedDockerBase(t, dockerfile, `FROM alpine:3.20@sha256:`) + assertDigestPinnedDockerStage(t, dockerfile, `FROM --platform=linux/amd64 node:20-alpine@sha256:`, ` AS frontend-builder`) + assertDigestPinnedDockerStage(t, dockerfile, `FROM --platform=linux/amd64 golang:1.26.7-alpine@sha256:`, ` AS backend-builder`) + assertDigestPinnedDockerStage(t, dockerfile, `FROM alpine:3.20@sha256:`, ` AS agent_runtime`) + assertDigestPinnedDockerStage(t, dockerfile, `FROM alpine:3.20@sha256:`, ` AS pulse-runtime-foundation`) hostedStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS hosted_runtime`) runtimeStart := strings.Index(dockerfile, `FROM pulse-runtime-base AS runtime`) if hostedStart == -1 || runtimeStart == -1 || hostedStart > runtimeStart { diff --git a/scripts/installtests/dependency_update_test.go b/scripts/installtests/dependency_update_test.go index 597c64ac6..23dc97b0f 100644 --- a/scripts/installtests/dependency_update_test.go +++ b/scripts/installtests/dependency_update_test.go @@ -5,12 +5,19 @@ import ( "testing" ) -// assertDigestPinnedDockerBase allows automated digest refreshes while still -// rejecting shortened, malformed, or mutable base-image references. -func assertDigestPinnedDockerBase(t *testing.T, dockerfile, prefix string) { +// assertDigestPinnedDockerStage allows automated digest refreshes while still +// rejecting shortened, malformed, mutable, or decoy base-image references for +// the named production stage. +func assertDigestPinnedDockerStage(t *testing.T, dockerfile, prefix, suffix string) { t.Helper() - pattern := regexp.MustCompile(regexp.QuoteMeta(prefix) + `[0-9a-f]{64}(?:\s|$)`) + pattern := regexp.MustCompile( + `(?m)^` + regexp.QuoteMeta(prefix) + `[0-9a-f]{64}` + regexp.QuoteMeta(suffix) + `$`, + ) if !pattern.MatchString(dockerfile) { - t.Fatalf("Dockerfile base image must use a full immutable digest: %s<64 lowercase hex characters>", prefix) + t.Fatalf( + "Dockerfile stage must use a full immutable digest: %s<64 lowercase hex characters>%s", + prefix, + suffix, + ) } } diff --git a/scripts/installtests/provider_msp_deploy_test.go b/scripts/installtests/provider_msp_deploy_test.go index 96242d9ce..9cc5f8f89 100644 --- a/scripts/installtests/provider_msp_deploy_test.go +++ b/scripts/installtests/provider_msp_deploy_test.go @@ -332,9 +332,9 @@ func TestProviderMSPControlPlaneDockerfileBuildsReleaseLicenseBinary(t *testing. "FROM alpine:3.21", "CGO_ENABLED=0 go build -o /pulse-control-plane ./cmd/pulse-control-plane", ) - assertDigestPinnedDockerBase(t, text, `FROM --platform=linux/amd64 node:20-alpine@sha256:`) - assertDigestPinnedDockerBase(t, text, `FROM --platform=$BUILDPLATFORM golang:1.26.7-alpine@sha256:`) - assertDigestPinnedDockerBase(t, text, `FROM alpine:3.20@sha256:`) + assertDigestPinnedDockerStage(t, text, `FROM --platform=linux/amd64 node:20-alpine@sha256:`, ` AS frontend-builder`) + assertDigestPinnedDockerStage(t, text, `FROM --platform=$BUILDPLATFORM golang:1.26.7-alpine@sha256:`, ` AS builder`) + assertDigestPinnedDockerStage(t, text, `FROM alpine:3.20@sha256:`, ` AS control-plane-runtime-foundation`) } func assertContainsAll(t *testing.T, text string, required ...string) { diff --git a/scripts/tests/test_dependabot_config.py b/scripts/tests/test_dependabot_config.py index f3f112272..1ffbeb045 100644 --- a/scripts/tests/test_dependabot_config.py +++ b/scripts/tests/test_dependabot_config.py @@ -12,6 +12,18 @@ CONFIG = ROOT / ".github" / "dependabot.yml" SECURITY_SCAN = ROOT / ".github" / "workflows" / "security-scan.yml" +def manifest_directories(filename: str) -> list[str]: + """Return repository-relative Dependabot directories for every manifest.""" + directories = set() + for manifest in ROOT.rglob(filename): + relative = manifest.relative_to(ROOT) + if "node_modules" in relative.parts: + continue + parent = relative.parent.as_posix() + directories.add("/" if parent == "." else f"/{parent}") + return sorted(directories) + + class DependabotConfigTest(unittest.TestCase): @classmethod def setUpClass(cls) -> None: @@ -26,17 +38,12 @@ class DependabotConfigTest(unittest.TestCase): ) self.assertEqual(self.updates["github-actions"]["directory"], "/") self.assertEqual( - self.updates["gomod"]["directories"], - ["/", "/tests/integration/mock-github-server"], + sorted(self.updates["gomod"]["directories"]), + manifest_directories("go.mod"), ) self.assertEqual( - self.updates["npm"]["directories"], - [ - "/", - "/frontend-modern", - "/internal/cloudcp/portal/frontend", - "/tests/integration", - ], + sorted(self.updates["npm"]["directories"]), + manifest_directories("package-lock.json"), ) self.assertEqual( self.updates["docker"]["directories"], @@ -98,16 +105,17 @@ class DependabotConfigTest(unittest.TestCase): jobs = workflow["jobs"] self.assertEqual( set(jobs["govulncheck"]["strategy"]["matrix"]["directory"]), - {".", "tests/integration/mock-github-server"}, + { + "." if path == "/" else path.removeprefix("/") + for path in manifest_directories("go.mod") + }, ) npm_sets = jobs["npm-audit"]["strategy"]["matrix"]["include"] self.assertEqual( {item["directory"] for item in npm_sets}, { - ".", - "frontend-modern", - "internal/cloudcp/portal/frontend", - "tests/integration", + "." if path == "/" else path.removeprefix("/") + for path in manifest_directories("package-lock.json") }, ) scan_steps = jobs["npm-audit"]["steps"]