Ivy: Publishing & Multi-Project Builds

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page covers the producing side of Ivy: how a module becomes something other builds can depend on (deliver and publish), how several modules that depend on each other are built in the right order (buildlist with subant), how the published revision is chosen, how another project resolves what was published, and how to publish a POM beside the JAR so that Maven builds can use it (makepom). It is written against Apache Ivy 2.6.0 running in Apache Ant 1.10.18. Every example was run on 2026-10-16 on JDK 26 (macOS), with the Maven 3.10.0 distribution for the interoperability check. The work happened in a scratch directory with its own Ivy cache; paths in output were shortened to . and the machine name to host. Publishing goes only to build/repo inside that directory, a file:// style repository, never to a real server. The consuming side (resolving, retrieving, the cache) is on Ivy Ant Tasks; the descriptors are on Ivy: Module Descriptors & Configurations; resolvers and their patterns are on Ivy: Settings & Resolvers; version constraints and conflicts on Ivy: Conflicts & Versions. The repository and publishing ideas shared by all three build tools are on Repositories & Publishing Across Tools.

The scenario: three modules, one repository

The tidepool project is split into three modules that depend on each other in a line:

flowchart LR CLI["tidepool-cli"] -->|"compile, runtime"| ENG["tidepool-engine"] ENG -->|"compile, runtime"| MOD["tidepool-model"] ENG -->|"commons-csv 1.14.1"| CSV[("Maven Central")] CLI -->|"picocli 4.7.7"| CSV
.
├── build.xml                  root build: ordering and publishing every module
├── common.xml                 targets shared by the modules
├── ivysettings.xml            the repositories
├── data/stations.csv
├── tidepool-model/
│   ├── build.xml  ivy.xml  src/main/java/...
├── tidepool-engine/
│   ├── build.xml  ivy.xml  src/main/java/...
├── tidepool-cli/
│   ├── build.xml  ivy.xml  src/main/java/...
└── build/repo/                the file repository that publish writes to

Each module has an ivy.xml with two configurations, compile and runtime (which extends compile), one published JAR, and dependencies on its neighbour with a dynamic revision. The engine’s descriptor:

tidepool-engine/ivy.xml
<ivy-module version="2.0">
  <info organisation="io.tidepool" module="tidepool-engine" status="integration"/>
  <configurations>
    <conf name="compile"/>
    <conf name="runtime" extends="compile"/>
  </configurations>
  <publications>
    <artifact name="tidepool-engine" type="jar" conf="compile,runtime"/>
  </publications>
  <dependencies>
    <dependency org="io.tidepool" name="tidepool-model" rev="latest.integration" conf="compile->compile;runtime->runtime"/>
    <dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="compile->default"/>
  </dependencies>
</ivy-module>

The descriptor has no revision in info: the revision is decided at publication time (below). The model has no dependencies and the CLI depends on the engine and on picocli in the same way. The module’s build.xml only imports the shared targets:

tidepool-engine/build.xml
<project name="tidepool-engine" default="jar">
  <import file="../common.xml"/>
</project>
ivysettings.xml
<ivysettings>
  <properties environment="env"/>
  <property name="repo.dir" value="${ivy.settings.dir}/build/repo" override="false"/>
  <settings defaultResolver="chain"/>
  <caches defaultCacheDir="${ivy.settings.dir}/build/ivy-cache"/>
  <resolvers>
    <filesystem name="shared">
      <ivy pattern="${repo.dir}/shared/[organisation]/[module]/[revision]/ivy.xml"/>
      <artifact pattern="${repo.dir}/shared/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
    </filesystem>
    <filesystem name="m2" m2compatible="true">
      <ivy pattern="${repo.dir}/m2/[organisation]/[module]/[revision]/[module]-[revision].pom"/>
      <artifact pattern="${repo.dir}/m2/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
    </filesystem>
    <ibiblio name="central" m2compatible="true"/>
    <chain name="chain">
      <resolver ref="shared"/>
      <resolver ref="central"/>
    </chain>
  </resolvers>
</ivysettings>

shared is an Ivy-layout repository on the local disk: one directory per organisation, module and revision, with the ivy.xml next to the JAR. m2 is the same idea in Maven layout, used in the makepom section. Resolution looks in shared first and then in Maven Central, which is where commons-csv and picocli come from. Because the repository is a plain directory, "publish" in this page means "copy files into a directory tree", which is also what it means when that directory is a network share or the working copy of a repository server; a remote protocol only changes the resolver.

common.xml
<project name="common" xmlns:ivy="antlib:org.apache.ivy.ant">
  <dirname property="root.dir" file="${ant.file.common}"/>
  <property name="ivy.default.ivy.user.dir" location="${root.dir}/build/ivy-user"/>
  <property name="build.dir" location="build"/>
  <property name="classes.dir" location="${build.dir}/classes"/>
  <property name="dist.dir" location="${build.dir}/dist"/>
  <property name="status" value="integration"/>
  <property name="version.prefix" value="1.0."/>
  <property name="force" value="false"/>
  <property name="refresh" value="false"/>
  <property name="overwrite" value="false"/>

  <ivy:settings file="${root.dir}/ivysettings.xml"/>

  <target name="resolve" description="resolve the module's dependencies">
    <ivy:resolve file="ivy.xml" log="download-only" refresh="${refresh}"/>
  </target>

  <target name="compile" depends="resolve" description="compile the module">
    <ivy:cachepath pathid="compile.path" conf="compile"/>
    <mkdir dir="${classes.dir}"/>
    <javac srcdir="src/main/java" destdir="${classes.dir}" release="21" includeantruntime="false"
           classpathref="compile.path"/>
  </target>

  <target name="jar" depends="compile" description="package the module">
    <mkdir dir="${dist.dir}"/>
    <jar destfile="${dist.dir}/${ivy.module}.jar" basedir="${classes.dir}"/>
  </target>

  <target name="newversion" depends="resolve" unless="ivy.new.revision">
    <ivy:buildnumber organisation="${ivy.organisation}" module="${ivy.module}" revision="${version.prefix}"/>
  </target>

  <target name="publish" depends="jar, newversion" description="publish to the shared repository">
    <ivy:publish resolver="shared" pubrevision="${ivy.new.revision}" status="${status}"
                 forcedeliver="${force}" overwrite="${overwrite}"
                 artifactspattern="${dist.dir}/[artifact].[ext]"/>
    <echo>${ivy.module} published as ${ivy.new.revision}</echo>
  </target>

  <target name="clean"><delete dir="${build.dir}"/></target>
