diff --git a/README.md b/README.md index 66201a8..c62f371 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ - `Cargo.toml` - rust dependencies, including workspaces - `.{github,gitea,forgejo}/{workflows,actions}`, `workflow-templates`, `action.{yml,yaml}` - actions and docker images - `{Docker,Container}file*`, `*.{Docker,Container}file`, `compose*.{yml,yaml}`, `docker-*.{yml,yaml}` - docker images -- `Makefile`, `*.mk` - go tool versions in `go install` paths and docker image tags +- `Makefile`, `*.mk`, `*.sh` - go tool versions in `go install` paths and docker image tags ## Usage @@ -33,7 +33,7 @@ pnpm dlx updates -u && pnpm i |`-f, --file `|File or directory to use, defaults to current directory| |`-N, --include-paths `|Only use paths matching the globs| |`-X, --exclude-paths `|Skip paths matching the globs| -|`-M, --modes `|Which modes to enable. Either `npm`, `pypi`, `go`, `cargo`, `actions`, `docker`, `make`. Default: all| +|`-M, --modes `|Which modes to enable. Either `npm`, `pypi`, `go`, `cargo`, `actions`, `docker`, `make`, `shell`. Default: all| |`-i, --include `|Include only given dependencies| |`-e, --exclude `|Exclude given dependencies| |`-l, --pin `|Pin dependency to given semver range| diff --git a/api.ts b/api.ts index 962ee6f..61004a1 100755 --- a/api.ts +++ b/api.ts @@ -47,9 +47,10 @@ import { } from "./modes/docker.ts"; import { type MakeDockerImage, - isMakeFileName, makeExactFileNames, parseMakeGoInstalls, parseMakeDockerImages, + isMakeFileName, makeExactFileNames, makeAssignmentValues, parseGoInstalls, parseImages, resolveGoModuleRoot, formatMakeImageSpec, updateMakefile, } from "./modes/make.ts"; +import {isShellFileName, shellAssignmentValues} from "./modes/shell.ts"; import {fetchCratesIoInfo, updateCargoToml, updateCargoRange, cargoToNpmRange, parseCargoLock, findLockedVersion} from "./modes/cargo.ts"; import { baseType, filterDepsForMember, resolveWorkspaceMembers, parsePnpmWorkspace, pnpmCatalogEntries, @@ -78,7 +79,7 @@ export type Output = ModeOutput & {errors?: Array}; export type {Config, Override, Dep, Deps, DepsByMode}; export {cliBaseConfig as cliConfigBaseDir, defaultExcludePaths}; -const modeOrder = [...new Set(Object.values(modeByFileName)), "actions", "docker", "make"]; +const modeOrder = [...new Set(Object.values(modeByFileName)), "actions", "docker", "make", "shell"]; const defaultModes = new Set(modeOrder); const semversByPrecision = { @@ -531,6 +532,7 @@ async function runUpdates(opts: UpdatesOptions): Promise { const filename = basename(file); if (isDockerFileName(filename)) return enabledModes.has("docker"); if (isMakeFileName(filename)) return enabledModes.has("make"); + if (isShellFileName(filename)) return enabledModes.has("shell"); return enabledModes.has(modeByFileName[filename]); }), async (file): Promise<[string, string]> => { try { @@ -541,7 +543,7 @@ async function runUpdates(opts: UpdatesOptions): Promise { }, {concurrency})); const fileData: Record, + absPath: string, content: string, fileType?: "dockerfile" | "compose" | "workflow", workflowLines?: Set, }> = {}; const goModFiles: WorkspaceMember[] = []; @@ -553,7 +555,7 @@ async function runUpdates(opts: UpdatesOptions): Promise { const actionDepInfos: Array = []; const dockerDepInfos: Array<{key: string, ref: DockerImageRef, filters: FileFilters}> = []; - type MakeDepInfo = {key: string, name: string, oldSpec: string, filters: FileFilters, newSpec?: string} & ( + type MakeDepInfo = {mode: "make" | "shell", key: string, name: string, oldSpec: string, filters: FileFilters, newSpec?: string} & ( {kind: "go", installPath: string, version: string} | {kind: "docker", image: MakeDockerImage} ); @@ -733,27 +735,34 @@ async function runUpdates(opts: UpdatesOptions): Promise { continue; } - if (isMakeFileName(filename)) { + const specMode = isMakeFileName(filename) ? "make" : isShellFileName(filename) ? "shell" : null; + if (specMode) { + const values = specMode === "make" ? makeAssignmentValues(content) : shellAssignmentValues(content); + const goInstalls = parseGoInstalls(values); + const images = parseImages(values); + if (!goInstalls.length && !images.length) continue; const relPath = toRelPath(file); const filters = await resolveDirConfig(dirname(file)); - fileData[relPath] = {absPath: file, content, fileType: "make"}; - deps.make ??= {}; - for (const {installPath, version} of parseMakeGoInstalls(content)) { - if (!canInclude(installPath, "make", filters, "make", "go")) continue; + fileData[relPath] = {absPath: file, content}; + const modeDeps = deps[specMode] ??= {}; + for (const {installPath, version} of goInstalls) { + if (!canInclude(installPath, specMode, filters, specMode, "go")) continue; const key = dependencyKey(relPath, installPath, version); - if (deps.make[key]) continue; - deps.make[key] = {old: stripv(version), oldOrig: version} as Dep; - makeDepInfos.push({kind: "go", key, name: installPath, oldSpec: `${installPath}@${version}`, installPath, version, filters}); + if (modeDeps[key]) continue; + modeDeps[key] = {old: stripv(version), oldOrig: version} as Dep; + makeDepInfos.push({ + mode: specMode, kind: "go", key, name: installPath, oldSpec: `${installPath}@${version}`, installPath, version, filters, + }); } - for (const image of parseMakeDockerImages(content)) { - if (!canInclude(image.writtenImage, "make", filters, "make", "docker")) continue; + for (const image of images) { + if (!canInclude(image.writtenImage, specMode, filters, specMode, "docker")) continue; const key = dependencyKey(relPath, image.writtenImage, image.ref.tag); - if (deps.make[key]) continue; + if (modeDeps[key]) continue; const parsed = parseDockerTag(image.ref.tag); if (!parsed) continue; const oldSpec = formatMakeImageSpec(image.writtenImage, image.ref.tag, image.digest); - deps.make[key] = {old: parsed.version, oldOrig: image.ref.tag} as Dep; - makeDepInfos.push({kind: "docker", key, name: image.writtenImage, oldSpec, image, filters}); + modeDeps[key] = {old: parsed.version, oldOrig: image.ref.tag} as Dep; + makeDepInfos.push({mode: specMode, kind: "docker", key, name: image.writtenImage, oldSpec, image, filters}); } continue; } @@ -1190,20 +1199,21 @@ async function runUpdates(opts: UpdatesOptions): Promise { const makeTask = pMap(makeDepInfos, async (info) => { const opts = resolveVersionOpts(info.filters, info.kind, info.name, info.name); - const dep = deps.make[info.key]; + const modeDeps = deps[info.mode]; + const dep = modeDeps[info.key]; try { if (info.kind === "go") { const {projectDir} = info.filters; const modulePath = await resolveGoModuleRoot(info.installPath, projectDir, ctx, goNoProxy); - if (!modulePath) { delete deps.make[info.key]; return; } + if (!modulePath) { delete modeDeps[info.key]; return; } const [rawData] = await fetchGoProxyInfo(modulePath, "tool", stripv(info.version), projectDir, ctx, goNoProxy); const data = filterVersionData(rawData, "go", opts.allowedVersions); const newVersion = findNewVersion(data, {...opts, mode: "go", range: stripv(info.version)}); - if (!newVersion) { delete deps.make[info.key]; return; } + if (!newVersion) { delete modeDeps[info.key]; return; } const newModulePath = data.newPath ?? goModulePathForVersion(modulePath, newVersion); const newInstallPath = `${newModulePath}${info.installPath.slice(modulePath.length)}`; const formattedVersion = formatVersionPrecision(newVersion, info.version); - if (newInstallPath === info.installPath && formattedVersion === info.version) { delete deps.make[info.key]; return; } + if (newInstallPath === info.installPath && formattedVersion === info.version) { delete modeDeps[info.key]; return; } info.newSpec = `${newInstallPath}@${formattedVersion}`; dep.new = formattedVersion; dep.info = getGoInfoUrl(newModulePath); @@ -1214,17 +1224,17 @@ async function runUpdates(opts: UpdatesOptions): Promise { filterVersionData(data, "docker", opts.allowedVersions).tags, info.image.ref.tag, opts.semvers, opts.cooldownDays, opts.now, opts.pinnedRange, opts.usePre, opts.useRel, ); - if (!dockerUpdate) { delete deps.make[info.key]; return; } + if (!dockerUpdate) { delete modeDeps[info.key]; return; } const newDigest = info.image.digest ? await resolveDockerTagDigest(info.image.ref.namespace, info.image.ref.repo, dockerUpdate.newTag) : null; - if (info.image.digest && !newDigest) { delete deps.make[info.key]; return; } + if (info.image.digest && !newDigest) { delete modeDeps[info.key]; return; } info.newSpec = formatMakeImageSpec(info.image.writtenImage, dockerUpdate.newTag, newDigest); dep.new = dockerUpdate.newTag; dep.info = getDockerInfoUrl(info.image.ref); setDepAge(dep, dockerUpdate.date); } } catch (err) { - rejectDep("make", info.key, err); + rejectDep(info.mode, info.key, err); } }, {concurrency}); @@ -1288,8 +1298,8 @@ async function runUpdates(opts: UpdatesOptions): Promise { workflowLines!.has(lineNumber) ? updateWorkflowDockerImages(line, workflowDeps) : line).join("\n"); write(absPath, updateFn(content, Object.fromEntries(entries))); } - } else if (mode === "make") { - const makeUpdates = makeDepInfos.filter(info => info.newSpec && deps.make[info.key]); + } else if (mode === "make" || mode === "shell") { + const makeUpdates = makeDepInfos.filter(info => info.newSpec && deps[mode][info.key]); for (const [relPath, infos] of Map.groupBy(makeUpdates, info => info.key.split(fieldSep)[0])) { const {absPath, content} = fileData[relPath]; write(absPath, updateMakefile(content, infos.map(info => ({oldSpec: info.oldSpec, newSpec: info.newSpec!})))); diff --git a/cli.ts b/cli.ts index 11b934d..23d48f8 100644 --- a/cli.ts +++ b/cli.ts @@ -187,7 +187,7 @@ export async function runCli(rawArgs: Array, io: CliIo, prewarm = true): -f, --file File or directory to use, defaults to current directory -N, --include-paths Only use paths matching the globs -X, --exclude-paths Skip paths matching the globs - -M, --modes Which modes to enable. Default: npm,pypi,go,cargo,actions,docker,make + -M, --modes Which modes to enable. Default: npm,pypi,go,cargo,actions,docker,make,shell -i, --include Include only given dependencies -e, --exclude Exclude given dependencies -l, --pin Pin dependency to given semver range @@ -229,6 +229,7 @@ export async function runCli(rawArgs: Array, io: CliIo, prewarm = true): $ updates -f Dockerfile $ updates -f docker-compose.yml $ updates -f Makefile + $ updates -f build.sh `); return 0; } diff --git a/index.test.ts b/index.test.ts index c127549..f81229f 100644 --- a/index.test.ts +++ b/index.test.ts @@ -130,6 +130,7 @@ beforeAll(async () => { ["/docker/v2/repositories/library/redis/tags", fixtureText("docker/redis-tags.json")], ["/docker/v2/repositories/koalaman/shellcheck/tags", dockerTags(["v0.11.0", "2025-01-01T00:00:00Z"], ["v0.12.0", "2025-06-01T00:00:00Z"])], ["/docker/v2/repositories/koalaman/shellcheck/tags/v0.12.0", JSON.stringify({digest: "sha256:list-new"})], + ["/docker/v2/repositories/example/image/tags", dockerTags(["1.0", "2025-01-01T00:00:00Z"], ["1.1", "2025-06-01T00:00:00Z"])], ["/docker/v2/repositories/example/makeallowed/tags", dockerTags(["1.0", "2025-01-01T00:00:00Z"], ["1.1", "2025-03-01T00:00:00Z"], ["2.0", "2025-06-01T00:00:00Z"])], ["/cargo/se/rd/serde", fixtureText("cargo/serde-index.ndjson")], @@ -157,6 +158,7 @@ beforeAll(async () => { const gz = await promisify(gzip)(await body, gzipOptions); routes.set(path, (_, res) => res.send(gz)); })); + routes.set("/docker/v2/repositories/example/broken/tags", (_, res) => { res.writeHead(500).end(); }); routes.set("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/github/user", (req, res) => { res.writeHead(req.headers.authorization === "Bearer tok" ? 200 : 401).end(JSON.stringify({login: "someone"})); }); @@ -541,6 +543,39 @@ test("make mode bumps docker image tags and re-resolves digests in Makefiles", a )); }); +test("make and shell modes bump discovered files alike, leaving current, unresolvable, excluded and failed values untouched", async () => { + const digest = `sha256:${"a".repeat(64)}`; + const tools = [ + "github.com/example/testpkg/v2@v2.0.0", "github.com/example/pseudoupd@v1", "github.com/example/missing@v1.0.0", + "github.com/example/excluded@v1.0.0", + ].join(" "); + const images = `example/makeallowed:1.0@${digest} example/broken:1.0`; + const dir = writeTree("shell", { + "Makefile": lines("TOOL ?= github.com/example/testpkg@v1.0.0", "IMAGE := example/image:1.0"), + "build.sh": lines( + `TOOL="\${TOOL:-github.com/example/testpkg@v1.0.0}"`, + "export IMAGE=example/image:1.0", + `TOOLS=(github.com/example/testpkg@v1.0.0 ${tools})`, + `IMAGES=(example/image:1.0 ${images})`, + ), + "noop.sh": "echo done\n", + }); + const {results, errors} = await updates({ + files: [dir], modes: ["make", "shell"], exclude: ["github.com/example/excluded"], goproxy: goProxyUrl, dockerapi: dockerUrl, + update: true, color: false, noCache: true, + }); + expect(Object.fromEntries(Object.entries(results).map(([mode, files]) => [mode, Object.keys(files).map(file => basename(file))]))) + .toEqual({make: ["Makefile"], shell: ["build.sh"]}); + expect(errors!.map(({mode, name}) => [mode, name])).toEqual([["shell", "example/broken"]]); + expect(read(dir, "Makefile")).toBe(lines("TOOL ?= github.com/example/testpkg/v2@v2.0.0", "IMAGE := example/image:1.1")); + expect(read(dir, "build.sh")).toBe(lines( + `TOOL="\${TOOL:-github.com/example/testpkg/v2@v2.0.0}"`, + "export IMAGE=example/image:1.1", + `TOOLS=(github.com/example/testpkg/v2@v2.0.0 ${tools})`, + `IMAGES=(example/image:1.1 ${images})`, + )); +}); + test("make allowedVersions falls back to the highest allowed candidate", async () => { const dir = writeTree("make-allowed", { "Makefile": lines("TOOL := github.com/example/makeallowed/cmd/tool@v1.0.0", "IMAGE := example/makeallowed:1.0"), diff --git a/modes/make.test.ts b/modes/make.test.ts index b7f77b4..75a9bf2 100644 --- a/modes/make.test.ts +++ b/modes/make.test.ts @@ -1,8 +1,9 @@ import { isMakeFileName, - parseMakeGoInstalls, + makeAssignmentValues, + parseGoInstalls, + parseImages, parseMakeImageValue, - parseMakeDockerImages, resolveGoModuleRoot, updateMakefile, } from "./make.ts"; @@ -29,8 +30,8 @@ test("isMakeFileName matches make filenames", () => { .toEqual([true, true, true, true, false, false]); }); -test("parseMakeGoInstalls extracts go install specs across assignment operators", () => { - expect(parseMakeGoInstalls(sample)).toEqual([ +test("parseGoInstalls extracts go install specs across make assignment operators", () => { + expect(parseGoInstalls(makeAssignmentValues(sample))).toEqual([ {installPath: "github.com/golangci/golangci-lint/v2/cmd/golangci-lint", version: "v2.12.2"}, {installPath: "github.com/air-verse/air", version: "v1.65.1"}, {installPath: "github.com/go-delve/delve/cmd/dlv", version: "v1"}, @@ -84,6 +85,10 @@ test.each([ ["does not match inside a registry prefix", "PREFIXED := docker.io/koalaman/shellcheck:v0.11.0\n", [{oldSpec: "koalaman/shellcheck:v0.11.0", newSpec: "koalaman/shellcheck:v0.12.0"}], "PREFIXED := docker.io/koalaman/shellcheck:v0.11.0\n"], + ["rewrites specs delimited by shell syntax", + `A="\${A:-org/app:1.0}"\nB=(org/app:1.0)\nC=org/app:1.0;\n`, + [{oldSpec: "org/app:1.0", newSpec: "org/app:1.1"}], + `A="\${A:-org/app:1.1}"\nB=(org/app:1.1)\nC=org/app:1.1;\n`], ["rewrites tag and digest", "SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@sha256:aaa # renovate: datasource=docker\n", [{oldSpec: "docker.io/koalaman/shellcheck:v0.11.0@sha256:aaa", newSpec: "docker.io/koalaman/shellcheck:v0.12.0@sha256:bbb"}], "SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.12.0@sha256:bbb # renovate: datasource=docker\n"], @@ -238,7 +243,7 @@ test("parseMakeImageValue parses a Hub image with registry prefix and digest", ( expect(["mysql:3306", "golang:1.21", "ghcr.io/foo/bar:1.2.3", "plain-no-tag"].map(parseMakeImageValue)).toEqual([null, null, null, null]); }); -test("parseMakeDockerImages extracts only namespaced Hub images, skipping comments", () => { +test("parseImages extracts only namespaced Hub images from make assignments, skipping comments", () => { const content = [ `SHELLCHECK_IMAGE ?= docker.io/koalaman/shellcheck:v0.11.0@${digestA} # renovate: datasource=docker`, "PLAIN := koalaman/shellcheck:0.9.0", @@ -247,7 +252,7 @@ test("parseMakeDockerImages extracts only namespaced Hub images, skipping commen "MYSQL_HOST ?= mysql:3306", `# DISABLED := koalaman/shellcheck:0.1.0@${digestB}`, ].join("\n"); - expect(parseMakeDockerImages(content).map(i => ({image: i.writtenImage, tag: i.ref.tag, digest: i.digest}))).toEqual([ + expect(parseImages(makeAssignmentValues(content)).map(i => ({image: i.writtenImage, tag: i.ref.tag, digest: i.digest}))).toEqual([ {image: "docker.io/koalaman/shellcheck", tag: "v0.11.0", digest: digestA}, {image: "koalaman/shellcheck", tag: "0.9.0", digest: null}, {image: "koalaman/shellcheck", tag: "0.10.0", digest: null}, diff --git a/modes/make.ts b/modes/make.ts index 4325fa8..b2afd2f 100644 --- a/modes/make.ts +++ b/modes/make.ts @@ -15,7 +15,7 @@ const makeAssignRe = /^\s*(?:(?:export|override|private|unexport)\s+)*[A-Za-z_][ const makeGoInstallRe = /^([^@\s]+)@(v\d\S*)$/; const goHostRe = /^[^/\s]+\.[^/\s]+\//; -function* makeAssignmentValues(content: string): Generator { +function* assignmentWords(content: string, assignRe: RegExp): Generator { let logicalLine = ""; for (const rawLine of [...content.split(/\r?\n/), ""]) { const commentIndex = rawLine.indexOf("#"); @@ -26,7 +26,7 @@ function* makeAssignmentValues(content: string): Generator { logicalLine += `${line.slice(0, -1)} `; continue; } - const assignment = makeAssignRe.exec(logicalLine + line); + const assignment = assignRe.exec(logicalLine + line); logicalLine = ""; if (!assignment) continue; let quote = ""; @@ -48,13 +48,20 @@ function* makeAssignmentValues(content: string): Generator { } } -export function parseMakeGoInstalls(content: string) { - const installs: Array<{installPath: string, version: string}> = []; - for (const value of makeAssignmentValues(content)) { +export function assignmentValues(content: string, assignRe: RegExp, unwrapRe?: RegExp): Array { + return Array.from(assignmentWords(content, assignRe), word => unwrapRe?.exec(word)?.[1] ?? word) + .filter(value => !value.includes("$")); +} + +export function makeAssignmentValues(content: string): Array { + return assignmentValues(content, makeAssignRe); +} + +export function parseGoInstalls(values: Array): Array<{installPath: string, version: string}> { + return values.flatMap(value => { const match = makeGoInstallRe.exec(value); - if (match && goHostRe.test(match[1])) installs.push({installPath: match[1], version: match[2]}); - } - return installs; + return match && goHostRe.test(match[1]) ? [{installPath: match[1], version: match[2]}] : []; + }); } export type MakeDockerImage = {writtenImage: string, ref: DockerImageRef, digest: string | null}; @@ -73,8 +80,8 @@ export function parseMakeImageValue(value: string): MakeDockerImage | null { return {writtenImage: imageWithTag.slice(0, imageWithTag.lastIndexOf(":")), ref, digest: digestMatch?.[1] ?? null}; } -export function parseMakeDockerImages(content: string): Array { - return Array.from(makeAssignmentValues(content), parseMakeImageValue).filter(image => image !== null); +export function parseImages(values: Array): Array { + return values.map(parseMakeImageValue).filter(image => image !== null); } const midMajorRe = /\/v(?:[2-9]|[1-9]\d+)(?=\/|$)/; @@ -129,7 +136,7 @@ export function updateMakefile(content: string, rewrites: Array<{oldSpec: string if (!bySpec.size) return content; const specs = Array.from(bySpec.keys()).sort((left, right) => right.length - left.length) .map(spec => Array.from(spec, esc).join(`["']*`)).join("|"); - const specRe = new RegExp(`(? code.replace(specRe, authoredSpec => { const newSpec = bySpec.get(authoredSpec.replace(/["']/g, ""))!; let newIndex = 0; diff --git a/modes/shell.test.ts b/modes/shell.test.ts new file mode 100644 index 0000000..b8cfcac --- /dev/null +++ b/modes/shell.test.ts @@ -0,0 +1,22 @@ +import {parseGoInstalls, parseImages} from "./make.ts"; +import {shellAssignmentValues} from "./shell.ts"; + +const script = [ + `TOOL_PACKAGE="\${TOOL_PACKAGE:-example.com/org/tool/cmd/tool@v1}"`, + " export LINTER=example.com/org/linter@v1.2.3; # linter", + "declare -r TOOLS=(example.com/org/a@v1.0.0 'example.com/org/b@v2.0.0') # tools", + "SPACED = example.com/org/spaced@v1.0.0", + `readonly APP_IMAGE="\${APP_IMAGE:=org/app:1.0}"`, + `REMOTE_IMAGE=\${REGISTRY}/org/app:1.0`, +].join("\n"); + +test("shellAssignmentValues feeds make's parsers, unwrapping arrays and default expansions", () => { + const values = shellAssignmentValues(script); + expect(parseGoInstalls(values)).toEqual([ + {installPath: "example.com/org/tool/cmd/tool", version: "v1"}, + {installPath: "example.com/org/linter", version: "v1.2.3"}, + {installPath: "example.com/org/a", version: "v1.0.0"}, + {installPath: "example.com/org/b", version: "v2.0.0"}, + ]); + expect(parseImages(values).map(image => `${image.writtenImage}:${image.ref.tag}`)).toEqual(["org/app:1.0"]); +}); diff --git a/modes/shell.ts b/modes/shell.ts new file mode 100644 index 0000000..e0bdab3 --- /dev/null +++ b/modes/shell.ts @@ -0,0 +1,12 @@ +import {assignmentValues} from "./make.ts"; + +export function isShellFileName(filename: string): boolean { + return filename.endsWith(".sh"); +} + +const shellAssignRe = /^\s*(?:(?:export|readonly|local|declare|typeset)\s+(?:-\w+\s+)*)*[A-Za-z_]\w*=\(?((?:.*[^\s;)])?)[\s;)]*$/; +const defaultExpansionRe = /^\$\{\w+:?[-=](.*)\}$/; + +export function shellAssignmentValues(content: string): Array { + return assignmentValues(content, shellAssignRe, defaultExpansionRe); +} diff --git a/updates.config.ts b/updates.config.ts index bd400cb..a2e5a0d 100644 --- a/updates.config.ts +++ b/updates.config.ts @@ -1,6 +1,3 @@ import {base} from "updates-config-silverwind"; -export default { - ...base({url: import.meta.url}), - excludePaths: ["fixtures/**"], -}; +export default base({url: import.meta.url, excludePaths: ["fixtures/**"]}); diff --git a/utils/files.ts b/utils/files.ts index becd0fa..2e245e5 100644 --- a/utils/files.ts +++ b/utils/files.ts @@ -6,6 +6,7 @@ import {braceAlternatives, closingIndex, esc, forgeDirs, getOrSet, modeByFileNam import {isWorkflowFile} from "../modes/actions.ts"; import {isDockerFileName} from "../modes/docker.ts"; import {isMakeFileName} from "../modes/make.ts"; +import {isShellFileName} from "../modes/shell.ts"; const execFileAsync = promisify(execFile); const forgeDirNames = new Set(forgeDirs); @@ -62,7 +63,7 @@ export function passesPathFilters(path: string, {includePaths, excludePaths}: Pa function isDependencyFile(path: string): boolean { const filename = basename(path); return Object.hasOwn(modeByFileName, filename) || isDockerFileName(filename) || isMakeFileName(filename) || - isWorkflowFile(path); + isShellFileName(filename) || isWorkflowFile(path); } async function gitFiles(root: string): Promise | null> { diff --git a/utils/prewarm.test.ts b/utils/prewarm.test.ts index ca14e24..a5648f0 100644 --- a/utils/prewarm.test.ts +++ b/utils/prewarm.test.ts @@ -14,7 +14,7 @@ const sampleContent = (path: string, content: string) => { if (path.endsWith("Cargo.toml")) return '[dependencies]\nserde = "1"\n'; if (path.endsWith("go.mod") || path.endsWith("go.work")) return "require example.com/pkg v1.0.0\n"; if (path.includes("Dockerfile") || /compose|docker-stack/.test(path)) return "FROM node:22\n"; - if (/Makefile|makefile|GNUmakefile|\.mk$/.test(path)) return "go install example.com/tool@v1.0.0\ndocker image node:22\n"; + if (/Makefile|makefile|GNUmakefile|\.mk$|\.sh$/.test(path)) return "TOOL=example.com/tool@v1.0.0\nIMAGE=org/app:1.0\n"; if (/\.ya?ml$/.test(path)) return "uses: actions/checkout@v4\ncontainer: node:22\n"; return content; }; @@ -77,7 +77,7 @@ test("GOPROXY decides the go origin", () => { delete process.env.GOPROXY; }); -test.each(["Makefile", "makefile", "GNUmakefile", "tools.mk"])("%s triggers proxy.golang.org + hub.docker.com", (filename) => { +test.each(["Makefile", "makefile", "GNUmakefile", "tools.mk", "build.sh"])("%s triggers proxy.golang.org + hub.docker.com", (filename) => { expect(prewarmOrigins(makeDir({[filename]: ""}), {})).toEqual(expect.arrayContaining(["https://proxy.golang.org/", "https://hub.docker.com/"])); }); diff --git a/utils/prewarm.ts b/utils/prewarm.ts index 2212346..dd3654d 100644 --- a/utils/prewarm.ts +++ b/utils/prewarm.ts @@ -24,6 +24,7 @@ function modeByName(filename: string): string { if (/^(?:[Dd]ocker|[Cc]ontainer)file|\.(?:[Dd]ocker|[Cc]ontainer)file$/.test(filename) || /^(?:docker-|compose).*\.ya?ml$/.test(filename)) return "docker"; if (["Makefile", "makefile", "GNUmakefile"].includes(filename) || filename.endsWith(".mk")) return "make"; + if (filename.endsWith(".sh")) return "shell"; if (/\.ya?ml$/.test(filename)) return "actions"; return ""; } @@ -82,9 +83,9 @@ export function prewarmOrigins(dir: string, args: Record): stri if (/^\s*(?:require\s+)?\S+\s+v\d/m.test(content)) resources.add("goproxy"); } else if (mode === "docker") { if (/^\s*(?:FROM\s+(?:--\S+\s+)*|image\s*:\s*)[^\s#]+[:@]/im.test(content)) resources.add("dockerapi"); - } else if (mode === "make") { - if (/\bgo\s+install\s+\S+@v\d/.test(content)) resources.add("goproxy"); - if (/\b(?:docker|image)\b[^\n]*[\w./-]+:[\w.-]+/i.test(content)) resources.add("dockerapi"); + } else if (mode === "make" || mode === "shell") { + if (/[\w-]+\.[\w.-]+\/\S*@v\d/.test(content)) resources.add("goproxy"); + if (/[\w.-]+\/[\w.-]+:[\w.-]+/.test(content)) resources.add("dockerapi"); } else { let blockIndent = -1; for (const line of content.split(/\r?\n/)) {