Files
renovate/lib/modules/manager/swift/artifacts.spec.ts
Bartosz Dolewski d735b9b6b8 fix(swift): support Package Registry pins in Package.resolved (#44358)
* fix(swift): allow Package.resolved pins without a `revision`

The swift manager's Package.resolved Zod schema requires
`state.revision: z.string()` on every pin. That is correct for
source-control pins (`kind: "remoteSourceControl"`), which always
have a git commit SHA, but wrong for SwiftPM Package Registry pins
(`kind: "registry"`) introduced by SE-0292: registry packages are
identified by a version alone and have no revision. As a result,
any Package.resolved that mixes source-control and registry pins
fails `PackageResolvedJson.safeParse`, `updateArtifacts` logs
`swift: could not parse Package.resolved` and returns `null`, and
Renovate silently stops writing lockfile updates on that repository.

This makes `state.revision` optional in the pin schema. The existing
`updateArtifacts` logic already guards the revision-replacement
branch with `if (newRevision)`, so registry pins are still safely
left untouched during source-control dep bumps.

Adds a regression test with a mixed-pins fixture. Before this change
that test panicked at parse time; after the change it demonstrates
the source-control pin gets updated and the registry pin is preserved
without gaining a spurious revision field.

Refs: #41534 (initial Package.resolved support, tested with
source-control pins only), #42303 (from: prefix strip).

* test(manager/swift): address review feedback on mixed-pins test

Per review on the Package.resolved mixed source-control/registry pins
test: inline the fixture into the test body, trim the comment to a
concise regression note, and assert on the parsed JSON with
toMatchObject instead of substring matching.

Co-Authored-By: Claude (Cricut Security policy) <noreply@anthropic.com>

* test(manager/swift): assert full updateArtifacts result for mixed pins

Per review: replace the parse/find/toMatchObject assertions with a
single resolves.toEqual on the whole updateArtifacts result, comparing
against the expected Package.resolved output directly.

Co-Authored-By: Claude (Cricut Security policy) <noreply@anthropic.com>

* test(manager/swift): trim mixed-pins regression comment

Apply review suggestion to shorten the comment to a single line.

Co-Authored-By: Claude (Cricut Security policy) <noreply@anthropic.com>

---------

Co-authored-by: Claude (Cricut Security policy) <noreply@anthropic.com>
2026-08-11 06:26:37 +00:00

957 lines
28 KiB
TypeScript