</project>

The shape follows the multi-project tutorial of the Ivy distribution: every module imports one common build file, modules differ only in their ivy.xml, and the compile class path comes straight from the cache with cachepath. The properties force, refresh and overwrite are switches that the examples flip on the command line (-Dforce=true); each is explained where it matters. The newversion target skips itself when ivy.new.revision is already set, which is how a release manager forces a revision with -Divy.new.revision=1.2.0.

One publication, step by step

For a single module the sequence that publish triggers, and the targets above implement, is:

flowchart TD A["resolve ivy.xml"] --> B["compile and jar"] B --> C["buildnumber: next revision from the repository"] C --> D["deliver: write a resolved ivy.xml with that revision"] D --> E["publish: copy artifacts and the delivered ivy.xml"] E --> F[("shared repository")]

buildnumber: choosing the revision

buildnumber (rules and attributes on Ivy Ant Tasks) asks the repository for the newest revision that starts with the prefix 1.0. and proposes the next one. With an empty repository the first proposal is 1.0.0; each later publication gets the next number (1.0.1, 1.0.2). Because the number comes from the repository, two developers publishing to the same repository get different revisions, and no revision is written into any source file. The tutorial puts a "target version" in a properties file and uses defaultBuildNumber and a separator for revisions like 1.0-dev-b1.

deliver: the resolved descriptor

deliver takes the descriptor of the module and the last resolve, and writes a resolved copy: the revision and status are filled in, variables are replaced, and every dynamic revision is replaced by the static one that the resolve found, with the original constraint kept in revConstraint. A module that was resolved on Monday with latest.integration and published as 1.0.2 therefore says rev="1.0.2", and a consumer who resolves it next year still gets the same dependency. Nothing is uploaded.

Attribute Effect

deliverpattern

Where the file is written; the default is ${ivy.deliver.ivy.pattern}, ${ivy.distrib.dir}/[type]s/[artifact]-[revision](-[classifier]).[ext].

pubrevision

The revision to put in the file. Default: ${ivy.deliver.revision}, else the resolved revision, else a timestamp.

pubbranch, pubdate

Branch and publication date (now or yyyyMMddHHmmss).

status

The status to write; default ${ivy.status} (the descriptor’s).

replacedynamicrev

true (default, since 1.3) replaces dynamic by static revisions. false keeps them, an exception to the rule that published descriptors should be fully resolved.

replaceForcedRev

Replace the revision of forced dependencies by the one the resolver forced (since 2.2, default false).

generateRevConstraint

Write revConstraint for replaced revisions (since 2.1, default true).

merge

When the descriptor extends a parent, merge the inherited content in and comment the extends out. Default true.

conf

Configurations to include; wildcards allowed (since 2.0). Default all.

delivertarget

Target to call for recursive delivery (below).

Three deliveries of the engine, from the same resolve, on the same day, with deliverpattern writing the files to different names:

tidepool-engine/deliver-demo.xml (excerpt)
<target name="deliver-static" depends="resolve">
  <ivy:deliver deliverpattern="${build.dir}/delivered/static-[revision].xml"
               pubrevision="1.1.0" status="milestone"/>
</target>

<target name="deliver-dynamic" depends="resolve">
  <ivy:deliver deliverpattern="${build.dir}/delivered/dynamic-[revision].xml"
               pubrevision="1.1.0" status="milestone" replacedynamicrev="false"/>
</target>

<target name="deliver-conf" depends="resolve">
  <ivy:deliver deliverpattern="${build.dir}/delivered/compile-only-[revision].xml"
               pubrevision="1.1.0" status="milestone" conf="compile"/>
</target>
deliver-static:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] 	delivering ivy file to ./tidepool-engine/build/delivered/static-1.1.0.xml
deliver-dynamic:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] 	delivering ivy file to ./tidepool-engine/build/delivered/dynamic-1.1.0.xml
deliver-conf:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:21 CEST 2026
[ivy:deliver] 	delivering ivy file to ./tidepool-engine/build/delivered/compile-only-1.1.0.xml

The relevant lines of the three files, side by side:

File info and artifact Dependency on the model

static-1.1.0.xml

revision="1.1.0" status="milestone", both configurations

rev="1.0.2" revConstraint="latest.integration"

dynamic-1.1.0.xml

same

rev="latest.integration" (no revConstraint, nothing was replaced)

compile-only-1.1.0.xml

same info; only the compile configuration; artifact ... conf="compile"

rev="1.0.2" revConstraint="latest.integration" conf="compile->compile"

Note the case of a milestone depending on an integration build: the delivered descriptor of a milestone still points to an integration revision of the model. That is the situation recursive delivery exists for.

Recursive delivery. With delivertarget, deliver calls the named target (through antcall) for each dependency whose status is lower than the status being delivered, with the properties dependency.name, dependency.version (the resolved revision), dependency.published.version and dependency.published.status. By default those last two are asked of the user with input; setting recursive.delivery.status and recursive.delivery.version answers for everybody. The target is where a build would deliver and publish the dependency (usually by calling the dependency’s own build file):

<target name="deliver-recursive" depends="resolve">
  <property name="recursive.delivery.status" value="release"/>
  <property name="recursive.delivery.version" value="2.0.0"/>
  <ivy:deliver deliverpattern="${build.dir}/delivered/recursive-[revision].xml"
               pubrevision="2.0.0" status="release" delivertarget="deliver-dependency"/>
</target>

<target name="deliver-dependency">
  <echo>recursive delivery: ${dependency.name} ${dependency.version} becomes ${dependency.published.version} (${dependency.published.status})</echo>
