fix: report requested and dependency package versions separately

package-version-list held every installed package and both outputs were
identical. Outputs also carried arch suffixes inconsistently and followed
install order.

- package-version-list lists only requested packages; all-package-version-list
  lists every installed package.
- Entries are name=version, with :arch only for packages installed for a
  non-native architecture (e.g. libc6:i386), so native output matches v1.
- Entries are sorted with PackageName.compareTo.
- Requested packages are matched by name, and by arch when both specify one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
awalsh128 2026-11-06 09:51:27 -08:00
parent 10ade8305a
commit 2c7a114e26
7 changed files with 134 additions and 18 deletions

View file

@ -50,8 +50,8 @@ There are three kinds of version labels you can use.
### Outputs
- `cache-hit` - A boolean value to indicate a cache was found for the packages requested.
- `package-version-list` - The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. \<package1>=<version1\>,\<package2>=\<version2>,...).
- `all-package-version-list` - All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. \<package1>=<version1\>,\<package2>=\<version2>,...).
- `package-version-list` - The main requested packages and versions that are installed. Represented as a sorted comma delimited list with equals delimit on the package version (i.e. \<package1>=\<version1>,\<package2>=\<version2>,...). Packages installed for a non-native architecture include it (i.e. \<package>:\<arch>=\<version>).
- `all-package-version-list` - All the pulled in packages and versions, including dependencies, that are installed. Represented as a sorted comma delimited list with equals delimit on the package version (i.e. \<package1>=\<version1>,\<package2>=\<version2>,...). Packages installed for a non-native architecture include it (i.e. \<package>:\<arch>=\<version>).
### Security Compliance

View file

@ -37,9 +37,9 @@ outputs:
cache-hit:
description: "A boolean value to indicate a cache was found for the packages requested."
package-version-list:
description: "The main requested packages and versions that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>)."
description: "The main requested packages and versions that are installed. Represented as a sorted comma delimited list with equals delimit on the package version (i.e. <package1>=<version1>,<package2>=<version2>,...). Packages installed for a non-native architecture include it (i.e. <package>:<arch>=<version>)."
all-package-version-list:
description: "All the pulled in packages and versions, including dependencies, that are installed. Represented as a comma delimited list with equals delimit on the package version (i.e. <package>:<version,<package>:<version>)."
description: "All the pulled in packages and versions, including dependencies, that are installed. Represented as a sorted comma delimited list with equals delimit on the package version (i.e. <package1>=<version1>,<package2>=<version2>,...). Packages installed for a non-native architecture include it (i.e. <package>:<arch>=<version>)."
runs:
using: "node24"

View file

