Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
FROM ghcr.io/containerbase/devcontainer:14.6.14
FROM ghcr.io/containerbase/devcontainer:14.6.15
7 changes: 7 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,8 @@ jobs:
- setup-build
runs-on: ubuntu-latest
timeout-minutes: 7
permissions:
security-events: write

steps:
- name: Checkout code
Expand All @@ -379,6 +381,11 @@ jobs:
with:
args: -color -ignore "invalid activity type \"destroyed\" for \"merge_group\" Webhook event. available types are \"checks_requested\""

- name: Run zizmor
uses: zizmorcore/zizmor-action@71321a20a9ded102f6e9ce5718a2fcec2c4f70d8 # v0.5.2
with:
version: v1.23.1

test:
needs: [setup, prefetch]

Expand Down
4 changes: 2 additions & 2 deletions docs/usage/reading-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ If you're self-hosting or need to update private packages, complete the relevant
If you're new to Renovate, you should:

- Use the Mend Renovate App, or let someone else host Renovate for you
- Stick with the `config:recommended` preset
- Use the Dependency Dashboard (`config:recommended` enables it automatically)
- Stick with the [`config:recommended`](./presets-config.md#configrecommended) preset
- Use the Dependency Dashboard ([`config:recommended`](./presets-config.md#configrecommended) enables it automatically)
- Read the pages in the "Beginners" list
- Only create custom Renovate configuration when really needed

Expand Down
2 changes: 1 addition & 1 deletion lib/config/options/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -659,7 +659,7 @@ const options: Readonly<RenovateOptions>[] = [
description:
'Change this value to override the default Renovate sidecar image.',
type: 'string',
default: 'ghcr.io/renovatebot/base-image:13.33.9',
default: 'ghcr.io/renovatebot/base-image:13.33.10',
globalOnly: true,
deprecationMsg:
'The usage of `binarySource=docker` is deprecated, and will be removed in the future',
Expand Down
1 change: 1 addition & 0 deletions lib/config/presets/internal/workarounds.preset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@ export const presets: Record<string, Preset> = {
'adoptopenjdk',
'openjdk',
'java',
'java-jdk',
'java-jre',
'sapmachine',
'/^azul/zulu-openjdk/',
Expand Down
157 changes: 157 additions & 0 deletions lib/modules/manager/ant/extract.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
import { codeBlock } from 'common-tags';
import { fs } from '~test/util.ts';
import { extractAllPackageFiles, extractPackageFile } from './extract.ts';

vi.mock('../../../util/fs/index.ts');

describe('modules/manager/ant/extract', () => {
it('extracts inline version dependencies from build.xml', () => {
expect(
extractPackageFile(
codeBlock`
<project>
<artifact:dependencies>
<dependency groupId="junit" artifactId="junit" version="4.13.2" scope="test" />
</artifact:dependencies>
</project>
`,
'build.xml',
),
).toEqual({
deps: [
expect.objectContaining({
datasource: 'maven',
depName: 'junit:junit',
currentValue: '4.13.2',
depType: 'test',
registryUrls: [],
}),
],
});
});

it('extracts multiple dependencies', () => {
expect(
extractPackageFile(
codeBlock`
<project>
<artifact:dependencies>
<dependency groupId="junit" artifactId="junit" version="4.13.2" scope="test" />
<dependency groupId="org.slf4j" artifactId="slf4j-api" version="1.7.36" scope="compile" />
<dependency groupId="org.apache.commons" artifactId="commons-lang3" version="3.12.0" scope="runtime" />
</artifact:dependencies>
</project>
`,
'build.xml',
),
).toMatchObject({
deps: [
expect.objectContaining({
depName: 'junit:junit',
currentValue: '4.13.2',
depType: 'test',
}),
expect.objectContaining({
depName: 'org.slf4j:slf4j-api',
currentValue: '1.7.36',
depType: 'compile',
}),
expect.objectContaining({
depName: 'org.apache.commons:commons-lang3',
currentValue: '3.12.0',
depType: 'runtime',
}),
],
});
});

it('defaults depType to compile when no scope is set', () => {
expect(
extractPackageFile(
codeBlock`
<project>
<artifact:dependencies>
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`,
'build.xml',
),
).toEqual({
deps: [
expect.objectContaining({
depName: 'junit:junit',
depType: 'compile',
}),
],
});
});

it('returns null for invalid XML', () => {
expect(extractPackageFile('<<< not xml >>>', 'build.xml')).toBeNull();
});

it('returns null for build.xml with no dependencies', async () => {
fs.readLocalFile.mockResolvedValue(
'<project><target name="build" /></project>',
);

await expect(extractAllPackageFiles({}, ['build.xml'])).resolves.toBeNull();
});

it('ignores dependency nodes without version', () => {
expect(
extractPackageFile(
codeBlock`
<project>
<artifact:dependencies>
<dependency groupId="org.example" artifactId="lib" />
</artifact:dependencies>
</project>
`,
'build.xml',
),
).toBeNull();
});

it('extracts dependencies with single-quoted attributes', () => {
expect(
extractPackageFile(
"<project><artifact:dependencies><dependency groupId='junit' artifactId='junit' version='4.13.2' /></artifact:dependencies></project>",
'build.xml',
),
).toEqual({
deps: [
expect.objectContaining({
depName: 'junit:junit',
currentValue: '4.13.2',
}),
],
});
});

it('returns null for unreadable build.xml', async () => {
fs.readLocalFile.mockResolvedValue(null);

await expect(extractAllPackageFiles({}, ['build.xml'])).resolves.toBeNull();
});

it('does not revisit the same file', async () => {
let readCount = 0;
fs.readLocalFile.mockImplementation(() => {
readCount++;
return Promise.resolve(codeBlock`
<project>
<artifact:dependencies>
<dependency groupId="junit" artifactId="junit" version="4.13.2" />
</artifact:dependencies>
</project>
`);
});

const result = await extractAllPackageFiles({}, ['build.xml', 'build.xml']);

expect(result).toHaveLength(1);
expect(readCount).toBe(1);
});
});
125 changes: 125 additions & 0 deletions lib/modules/manager/ant/extract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import type { XmlElement } from 'xmldoc';
import { XmlDocument } from 'xmldoc';
import { logger } from '../../../logger/index.ts';
import { readLocalFile } from '../../../util/fs/index.ts';
import { MavenDatasource } from '../../datasource/maven/index.ts';
import { isXmlElement } from '../nuget/util.ts';
import type {
ExtractConfig,
PackageDependency,
PackageFile,
PackageFileContent,
} from '../types.ts';

const scopeNames = new Set([
'compile',
'runtime',
'test',
'provided',
'system',
]);

function getDependencyType(scope: string | undefined): string {
if (scope && scopeNames.has(scope)) {
return scope;
}
return 'compile';
}

function collectDependency(node: XmlElement): PackageDependency | null {
const { groupId, artifactId, version, scope } = node.attr;

if (!version || !groupId || !artifactId) {
return null;
}

return {
datasource: MavenDatasource.id,
depName: `${groupId}:${artifactId}`,
currentValue: version,
depType: getDependencyType(scope),
registryUrls: [],
};
}

function walkNode(
node: XmlElement | XmlDocument,
deps: PackageDependency[],
): void {
for (const child of node.children) {
if (!isXmlElement(child)) {
continue;
}

if (child.name === 'dependency') {
const dep = collectDependency(child);
if (dep) {
deps.push(dep);
}
} else {
walkNode(child, deps);
}
}
}

export function extractPackageFile(
content: string,
packageFile: string,
): PackageFileContent | null {
let doc: XmlDocument;
try {
doc = new XmlDocument(content);
} catch {
logger.debug(`ant manager: could not parse XML ${packageFile}`);
return null;
}

const deps: PackageDependency[] = [];
walkNode(doc, deps);

if (deps.length === 0) {
return null;
}

return { deps };
}

async function walkXmlFile(
packageFile: string,
visitedFiles: Set<string>,
): Promise<PackageFile | null> {
if (visitedFiles.has(packageFile)) {
return null;
}
visitedFiles.add(packageFile);

const content = await readLocalFile(packageFile, 'utf8');
if (!content) {
logger.debug(`ant manager: could not read ${packageFile}`);
return null;
}

const result = extractPackageFile(content, packageFile);
if (!result) {
return null;
}

return { packageFile, ...result };
}

export async function extractAllPackageFiles(
_config: ExtractConfig,
packageFiles: string[],
): Promise<PackageFile[] | null> {
const results: PackageFile[] = [];
const visitedFiles = new Set<string>();

for (const packageFile of packageFiles) {
const result = await walkXmlFile(packageFile, visitedFiles);
if (result) {
results.push(result);
}
}

return results.length > 0 ? results : null;
}
14 changes: 14 additions & 0 deletions lib/modules/manager/ant/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { Category } from '../../../constants/index.ts';
import { MavenDatasource } from '../../datasource/maven/index.ts';

export { extractAllPackageFiles, extractPackageFile } from './extract.ts';

export const displayName = 'Apache Ant';
export const url = 'https://ant.apache.org';
export const categories: Category[] = ['java'];

export const defaultConfig = {
managerFilePatterns: ['**/build.xml'],
};

export const supportedDatasources = [MavenDatasource.id];
2 changes: 2 additions & 0 deletions lib/modules/manager/ant/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Extracts Apache Ant dependencies from `build.xml` files that use the `maven-resolver-ant-tasks` library.
Dependencies are looked up using the Maven datasource.
2 changes: 2 additions & 0 deletions lib/modules/manager/api.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as ansible from './ansible/index.ts';
import * as ansibleGalaxy from './ansible-galaxy/index.ts';
import * as ant from './ant/index.ts';
import * as argoCD from './argocd/index.ts';
import * as asdf from './asdf/index.ts';
import * as azurePipelines from './azure-pipelines/index.ts';
Expand Down Expand Up @@ -113,6 +114,7 @@ import * as woodpecker from './woodpecker/index.ts';
const api = new Map<string, ManagerApi>();
export default api;

api.set('ant', ant);
api.set('ansible', ansible);
api.set('ansible-galaxy', ansibleGalaxy);
api.set('argocd', argoCD);
Expand Down
Loading
Loading