</target>
deliver-recursive:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-engine;working@host :: 2.0.0 :: release :: Fri Oct 16 12:23:21 CEST 2026

deliver-dependency:
     [echo] recursive delivery: tidepool-model 1.0.2 becomes 2.0.0 (release)
[ivy:deliver] 	delivering ivy file to ./tidepool-engine/build/delivered/recursive-2.0.0.xml

The delivered engine now says <dependency org="io.tidepool" name="tidepool-model" rev="2.0.0" revConstraint="latest.integration" .../>. The echo stands in for the work a real target does; that work (releasing model 2.0.0 first) was not run here, so that revision does not exist in the repository. When status is integration no recursive delivery happens at all.

Delivery is usually implicit. publish delivers by itself when it has no delivered file, and the manual’s advice is to call deliver yourself only when the delivered file must be edited or post-processed before upload (to add a source-control tag, for example), or when a recursive delivery has to be followed by a second resolve against the delivered file.

Source: Ivy: deliver

publish: uploading

publish copies the artifacts listed in the descriptor’s publications from artifactspattern into the resolver’s repository, together with the delivered ivy.xml, using the resolver named by resolver. It does not create the artifacts: a missing JAR fails the build (haltonmissing). The pattern may contain the tokens of the artifact ([artifact], [ext], [type], [conf]…​) and must be the pattern under which the build left the files; further patterns can be given with nested artifacts elements, and extra artifacts with nested artifact elements.

Attribute Effect

resolver

Required. The resolver, by name in the settings, that does the writing.

artifactspattern, srcivypattern

Where artifacts, and the Ivy file to publish, are found. The default of the second is the first.

pubrevision, pubbranch, status, pubdate

What to deliver, if a delivery is needed (as for deliver).

forcedeliver

Deliver again even when the delivered file exists. Default false.

update

Update revision, branch, publication date and status of the existing delivered file instead of delivering again.

publishivy

Publish the Ivy file (default true). false is for Maven-layout targets, below.

overwrite

Replace files for a revision that already exists. Default false.

replacedynamicrev, merge

As for deliver.

conf

Configurations to publish (wildcards since 2.2).

warnonmissing, haltonmissing

What to do with an artifact that is not where the pattern says. Both default to true.

validate, settingsRef

Validate the descriptor; the settings instance to use.

delivertarget

Recursive delivery, as above.

The first run of the root target publish-all (below) from an empty repository, for the model:

publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.0 :: integration :: Fri Oct 16 12:23:11 CEST 2026
[ivy:publish] 	delivering ivy file to ./tidepool-model/build/dist/ivy.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] 	published tidepool-model to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part/tidepool-model-1.0.0.jar
[ivy:publish] 	published ivy to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part/ivy.xml
[ivy:publish] 	publish committed: moved ./build/repo/shared/io.tidepool/tidepool-model/1.0.0.part
[ivy:publish] 		to ./build/repo/shared/io.tidepool/tidepool-model/1.0.0
     [echo] tidepool-model published as 1.0.0

The .part directory is the filesystem resolver’s atomic publish: the files are written into a directory next to the final one and the directory is renamed when everything is in place, so that a consumer never sees half a publication. After the three modules were published, the repository held, for each revision, the JAR and the ivy.xml plus .md5 and .sha1 checksum files of each (resolvers can check downloads against them; checksums and signatures across the three tools are on Supply Chain Security):

build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml
build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml.md5
build/repo/shared/io.tidepool/tidepool-model/1.0.0/ivy.xml.sha1
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar.md5
build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar.sha1
...

The ivy.checksums property (default sha1,md5) selects the algorithms. SHA-1 and MD5 are weak by today’s standards, so the checksums protect against truncated and corrupted transfers, not against tampering.

Source: Ivy: publish

The delivered file in the repository

The engine’s published ivy.xml (revision 1.0.0, before any other revision existed) is the delivered file:

<?xml version="1.0" encoding="UTF-8"?>
<ivy-module version="2.0">
  <info organisation="io.tidepool" module="tidepool-engine" revision="1.0.0" status="integration" publication="20261016121709"/>
  <configurations>
    <conf name="compile"/>
    <conf name="runtime" extends="compile"/>
  </configurations>
  <publications>
    <artifact name="tidepool-engine" type="jar" conf="compile,runtime"/>
  </publications>
  <dependencies>
    <dependency org="io.tidepool" name="tidepool-model" rev="1.0.0" revConstraint="latest.integration" conf="compile->compile;runtime->runtime"/>
    <dependency org="org.apache.commons" name="commons-csv" rev="1.14.1" conf="compile->default"/>
  </dependencies>
</ivy-module>

This is what any other project sees when it depends on tidepool-engine;1.0.0: the model is pinned to 1.0.0, and the original wish ("the latest integration build") is recorded for people.

Source: Ivy: deliver

Publishing the same module twice

Three things go wrong when a build is run a second time, and each produces an error that sends people to the manual.

A stale delivered file. publish delivers only if the delivered file does not exist yet. On the second publish-all, the file from the first run is still in build/dist:

publish:
[ivy:publish] :: publishing :: io.tidepool#tidepool-model

BUILD FAILED
./build.xml:13: The following error occurred while executing this line:
./common.xml:35: impossible to publish artifacts for io.tidepool#tidepool-model;working@host: java.lang.IllegalArgumentException: cannot publish ./tidepool-model/build/dist/ivy.xml as 1.0.1: bad revision found in ivy file (Revision: 1.0.0). Use forcedeliver or update.

buildnumber proposed 1.0.1, the leftover file says 1.0.0. Either clean build/ first or set forcedeliver="true" (the force property in common.xml, run as ant -Dforce=true publish-all), which delivers again every time. A tutorial build file uses forcedeliver="true" in its local publish target for the same reason.

An existing revision. Publishing a revision that is already in the repository is refused, so that a published revision can be trusted not to change:

publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.2 :: integration :: Fri Oct 16 12:23:17 CEST 2026
[ivy:publish] 	delivering ivy file to ./tidepool-model/build/dist/ivy.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model