import { fs, logger, scm } from '~test/util.ts';
import { GitTagsDatasource } from '../../datasource/git-tags/index.ts';
import { GithubTagsDatasource } from '../../datasource/github-tags/index.ts';
import { GitlabTagsDatasource } from '../../datasource/gitlab-tags/index.ts';
import * as datasource from '../../datasource/index.ts';
import { updateArtifacts } from './artifacts.ts';
vi.mock('../../../util/fs/index.ts');
vi.mock('../../datasource/index.ts', async () => {
const actual = await vi.importActual<typeof datasource>(
'../../datasource/index.ts',
);
return {
...actual,
getDigest: vi.fn(),
};
});
const v2Fixture = JSON.stringify(
{
pins: [
{
identity: 'by-commit',
kind: 'remoteSourceControl',
location: 'https://github.com/example/by-commit',
state: {
revision: 'decaf',
},
},
{
identity: 'alamofire',
kind: 'remoteSourceControl',
location: 'https://github.com/Alamofire/Alamofire',
state: {
revision: 'f455c2975872ccd2d9c81594c658af65716e9b9a',
version: '5.9.1',
},
},
{
identity: 'swift-argument-parser',
kind: 'remoteSourceControl',
location: 'https://github.com/apple/swift-argument-parser.git',
state: {
revision: 'c8ed701b513cf5177118a175d85c4c06d8f9f97c',
version: '1.4.0',
},
},
],
version: 2,
},
null,
2,
);
const v3Fixture = JSON.stringify(
{
originHash: 'abc123def456',
pins: [
{
identity: 'alamofire',
kind: 'remoteSourceControl',
location: 'https://github.com/Alamofire/Alamofire',
state: {
revision: 'f455c2975872ccd2d9c81594c658af65716e9b9a',
version: '5.9.1',
},
},
],
version: 3,
},
null,
2,
);
describe('modules/manager/swift/artifacts', () => {
it('returns null when no Package.resolved files exist', async () => {
scm.getFileList.mockResolvedValue(['Package.swift', 'Sources/main.swift']);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('returns null when updatedDeps is empty', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('returns null for lockFileMaintenance', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: { isLockFileMaintenance: true },
});
expect(result).toBeNull();
});
it('returns null for unparseable JSON', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue('not valid json{{{');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('returns null for unsupported v1 format', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(
JSON.stringify({
object: { pins: [] },
version: 1,
}),
);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('updates a single pin version and revision', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newrevisionsha123');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
expect(contents).toContain('"revision": "newrevisionsha123"');
// Other pin unchanged
expect(contents).toContain('"version": "1.4.0"');
expect(contents).toContain(
'"revision": "c8ed701b513cf5177118a175d85c4c06d8f9f97c"',
);
});
it('parses lockfile with mixed source-control and registry pins', async () => {
// Regression test: SwiftPM Package Registry (SE-0292)
const v2MixedFixture = JSON.stringify(
{
pins: [
{
identity: 'alamofire',
kind: 'remoteSourceControl',
location: 'https://github.com/Alamofire/Alamofire',
state: {
revision: 'f455c2975872ccd2d9c81594c658af65716e9b9a',
version: '5.9.1',
},
},
{
identity: 'example.registry-package',
kind: 'registry',
location: '',
state: {
version: '1.2.3',
},
},
],
version: 2,
},
null,
2,
);
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2MixedFixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newrevisionsha123');
await expect(
updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
}),
).resolves.toEqual([
{
file: {
type: 'addition',
path: 'Package.resolved',
contents: JSON.stringify(
{
pins: [
{
identity: 'alamofire',
kind: 'remoteSourceControl',
location: 'https://github.com/Alamofire/Alamofire',
state: {
revision: 'newrevisionsha123',
version: '5.10.0',
},
},
{
identity: 'example.registry-package',
kind: 'registry',
location: '',
state: { version: '1.2.3' },
},
],
version: 2,
},
null,
2,
),
},
},
]);
});
it('does not write `from:` range to Package.resolved', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newrevisionsha123');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: '5.10.0',
newValue: 'from: "5.10.0"',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
expect(contents).not.toContain('from:');
});
it('updates multiple pins in one call', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest)
.mockResolvedValueOnce('alamofiresha')
.mockResolvedValueOnce('argparsersha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
{
depName: 'apple/swift-argument-parser',
datasource: GithubTagsDatasource.id,
newVersion: 'v1.5.0',
newValue: '1.5.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
expect(contents).toContain('"revision": "alamofiresha"');
expect(contents).toContain('"version": "1.5.0"');
expect(contents).toContain('"revision": "argparsersha"');
});
it('skips dep with no matching pin', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'nonexistent/package',
datasource: GithubTagsDatasource.id,
newVersion: 'v1.0.0',
newValue: '1.0.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('handles getDigest failure — updates version, keeps old revision', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue(null);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
// Revision unchanged since getDigest returned null
expect(contents).toContain(
'"revision": "f455c2975872ccd2d9c81594c658af65716e9b9a"',
);
});
it('updates multiple Package.resolved files', async () => {
scm.getFileList.mockResolvedValue([
'Package.resolved',
'App.xcworkspace/xcshareddata/swiftpm/Package.resolved',
]);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(2);
expect(result![0].file!.path).toBe('Package.resolved');
expect(result![1].file!.path).toBe(
'App.xcworkspace/xcshareddata/swiftpm/Package.resolved',
);
});
it('matches URL with .git suffix normalization', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
// swift-argument-parser has .git in the fixture location
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'apple/swift-argument-parser',
datasource: GithubTagsDatasource.id,
newVersion: 'v1.5.0',
newValue: '1.5.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "1.5.0"');
});
it('matches URL with trailing slash normalization', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const fixtureWithSlash = v2Fixture.replace(
'"https://github.com/Alamofire/Alamofire"',
'"https://github.com/Alamofire/Alamofire/"',
);
fs.readLocalFile.mockResolvedValue(fixtureWithSlash);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
});
it('matches URL case-insensitively', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'alamofire/alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
});
it('handles git-tags datasource (full URL as depName)', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'https://github.com/Alamofire/Alamofire.git',
datasource: GitTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
});
it('matches pin when Package.resolved uses SSH URL', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const sshFixture = v2Fixture.replace(
'"https://github.com/Alamofire/Alamofire"',
'"git@github.com:Alamofire/Alamofire.git"',
);
fs.readLocalFile.mockResolvedValue(sshFixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
});
it('matches pin with git-tags datasource using SSH URL as depName', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'git@github.com:Alamofire/Alamofire.git',
datasource: GitTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
});
it('matches pin when Package.resolved uses ssh:// URL', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const sshFixture = v2Fixture.replace(
'"https://github.com/Alamofire/Alamofire"',
'"ssh://git@github.com/Alamofire/Alamofire.git"',
);
fs.readLocalFile.mockResolvedValue(sshFixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
});
it('falls back to basic normalization for host-only URLs', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const hostOnlyFixture = JSON.stringify(
{
pins: [
{
identity: 'my-lib',
kind: 'remoteSourceControl',
location: 'https://custom-registry.example.com',
state: {
revision: 'oldsha',
version: '1.0.0',
},
},
],
version: 2,
},
null,
2,
);
fs.readLocalFile.mockResolvedValue(hostOnlyFixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
// No match expected since host-only URL won't match any dep
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'org/my-lib',
datasource: GithubTagsDatasource.id,
newVersion: 'v2.0.0',
newValue: '2.0.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('handles gitlab-tags with custom registryUrls', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
const gitlabFixture = JSON.stringify(
{
pins: [
{
identity: 'my-lib',
kind: 'remoteSourceControl',
location: 'https://gitlab.example.com/org/my-lib',
state: {
revision: 'oldsha',
version: '1.0.0',
},
},
],
version: 2,
},
null,
2,
);
fs.readLocalFile.mockResolvedValue(gitlabFixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'org/my-lib',
datasource: GitlabTagsDatasource.id,
registryUrls: ['https://gitlab.example.com'],
newVersion: 'v2.0.0',
newValue: '2.0.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "2.0.0"');
expect(contents).toContain('"revision": "newsha"');
});
it('uses dep.newDigest when already present', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
newDigest: 'precomputedsha456',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"revision": "precomputedsha456"');
// getDigest should not have been called
expect(datasource.getDigest).not.toHaveBeenCalled();
});
it('preserves v3 originHash', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v3Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"originHash": "abc123def456"');
expect(contents).toContain('"version": "5.10.0"');
});
it('returns null when pin is already up-to-date', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.9.1', // same as fixture
newValue: '5.9.1',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('preserves formatting in targeted replacement', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newsha');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
// Verify overall structure is preserved
expect(contents).toContain('"identity": "alamofire"');
expect(contents).toContain('"version": 2');
});
it('returns null when Package.resolved cannot be read', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(null);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('skips dep with no newValue', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
// no newValue
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('returns null when dep has no datasource or packageName', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
// no datasource — resolveCommitSha returns null
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
// Revision unchanged — no datasource to resolve SHA
expect(contents).toContain(
'"revision": "f455c2975872ccd2d9c81594c658af65716e9b9a"',
);
});
it('returns null when Package.resolved has no pins array', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(
JSON.stringify({ version: 2, somethingElse: true }),
);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
});
it('handles getDigest throwing an error', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockRejectedValue(
new Error('network error'),
);
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
const { contents } = result![0].file as { contents: string };
expect(contents).toContain('"version": "5.10.0"');
// Revision unchanged — getDigest threw
expect(contents).toContain(
'"revision": "f455c2975872ccd2d9c81594c658af65716e9b9a"',
);
});
it('if newValue is present, but newVersion is absent, no update is performed', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newrevisionsha123');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: undefined,
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toBeNull();
expect(logger.logger.debug).toHaveBeenCalledWith(
{
depName: 'Alamofire/Alamofire',
newVersion: undefined,
newValue: '5.10.0',
},
'swift: found a newValue but not a newVersion',
);
});
it('newValue is used to look up digest', async () => {
scm.getFileList.mockResolvedValue(['Package.resolved']);
fs.readLocalFile.mockResolvedValue(v2Fixture);
vi.mocked(datasource.getDigest).mockResolvedValue('newrevisionsha123');
const result = await updateArtifacts({
packageFileName: 'Package.swift',
updatedDeps: [
{
depName: 'Alamofire/Alamofire',
datasource: GithubTagsDatasource.id,
newVersion: 'v5.10.0',
newValue: '5.10.0',
},
],
newPackageFileContent: '',
config: {},
});
expect(result).toHaveLength(1);
// getDigest should have been called with the v-prefixed newVersion tag
expect(datasource.getDigest).toHaveBeenCalledWith(
expect.objectContaining({ datasource: GithubTagsDatasource.id }),
'v5.10.0',
);
});
});