mirror of
https://github.com/renovatebot/renovate.git
synced 2026-10-31 09:41:46 +00:00
d735b9b6b8
* 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>
957 lines
28 KiB
TypeScript
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',
|
|
);
|
|
});
|
|
});
|