BUILD FAILED
./common.xml:35: impossible to publish artifacts for io.tidepool#tidepool-model;working@host: java.io.IOException: file copy not done from ./tidepool-model/build/dist/tidepool-model.jar to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/tidepool-model-1.0.2.jar: destination already exists and overwrite is false

(That was ant -Dforce=true -Divy.new.revision=1.0.2 publish in tidepool-model after 1.0.2 had been published.) With overwrite="true" (here -Doverwrite=true) the same command succeeds, and, as the output shows, the files go straight to the final directory without the .part step:

[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] 	published tidepool-model to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/tidepool-model-1.0.2.jar
[ivy:publish] 	published ivy to ./build/repo/shared/io.tidepool/tidepool-model/1.0.2/ivy.xml
     [echo] tidepool-model published as 1.0.2

Overwriting is meant for integration builds in a private repository. A consumer’s cache will not notice that a published revision changed: it keeps the copy it downloaded unless the dependency is marked changing.

A stale dynamic revision. Ivy remembers what a dynamic revision resolved to for a time to live, by default ${ivy.cache.ttl.default}, which is 10s in the ivy.properties file inside the 2.6.0 JAR. A build that publishes the model and then, seconds later, resolves latest.integration for the engine can therefore still see the old model. Running publish-all right after another one:

$ grep tidepool- tidepool-engine/build/dist/ivy.xml tidepool-cli/build/dist/ivy.xml    # after ant -Dforce=true publish-all
tidepool-engine/build/dist/ivy.xml:    <dependency org="io.tidepool" name="tidepool-model" rev="1.0.0" revConstraint="latest.integration" ...
tidepool-cli/build/dist/ivy.xml:    <dependency org="io.tidepool" name="tidepool-engine" rev="1.0.0" revConstraint="latest.integration" ...

The engine and the CLI published as 1.0.1 pin the model and the engine at 1.0.0, although 1.0.1 of each had just been published, because their resolve reused the answer that was a second old. Running it again with the refresh switch (resolve has refresh="true", which forces dynamic revisions to be recomputed) fixes it:

$ ant -Dforce=true -Drefresh=true publish-all
$ grep tidepool- tidepool-engine/build/dist/ivy.xml tidepool-cli/build/dist/ivy.xml
tidepool-engine/build/dist/ivy.xml:    <dependency org="io.tidepool" name="tidepool-model" rev="1.0.2" revConstraint="latest.integration" ...
tidepool-cli/build/dist/ivy.xml:    <dependency org="io.tidepool" name="tidepool-engine" rev="1.0.2" revConstraint="latest.integration" ...

A build server that publishes a whole chain should set refresh="true" on the resolve of the publishing build, or set a ttl of 0ms for the organisation (the property ivy.cache.ttl.default in the settings does that for everything); the ttl elements are described on Ivy: Settings & Resolvers.

Several modules at once

buildlist

buildlist takes a fileset of build files, looks for an ivy.xml beside each one (the attribute ivyfilepath changes where), reads the declared dependencies between them and returns an Ant path of the build files in dependency order: a module comes after the modules it depends on, which is exactly the order a reactor-less build needs. It also sets ivy.sorted.modules, a comma-separated list of the module names, which is handy in a log. With revisions in the info elements, the revisions are taken into account (a dependency on a revision that does not match is reported and treated as a different module); with none, as here, they are ignored.

Attribute or element Effect

reference

Required. The id of the path to create.

ivyfilepath

Relative path from the build file to its descriptor; default ${ivy.buildlist.ivyfilepath}, ivy.xml.

root, excluderoot

Only the modules that the named module(s) depend on, directly or transitively (comma-separated names since 2.0); and whether to leave the roots out.

leaf, excludeleaf

Only the modules that depend on the named module(s) (since 1.4.1), and whether to leave them out.

onlydirectdep

Restrict the result to direct dependencies of the roots, or direct dependents of the leaves.

reverse

Most dependent module first.

restartFrom

Start the list at the named module, followed by everything after it (since 2.0), for resuming a failed build.

onMissingDescriptor

For build files with no descriptor: head (default), tail, skip, warn or fail. The older skipbuildwithoutivy is deprecated.

haltonerror

Stop on an invalid descriptor. Default true.

nested root and leaf (since 2.5)

Name modules with organisation, module, revision, branch or a file.

Circular dependencies are tolerated: the modules in a cycle are kept together and placed so that every outside dependency comes first. The task works on the descriptors only, without resolving anything.

The root build.xml, which every example uses to order and run the modules:

build.xml
<project name="tidepool" default="publish-all" xmlns:ivy="antlib:org.apache.ivy.ant">
  <property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
  <ivy:settings file="ivysettings.xml"/>

  <target name="build-list" description="order the modules by their dependencies">
    <ivy:buildlist reference="build-path">
      <fileset dir="." includes="tidepool-*/build.xml"/>
    </ivy:buildlist>
    <echo>build order: ${ivy.sorted.modules}</echo>
  </target>

  <target name="publish-all" depends="build-list" description="build and publish every module in order">
    <subant target="publish" buildpathref="build-path" inheritall="false"/>
  </target>

  <target name="clean-all" depends="build-list">
    <subant target="clean" buildpathref="build-path" inheritall="false"/>
  </target>
</project>
$ ant -lib lib/ivy-2.6.0.jar build-list
Buildfile: ./build.xml

build-list:
[ivy:buildlist] :: Apache Ivy 2.6.0 - 20260712075753 :: https://ant.apache.org/ivy/ ::
[ivy:buildlist] :: loading settings :: file = ./ivysettings.xml
     [echo] build order: io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli

The fileset delivers tidepool-cli, tidepool-engine, tidepool-model (alphabetical order); the descriptors turn that into model, engine, CLI. The subant task then runs the publish target in each build file of the path, in that order, in a separate Ant project each (inheritall="false" keeps the root’s properties out, so each module picks up its own). The same list, with the options from the table, on the same three modules:

<ivy:buildlist reference="p.root" root="tidepool-engine"><fileset dir="." includes="tidepool-*/build.xml"/></ivy:buildlist>
<echo>root=engine        : ${ivy.sorted.modules}</echo>
     [echo] default            : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
     [echo] reverse            : io.tidepool#tidepool-cli, io.tidepool#tidepool-engine, io.tidepool#tidepool-model
     [echo] root=engine        : io.tidepool#tidepool-model, io.tidepool#tidepool-engine
     [echo] root=engine, excl. : io.tidepool#tidepool-model
     [echo] leaf=model         : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
     [echo] leaf=model, direct : io.tidepool#tidepool-model, io.tidepool#tidepool-engine
     [echo] restartFrom=engine : io.tidepool#tidepool-engine, io.tidepool#tidepool-cli
     [echo] nested root=cli    : io.tidepool#tidepool-model, io.tidepool#tidepool-engine, io.tidepool#tidepool-cli

In build terms: root answers "what must I build to build this module?", leaf answers "what must I rebuild when this module changes?" (the model’s change reaches the engine and the CLI, and with onlydirectdep only the engine), reverse is the order for a clean-up or an uninstall, restartFrom resumes a failed publish-all at the module that failed. The ivy.sorted.modules property contains only modules that have a descriptor.

Build files without a descriptor. A directory with a build.xml but no ivy.xml (documentation, say) is placed according to onMissingDescriptor. In a scratch directory with alpha, beta, gamma (alpha and beta depend on each other, gamma on beta) and docs (no descriptor), the paths came out as:

     [echo] head : docs, beta, alpha, gamma
     [echo] tail : beta, alpha, gamma, docs
     [echo] skip : beta, alpha, gamma

and with onMissingDescriptor="fail":

BUILD FAILED
./circular/build.xml:12: a module has no module descriptor and onMissingDescriptor=fail. Build file: ./circular/docs/build.xml. Expected descriptor: ./circular/docs/ivy.xml

The cycle alpha, beta stays together (beta, alpha), and gamma, which depends on it, comes after. The ordering tolerates a cycle that resolution may not: whether a resolve of a circular dependency is allowed is decided by the circularDependencyStrategy of the settings, covered on Ivy: Conflicts & Versions.

Source: Ivy: buildlist

subant and import

Ordering is only half of a multi-module build; the other half is running something in each module. subant runs a target in each build file of a path (or a fileset), once per file, in the order of the path; with genericantfile it runs the same build file in each directory; failonerror="false" continues past a failing module. The multi-file chapter, Multi-File Builds: import, include & subant, describes import, include and subant in detail; the pattern used here (a common.xml imported by each module, an ordered subant in the root) is the one of the Ivy tutorial, and the Maven and Gradle equivalents are the reactor (see Comparing Ant, Maven & Gradle). The root build declares its own ivy:settings, and each module’s common.xml declares one too: with inheritall="false" every subant project loads its own instance. The manual warns that a settings instance shared with a sub-build through a reference must be created by the same class loader, which is a risk when the Ivy antlib is loaded by a typedef in each build; with -lib (as here) or Ivy in Ant’s lib directory there is a single class loader and the problem does not arise.

Publishing the whole chain

The first ant publish-all, from an empty repository, built and published the model, then the engine, then the CLI, each resolving what the previous one had just put in the repository. The engine’s resolve, as it appears in the log (the CLI’s is alike):

resolve:
[ivy:resolve] :: loading settings :: file = ./ivysettings.xml
[ivy:resolve] downloading ./build/repo/shared/io.tidepool/tidepool-model/1.0.0/tidepool-model-1.0.0.jar ...
[ivy:resolve] .. (3kB)
[ivy:resolve] .. (0kB)
[ivy:resolve] 	[SUCCESSFUL ] io.tidepool#tidepool-model;1.0.0!tidepool-model.jar (2ms)
[ivy:resolve] downloading https://repo1.maven.org/maven2/org/apache/commons/commons-csv/1.14.1/commons-csv-1.14.1.jar ...
...

The "download" of a local file is the copy from the file repository into the cache. Run without the -Dforce=true switch the second publish-all fails as shown earlier; run with it (and -Drefresh=true when the chain was published a few seconds before) it produces 1.0.1 or 1.0.2 for all three. The result after three rounds (all using the same repository) is one directory per module and revision under build/repo/shared/io.tidepool, with the CLI’s ivy.xml pointing at the engine revision that was current when it was delivered.

Using what was published

A project that is not part of the build uses the published CLI like any other library. Its descriptor asks for the newest integration build:

consumer/ivy.xml
<ivy-module version="2.0">
  <info organisation="io.tidepool.demo" module="consumer"/>
  <configurations>
    <conf name="runtime"/>
  </configurations>
  <dependencies>
    <dependency org="io.tidepool" name="tidepool-cli" rev="latest.integration" conf="runtime->runtime"/>
  </dependencies>
</ivy-module>

with its own settings: a chain of the file repository and Maven Central, and its own empty cache:

consumer/consumer-settings.xml
<ivysettings>
  <settings defaultResolver="shared"/>
  <caches defaultCacheDir="${cache.dir}"/>
  <resolvers>
    <chain name="shared">
      <filesystem name="local-repo">
        <ivy pattern="${ivy.settings.dir}/../build/repo/shared/[organisation]/[module]/[revision]/ivy.xml"/>
        <artifact pattern="${ivy.settings.dir}/../build/repo/shared/[organisation]/[module]/[revision]/[artifact]-[revision].[ext]"/>
      </filesystem>
      <ibiblio name="central" m2compatible="true"/>
    </chain>
  </resolvers>
</ivysettings>
consumer/build.xml
<project name="consumer" default="run" xmlns:ivy="antlib:org.apache.ivy.ant">
  <property name="ivy.default.ivy.user.dir" location="../build/ivy-user"/>
  <property name="cache.dir" location="build/consumer-cache"/>
  <property name="data.file" location="../data/stations.csv"/>
  <ivy:settings file="consumer-settings.xml"/>

  <target name="resolve">
    <ivy:resolve file="ivy.xml"/>
  </target>

  <target name="run" depends="resolve">
    <ivy:cachepath pathid="run.path" conf="runtime"/>
    <java classname="io.tidepool.cli.Main" classpathref="run.path" fork="true" failonerror="true">
      <arg value="${data.file}"/>
      <arg value="SF"/>
    </java>
  </target>