@ -45,14 +45,31 @@ export class ActionRunner {
}
/**
* Converts manifest entries into the action output CSV format.
* Converts package names into the action output CSV format.
*
* @param manifest Parsed manifest.
* @returns Comma-delimited name=version list.
* Arch is shown only for foreign-arch packages (e.g. `libc6:i386=2.39`), so
* native output matches the v1 format while multi-arch installs stay
* unambiguous. Entries are sorted with `PackageName.compareTo` so the output
* is stable across install order.
*
* @param packageNames Installed package names.
* @param nativeArch Runner architecture as reported by dpkg (e.g. amd64).
* @returns Comma-delimited name[:arch]=version list.
*/
private toCsv(manifest: Manifest): string {
return manifest.entries
.map((entry) => entry.packageName.serialize())
private toCsv(
packageNames: readonly PackageName[],
nativeArch: string,
): string {
return [...packageNames]
.sort((a, b) => a.compareTo(b))
.map((name) => {
const foreignArch =
name.arch !== undefined && name.arch !== nativeArch
? `:${name.arch}`
: "";
const version = name.version === undefined ? "" : `=${name.version}`;
return `${name.name}${foreignArch}${version}`;
})
.join(",");
}
@ -77,12 +94,14 @@ export class ActionRunner {
);
const cacheHit = manifest !== undefined;
// Needed on a cache hit too, to resolve the native arch for outputs.
const installManager = await createPackageManager(
true,
this.logger,
this.logger,
);
if (!cacheHit) {
const installManager = await createPackageManager(
true,
this.logger,
this.logger,
);
const packageInfos = await installManager.install(packageNames.toArray());
// Collect each package's installed files so the archives have content.
const entries = await Promise.all(
@ -98,10 +117,15 @@ export class ActionRunner {
await this.cache.archiveAndSave(manifest);
}
const nativeArch = await installManager.getNativeArchitecture();
const installed = manifest!.entries.map((entry) => entry.packageName);
return {
cacheHit,
packageVersionList: this.toCsv(manifest!),
allPackageVersionList: this.toCsv(manifest!),
packageVersionList: this.toCsv(
installed.filter((name) => packageNames.includes(name)),
nativeArch,
),
allPackageVersionList: this.toCsv(installed, nativeArch),
};
}
}

View file

@ -171,6 +171,25 @@ export class ActionPackageNames {
toArray(): readonly PackageName[] {
return this.items;
}
/**
* Checks whether an installed package was one of the requested packages.
*
* Matches on name, and on arch only when both sides specify one. Version is
* ignored because requests usually omit it and apt resolves the installed one.
*
* @param installed Package name as reported by apt after install.
* @returns True when the package was requested.
*/
includes(installed: PackageName): boolean {
return this.items.some(
(requested) =>
requested.name === installed.name &&
(requested.arch === undefined ||
installed.arch === undefined ||
requested.arch === installed.arch),
);
}
}
/**

View file

@ -15,6 +15,8 @@ vi.mock("ts-apt", async (importOriginal) => {
};
});
const NATIVE_ARCH = "amd64";
describe("ActionRunner", () => {
afterEach(() => {
vi.restoreAllMocks();
@ -45,6 +47,9 @@ describe("ActionRunner", () => {
const cacheKey = new CacheKey("v1", "0", process.arch, packageNames);
const restored = new Manifest(new Date(), [], cacheKey);
cache.loadAndRestore.mockResolvedValue(restored);
vi.mocked(createPackageManager).mockResolvedValue({
getNativeArchitecture: vi.fn().mockResolvedValue(NATIVE_ARCH),
} as never);
const result = await runner.runAction({
packages: "curl git",
@ -68,6 +73,7 @@ describe("ActionRunner", () => {
vi.mocked(createPackageManager).mockResolvedValue({
install: vi.fn().mockResolvedValue([{ name: installedPackage }]),
listInstalledFiles: vi.fn().mockResolvedValue([installedFile]),
getNativeArchitecture: vi.fn().mockResolvedValue(NATIVE_ARCH),
} as never);
await runner.runAction({

View file

@ -1,4 +1,4 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import {
runAction,
type ActionInputs,
@ -9,6 +9,19 @@ import { CacheKey } from "../src/cache.js";
import { createPackageName } from "ts-apt";
import { ActionPackageNames } from "../src/packages.js";
const NATIVE_ARCH = "amd64";
const FOREIGN_ARCH = "i386";
vi.mock("ts-apt", async (importOriginal) => {
const actual = await importOriginal<typeof import("ts-apt")>();
return {
...actual,
createPackageManager: async () => ({
getNativeArchitecture: async () => NATIVE_ARCH,
}),
};
});
describe("runAction", () => {
const PKG_NAME = "curl";
const PKG_VER = "1.2.3";
@ -71,4 +84,45 @@ describe("runAction", () => {
`${PKG_NAME}=${PKG_VER},${PKG2_NAME}=${PKG_VER},${PKG3_NAME}=${PKG_VER}`,
);
});
it("runAction cache hit with dependencies lists requested and all packages sorted with foreign arch only", async () => {
const inputs: ActionInputs = {
packages: `${PKG2_NAME} ${PKG_NAME}`,
version: PKG_VER,
executeInstallScripts: false,
emptyPackagesBehavior: "error",
debug: false,
};
const cacheKey = new CacheKey(
inputs.version,
"0",
process.arch,
ActionPackageNames.fromInput(inputs.packages),
);
// Install order, with a native-arch dependency and a foreign-arch request.
const manifest = new Manifest(
new Date(),
[
new ManifestEntry(createPackageName(PKG2_NAME, PKG_VER, FOREIGN_ARCH)),
new ManifestEntry(createPackageName(PKG3_NAME, PKG_VER, NATIVE_ARCH)),
new ManifestEntry(createPackageName(PKG_NAME, PKG_VER, undefined)),
],
cacheKey,
);
const mockCache = { loadAndRestore: async () => manifest };
const outputs = await runAction(
inputs,
{} as never,
mockCache as never,
{} as never,
);
expect(outputs.packageVersionList).toBe(
`${PKG_NAME}=${PKG_VER},${PKG2_NAME}:${FOREIGN_ARCH}=${PKG_VER}`,
);
expect(outputs.allPackageVersionList).toBe(
`${PKG_NAME}=${PKG_VER},${PKG2_NAME}:${FOREIGN_ARCH}=${PKG_VER},${PKG3_NAME}=${PKG_VER}`,
);
});
});

View file

@ -170,5 +170,18 @@ describe("packages", () => {
createPackageName("test-package", "1.0.0", "amd64"),
]);
});
it.each([
["same name without arch returns true", "xdot", createPackageName("xdot", "1.3-1"), true],
["requested without arch returns true", "xdot", createPackageName("xdot", "1.3-1", "amd64"), true],
["installed without arch returns true", "xdot:amd64", createPackageName("xdot", "1.3-1"), true],
["matching arch returns true", "xdot:amd64", createPackageName("xdot", "1.3-1", "amd64"), true],
["different arch returns false", "xdot:i386", createPackageName("xdot", "1.3-1", "amd64"), false],
["different name returns false", "xdot", createPackageName("rolldice", "1.16"), false],
])("includes %s", (_case, input, installed, expected) => {
expect(ActionPackageNames.fromInput(input).includes(installed)).toBe(
expected,
);
});
});
});