</project>
$ ant -lib lib/ivy-2.6.0.jar run
resolve:
[ivy:resolve] :: Apache Ivy 2.6.0 - 20260712075753 :: https://ant.apache.org/ivy/ ::
[ivy:resolve] :: loading settings :: file = ./consumer/consumer-settings.xml
[ivy:resolve] :: resolving dependencies :: io.tidepool.demo#consumer;working@host
[ivy:resolve] 	confs: [runtime]
[ivy:resolve] 	found io.tidepool#tidepool-cli;1.0.2 in local-repo
[ivy:resolve] 	[1.0.2] io.tidepool#tidepool-cli;latest.integration
[ivy:resolve] 	found io.tidepool#tidepool-engine;1.0.2 in local-repo
[ivy:resolve] 	found io.tidepool#tidepool-model;1.0.2 in local-repo
[ivy:resolve] 	found org.apache.commons#commons-csv;1.14.1 in central
[ivy:resolve] 	found commons-io#commons-io;2.20.0 in central
[ivy:resolve] 	found commons-codec#commons-codec;1.19.0 in central
[ivy:resolve] 	found info.picocli#picocli;4.7.7 in central
...
[ivy:resolve] :: resolution report :: resolve 1876ms :: artifacts dl 573ms
	---------------------------------------------------------------------
	|                  |            modules            ||   artifacts   |
	|       conf       | number| search|dwnlded|evicted|| number|dwnlded|
	---------------------------------------------------------------------
	|      runtime     |   7   |   7   |   7   |   0   ||   7   |   7   |
	---------------------------------------------------------------------

run:
     [java] San Francisco: -0,869 m

Seven modules were resolved from two repositories: the three tidepool modules from the file repository and Commons CSV, Commons IO, Commons Codec and picocli from Maven Central; the last three come in through Commons CSV’s own POM. The program printed a tide height (the decimal comma is the formatting of the machine’s Spanish locale). The point of the run is the found ... in local-repo lines: the engine and the model were not declared by the consumer, they came in transitively through the published ivy.xml files, and at the revisions that were pinned when they were delivered.

Integration versions and statuses

The Ivy documentation distinguishes two ways of dealing with integration builds: publish them all under the same, special revision (the "snapshot" convention, which the dependency must flag with changing), or give each a new revision, from a build number or a timestamp, and let consumers ask for latest.integration. Ivy recommends the second way because a revision then identifies exactly one build, which keeps even integration builds reproducible and lets an integration build be promoted to a milestone or a release. This page does the second way.

A module has a status (integration, milestone, release by default), set in the descriptor and overridden by publish/deliver. A dependency written as latest.<status> means "the newest revision whose status is at least that good". With the repository holding 1.0.0, 1.0.1 and 1.0.2 (integration), 1.1.0 published with -Dstatus=milestone and 1.1.1 published as an integration build:

statuses.xml
<project name="statuses" default="all" xmlns:ivy="antlib:org.apache.ivy.ant">
  <property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
  <property name="ivy.cache.ttl.default" value="0ms"/>
  <ivy:settings file="ivysettings.xml"/>

  <macrodef name="latest">
    <attribute name="constraint"/>
    <sequential>
      <ivy:findrevision organisation="io.tidepool" module="tidepool-model" revision="@{constraint}" property="found.@{constraint}"/>
      <echo>@{constraint} -> ${found.@{constraint}}</echo>
    </sequential>
  </macrodef>

  <target name="all">
    <latest constraint="latest.integration"/>
    <latest constraint="latest.milestone"/>
    <latest constraint="latest.release"/>
    <latest constraint="1.0.+"/>
    <latest constraint="[1.0,1.1)"/>
  </target>
</project>
     [echo] latest.integration -> 1.1.1
     [echo] latest.milestone -> 1.1.0
     [echo] latest.release -> ${found.latest.release}
     [echo] 1.0.+ -> 1.0.2
     [echo] [1.0,1.1) -> 1.0.2

latest.integration takes the newest build whatever its status; latest.milestone skips 1.1.1 because its status is lower than milestone; latest.release finds nothing, as no revision has status release, and the property is left unset (Ant shows the unexpanded ${found.latest.release}). A latest.release dependency on a module that was never released therefore fails to resolve rather than falling back to something less stable. The last two lines are a sub-revision and a range, from the matchers described on Ivy: Conflicts & Versions. The ivy.cache.ttl.default of 0ms in the build file is there because of the stale-answer behaviour shown earlier.

A milestone is published from the same publish target with a different status and revision:

$ ant -Dforce=true -Divy.new.revision=1.1.0 -Dstatus=milestone publish
publish:
[ivy:publish] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.1.0 :: milestone :: Fri Oct 16 12:23:27 CEST 2026
...
     [echo] tidepool-model published as 1.1.0

Promoting an integration build to a better status is not something Ivy does by itself: the best-practices page says that Ivy is a dependency manager, not a build tool, and that publishing only distinctly named versions, replacing constraints with static revisions at publication, and using recursive delivery is what gives reproducible builds. It also points out the cost of this approach, a lot of intermediate revisions that a cleaning script has to remove from the repository.

Looking into a repository

Two tasks work on the repository rather than on a module, and need a resolver that can list its contents. An Ivy-layout file repository can; a Maven-layout one gives unreliable answers (see the note on listmodules on Ivy Ant Tasks).

repo-tools.xml
<project name="repo-tools" default="all" xmlns:ivy="antlib:org.apache.ivy.ant">
  <property name="ivy.default.ivy.user.dir" location="build/ivy-user"/>
  <ivy:settings file="ivysettings.xml"/>

  <target name="listmodules">
    <ivy:listmodules organisation="io.tidepool" module="tidepool-.*" revision="1\.0\..*" resolver="shared"
                     property="found.[module].[revision]" value="[organisation]"/>
    <echoproperties prefix="found."/>
  </target>

  <target name="repreport">
    <ivy:repreport organisation="io.tidepool" module="tidepool-.*" revision="1.0.+"
                   todir="build/repreport" dot="true" xml="true" graph="false"/>
    <loadfile srcfile="build/repreport/ivy-repository-report.dot" property="dot"/>
    <echo>${dot}</echo>
  </target>
</project>
listmodules:
[echoproperties] found.tidepool-cli.1.0.0=io.tidepool
[echoproperties] found.tidepool-cli.1.0.1=io.tidepool
[echoproperties] found.tidepool-cli.1.0.2=io.tidepool
[echoproperties] found.tidepool-engine.1.0.0=io.tidepool
[echoproperties] found.tidepool-engine.1.0.1=io.tidepool
[echoproperties] found.tidepool-engine.1.0.2=io.tidepool
[echoproperties] found.tidepool-model.1.0.0=io.tidepool
[echoproperties] found.tidepool-model.1.0.1=io.tidepool
[echoproperties] found.tidepool-model.1.0.2=io.tidepool

listmodules found the nine 1.0.x revisions (the 1.1.x ones were excluded by the revision pattern) with the default exactOrRegexp matcher; property and value are patterns built from [organisation], [module], [branch] and [revision]. repreport resolves the matching modules and everything they depend on, Maven Central included, and writes a report of the dependencies among them, as XML by default, GraphML (graph) or Graphviz DOT (dot). The DOT output for the 1.0.+ modules, after running the task:

/* * directed graph dot input file. * * generated by ivy report */
            digraph G {"tidepool-cli""picocli""tidepool-engine""commons-csv""commons-codec""commons-io""tidepool-model""tidepool-cli" -> "picocli";"tidepool-cli" -> "tidepool-engine";"tidepool-engine" -> "commons-csv";"commons-csv" -> "commons-codec";"commons-csv" -> "commons-io";"tidepool-engine" -> "tidepool-model";}

(The whitespace is as Ivy wrote it; Graphviz does not mind.) The revision of repreport is a constraint, not a pattern: my first attempt with the regular expression 1\.0\.2 was taken literally and reported the modules "not found".

Source: Ivy: repreport

Maven interoperability: makepom

Maven cannot read an ivy.xml. To let a Maven (or Gradle) project depend on a module that Ivy publishes, publish a POM next to the JAR in a Maven-layout repository. makepom converts a descriptor into a POM:

Attribute or element Effect

ivyfile, pomfile

Required. The descriptor to convert and the POM to write.

conf

Configurations to include (wildcards, since 2.2). Default all.

artifactName, artifactPackaging

The artifact the POM describes: its artifactId (default the module name) and packaging (default the type of the artifact, pom if there is none).

templatefile

A POM template (since 2.2). Lines with SKIP_LINE are dropped; ${ivy.pom.groupId}, ${ivy.pom.artifactId}, ${ivy.pom.version}, ${ivy.pom.packaging}, ${ivy.pom.name}, ${ivy.pom.description}, ${ivy.pom.url}, ${ivy.pom.license} and ${ivy.pom.header} are filled in.

description, headerFile, printIvyInfo

The description; a file whose content becomes the header (a licence); whether to add the Ivy banner. Default true.

nested mapping

conf and scope: which POM scope a configuration becomes.

nested dependency

Extra dependencies to add: group, artifact, version, type, classifier, scope, optional.

Not everything in an Ivy descriptor has a POM equivalent (the manual says so), and the conversion is therefore best done on the delivered descriptor, where revisions are static. The module-level target, which delivers, converts and publishes the JAR and the POM to the m2 resolver:

<target name="publish-m2" depends="jar" description="publish a POM and the JAR in Maven layout">
  <property name="m2.revision" value="1.0.2"/>
  <ivy:deliver deliverpattern="${dist.dir}/ivy-[revision].xml" pubrevision="${m2.revision}" status="release"/>
  <ivy:resolve file="${dist.dir}/ivy-${m2.revision}.xml" log="quiet"/>
  <ivy:makepom ivyfile="${dist.dir}/ivy-${m2.revision}.xml" pomfile="${dist.dir}/${ivy.module}.pom"
               description="Tidepool ${ivy.module}">
    <mapping conf="compile" scope="compile"/>
    <mapping conf="runtime" scope="runtime"/>
  </ivy:makepom>
  <ivy:publish resolver="m2" pubrevision="${m2.revision}" publishivy="false"
               artifactspattern="${dist.dir}/[artifact].[ext]">
    <artifact name="${ivy.module}" ext="pom" type="pom"/>
  </ivy:publish>
</target>

Three points about this target. The ivy:resolve of the delivered file is not decoration: without it publish does not know the module at the new revision and fails, with publishivy="false", either with Ivy file not found in cache for ...;1.0.2! (when the revision attribute is given) or by trying to move a directory called working@host. publishivy="false" is what keeps the Ivy descriptor out of the Maven repository, where the POM takes its place at [artifact]-[revision].pom; the nested artifact adds the POM to the list of files to publish. And status="release" in the delivery is a statement about the publication, not a check that the dependencies are releases.

Run for the three modules from the root (the target publish-m2-all is a subant of publish-m2 over the ordered path, with -Dm2.revision=1.0.2):

publish-m2:
[ivy:deliver] :: delivering :: io.tidepool#tidepool-model;working@host :: 1.0.2 :: release :: Fri Oct 16 12:22:12 CEST 2026
[ivy:deliver] 	delivering ivy file to ./tidepool-model/build/dist/ivy-1.0.2.xml
[ivy:publish] :: publishing :: io.tidepool#tidepool-model
[ivy:publish] 	published tidepool-model to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part/tidepool-model-1.0.2.jar
[ivy:publish] 	published tidepool-model to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part/tidepool-model-1.0.2.pom
[ivy:publish] 	publish committed: moved ./build/repo/m2/io/tidepool/tidepool-model/1.0.2.part
[ivy:publish] 		to ./build/repo/m2/io/tidepool/tidepool-model/1.0.2

The repository build/repo/m2 then holds io/tidepool/<module>/1.0.2/<module>-1.0.2.jar and .pom (plus checksums) for the three modules: the group io.tidepool became directories because the resolver is m2compatible. The generated POM of the engine:

<?xml version="1.0" encoding="UTF-8"?>
<!--
   Apache Maven 2 POM generated by Apache Ivy
   https://ant.apache.org/ivy/
   Apache Ivy version: 2.6.0 20260712075753
-->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool</groupId>
  <artifactId>tidepool-engine</artifactId>
  <packaging>jar</packaging>
  <version>1.0.2</version>
  <description>Tidepool tidepool-engine</description>
  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-model</artifactId>
      <version>1.0.2</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-csv</artifactId>
      <version>1.14.1</version>
      <scope>compile</scope>
    </dependency>
  </dependencies>
</project>

Both the Ivy dependency on the model (mapped from compile->compile;runtime->runtime) and the one on Commons CSV became compile dependencies, because the engine’s compile configuration is where both are declared and the engine’s runtime adds none of its own. Information that exists in an ivy.xml but not in a POM (a configuration with no scope, revConstraint, conflict managers) is lost, which is why the POM "may not resolve to the exact same dependencies".

Does Maven accept it? A Maven project outside Ivy’s world declares the file repository and the CLI:

mvn-consumer/pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0">
  <modelVersion>4.0.0</modelVersion>
  <groupId>io.tidepool.demo</groupId>
  <artifactId>maven-consumer</artifactId>
  <version>1.0.0</version>

  <repositories>
    <repository>
      <id>ivy-published</id>
      <url>file://${project.basedir}/../build/repo/m2</url>
    </repository>
  </repositories>

  <dependencies>
    <dependency>
      <groupId>io.tidepool</groupId>
      <artifactId>tidepool-cli</artifactId>
      <version>1.0.2</version>
    </dependency>
  </dependencies>
</project>
$ mvn -B -Dmaven.repo.local=./repo dependency:tree
...
[INFO] --- dependency:3.11.0:tree (default-cli) @ maven-consumer ---
[INFO] io.tidepool.demo:maven-consumer:jar:1.0.0
[INFO] \- io.tidepool:tidepool-cli:jar:1.0.2:compile
[INFO]    +- io.tidepool:tidepool-engine:jar:1.0.2:compile
[INFO]    |  +- io.tidepool:tidepool-model:jar:1.0.2:compile
[INFO]    |  \- org.apache.commons:commons-csv:jar:1.14.1:compile
[INFO]    |     +- commons-io:commons-io:jar:2.20.0:compile
[INFO]    |     \- commons-codec:commons-codec:jar:1.19.0:compile
[INFO]    \- info.picocli:picocli:jar:4.7.7:compile
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS

Maven 3.10.0 resolved the whole chain, the three modules from the Ivy-published repository and the rest from Maven Central through the POMs. (The download and "cached not-found" warning lines that Maven printed on its first run against an empty local repository were left out.) An earlier attempt, made when the CLI’s POM named an engine revision that had never been published in the Maven repository, printed The POM for io.tidepool:tidepool-engine:jar:1.0.1 is missing, no dependency information available and dropped the transitive dependencies without failing the build: a POM that points to something absent degrades silently, so publish the whole chain to the Maven repository at consistent revisions, as publish-m2-all does.

Going the other way, from a POM to a descriptor, is convertpom, described on Ivy Ant Tasks. If your goal is to publish Maven-style artifacts from Ant and nothing more, the Maven Resolver Ant Tasks have install and deploy that write a POM themselves, with no descriptor conversion.

Source: Ivy: makepom

Publishing in the other tools

Idea Ivy Maven Gradle

Revision of a build

buildnumber, or any property

<version> in the POM, usually changed by the release plugin

version in the build script

Resolved metadata

deliver (static revisions, revConstraint)

the POM as written, with versions normally fixed in it

the generated POM and module metadata; versionMapping can publish the resolved versions

Upload

publish with a resolver

deploy:deploy to distributionManagement

publish task of the publishing plugin

Several modules

buildlist plus subant

the reactor orders modules by their dependencies

multi-project build, ordered by task dependencies

Maven POM from the native metadata

makepom

native

the maven-publish plugin

The details of each column are on the Maven and Gradle pages of this section; the shared concepts (a repository as a directory tree, checksums, promotion) are on Repositories & Publishing Across Tools.

Where older material is outdated

  • The Ivy tutorials publish into ${ivy.local.default.root} and ${ivy.shared.default.root} directories configured through Ivy’s default settings (a local and a shared resolver in a ~/.ivy2 layout). The default settings of 2.6.0 work that way; the examples here use explicit settings, which is what a real build should do.

  • Tutorials use the class latest.integration for everything. Do so only with a repository you control and a cache ttl you understand, as shown above.

  • Older articles recommend ivy:install to "publish" a module from one repository to another; it is a copy of a resolved module with its dependencies (see Ivy Ant Tasks), not a publication of your build.

  • Examples that publish to a repository over sftp or ssh use credentials in the settings; such publication was not run here, and a modern setup uses a repository manager over HTTPS with a token from the environment, never a literal password. The same applies to Maven Central: makepom produces a POM that is not enough for Central (no licence, SCM, developers, signatures); publishing there is not run here and is covered for Maven on Repositories & Publishing Across Tools.

Common problems

  • bad revision found in ivy file ... Use forcedeliver or update: a delivered file from an earlier build is still in the output directory. Clean, or use forcedeliver="true", or update="true" to refresh only revision, status and date.

  • destination already exists and overwrite is false: the revision is already published. Compute a new revision or, for integration builds only, use overwrite="true".

  • A module pins an old dependency right after publishing it: the dynamic-revision TTL, see the section above.

  • The order from buildlist is not what you expect: the order comes from the ivy.xml files only; a dependency declared only in the build.xml is invisible. Check ivy.sorted.modules.

  • publish fails with Ivy file not found in cache ... when publishivy="false": resolve the delivered file first, as the publish-m2 target does.

  • Maven shows no transitive dependencies of an Ivy-published module: the POM is missing for some revision; see the Maven check above.

References