Skip to content

vedanthvdev/affected-tests

Repository files navigation

Affected Tests

A Gradle plugin that detects changes in the current branch and runs only the unit and integration tests relevant to those changes. No seed run required — it works immediately.

Target stack: Gradle 8+ (primary support: 9.x), Java 21+, JUnit 5

Quick Start

1. Apply the plugin

// build.gradle
plugins {
    id 'io.github.vedanthvdev.affectedtests' version 'x.y.z'
}

Check Gradle Plugin Portal for the latest version.

2. Run affected tests

./gradlew affectedTest

3. (Optional) Explain the decision

./gradlew affectedTest --explain

Prints the full decision trace — bucket counts, situation, action, and the tier of the priority ladder (explicit onXxx / mode default) that picked each action — without running a single test. Useful when a CI run escalated to the full suite and the operator needs to know why before filing a bug.

When outOfScopeTestDirs / outOfScopeSourceDirs are configured but zero files in the diff land in the out-of-scope bucket and the situation is DISCOVERY_SUCCESS or DISCOVERY_EMPTY, the trace emits a one-line Hint: pointing at the configured knob. That's the silent-failure trap a real adopter hit: a perfectly valid-looking glob that never bit anything, which the plugin would otherwise only surface after a 30-minute full-suite CI run. The hint is suppressed on EMPTY_DIFF, ALL_FILES_IGNORED, ALL_FILES_OUT_OF_SCOPE, and UNMAPPED_FILE because those branches ran the way they did for reasons an out-of-scope pattern could not have influenced.

Sample output:

=== Affected Tests — decision trace (--explain) ===
Base ref:        origin/master
Mode:            AUTO (effective: LOCAL)
Changed files:   3
Buckets:
  ignored         1
  out-of-scope    0
  production .java 1
  test .java      0
  unmapped        1
  ignored sample: README.md
  production sample: src/main/java/com/example/Foo.java
  unmapped sample: build.gradle
Situation:       UNMAPPED_FILE
Action:          FULL_SUITE (source: mode default)
Outcome:         FULL_SUITE — onUnmappedFile=FULL_SUITE — non-Java or unmapped file in diff
Action matrix (situation → action [source]):
  EMPTY_DIFF               SKIPPED    [mode default]
  ALL_FILES_IGNORED        SKIPPED    [mode default]
  ALL_FILES_OUT_OF_SCOPE   SKIPPED    [mode default]
  UNMAPPED_FILE            FULL_SUITE [mode default]
  DISCOVERY_INCOMPLETE     SELECTED   [mode default]
  DISCOVERY_EMPTY          SKIPPED    [mode default]
  DISCOVERY_SUCCESS        SELECTED   [explicit onXxx setting]
=== end --explain ===

That's it. With zero config, the plugin will:

  • Diff against origin/master (including uncommitted + staged changes).
  • Route each changed file through one of five buckets: ignored (*.md, LICENSE, CHANGELOG, images, **/generated/**), out-of-scope, production .java, test .java, or unmapped (everything else, e.g. application.yml).
  • Pick a discovery strategy — naming, usage, impl, transitive — and merge their results into one test set.
  • Follow 4 levels of transitive dependencies (tuned for typical controller → service → repository chains).
  • Fall through to the full suite if it encounters an unmapped file, so a YAML/Gradle/Liquibase diff never ships without tests.

CI Integration

GitHub Actions

- run: ./gradlew affectedTest -PaffectedTestsBaseRef=${{ github.event.pull_request.base.sha }}

GitLab CI / Jenkins

./gradlew affectedTest -PaffectedTestsBaseRef=origin/main

Make sure to use fetch-depth: 0 so git diff has access to the full history.

The v2 decision model

Every invocation resolves to exactly one Situation and exactly one Action, both of which appear in the log line so an operator can tell — at a glance — why the plugin chose what it did.

Summary log format

Every affectedTest run prints exactly one summary line in the form Affected Tests: <OUTCOME> (<SITUATION>) — <details>:

Affected Tests: SELECTED (DISCOVERY_SUCCESS) — 3 changed file(s), 2 production class(es), 5 test class(es) affected
Affected Tests: FULL_SUITE (UNMAPPED_FILE) — 1 changed file(s); running full suite (onUnmappedFile=FULL_SUITE — non-Java or unmapped file in diff).
Affected Tests: SKIPPED (ALL_FILES_IGNORED) — 1 changed file(s); every changed file matched ignorePaths.

The outcome (SELECTED / FULL_SUITE / SKIPPED) and the situation that produced it are first-class fields on every branch, so CI dashboards can grep -E '^Affected Tests: (SELECTED|FULL_SUITE|SKIPPED)' and bucket runs without parsing the tail.

On a SELECTED outcome, the task also prints the first few FQNs per module at lifecycle level so a reviewer can sanity-check the dispatch from the default CI log without rerunning with --info:

Running 17 affected test classes across 2 module(s):
  application:test (12 test classes)
    com.example.auth.LoginControllerTest
    com.example.auth.TokenServiceTest
    com.example.auth.PasswordHasherTest
    com.example.auth.SessionRepositoryTest
    com.example.auth.RoleMapperTest
    … and 7 more (use --info for full list)
  api:test (5 test classes)
    com.example.api.PublicEndpointsTest
    com.example.api.RateLimitTest
    com.example.api.VersionHeaderTest
    com.example.api.ErrorFormatTest
    com.example.api.HealthProbeTest

The preview caps at five FQNs per module; --info still surfaces the full per-FQN list. The cap exists so a utility change that ripples into hundreds of test classes can't blow past the 4 MiB GitHub Actions per-step log cap before the nested test output even starts.

Situations (what the engine saw)

Situation Fires when
EMPTY_DIFF git diff produced no files at all.
ALL_FILES_IGNORED Every file in the diff matched ignorePaths (e.g. a docs-only MR).
ALL_FILES_OUT_OF_SCOPE Every file sat under outOfScopeTestDirs or outOfScopeSourceDirs (e.g. a Cucumber/api-test-only MR).
UNMAPPED_FILE The diff contains at least one file the plugin cannot resolve to a Java class under sourceDirs/testDirs (e.g. application.yml, build.gradle, a Liquibase changelog).
DISCOVERY_EMPTY Mapping succeeded but the discovery strategies returned zero tests.
DISCOVERY_SUCCESS Mapping + discovery produced a non-empty test set.

Actions (what the engine will do)

Action Meaning
SELECTED Run only the discovered affected tests.
FULL_SUITE Run the entire test suite (no --tests filter).
SKIPPED Exit 0 without running tests.

Every situation gets an independently-configurable action. The matrix is resolved in strict priority order: explicit onXxx setting → mode profile default. Zero-config installs always resolve to a concrete mode via Mode.AUTO detection, so nothing you configure today silently regresses tomorrow.

Mode profiles

mode seeds the defaults for situations you haven't explicitly configured:

Mode EMPTY_DIFF ALL_FILES_IGNORED ALL_FILES_OUT_OF_SCOPE UNMAPPED_FILE DISCOVERY_EMPTY DISCOVERY_INCOMPLETE
local SKIPPED SKIPPED SKIPPED FULL_SUITE SKIPPED SELECTED
ci SKIPPED SKIPPED SKIPPED FULL_SUITE FULL_SUITE FULL_SUITE
strict FULL_SUITE FULL_SUITE SKIPPED FULL_SUITE FULL_SUITE FULL_SUITE
auto Detects CI / GITHUB_ACTIONS / GITLAB_CI / JENKINS_HOME and resolves to ci or local.

Leaving mode unset picks auto, which resolves to local or ci depending on the environment. The UNMAPPED_FILE → FULL_SUITE safety net is the default in every built-in mode, so a zero-config install still escalates on unmapped files without any DSL wiring.

Configuration

All settings have sensible defaults. Override only what you need.

affectedTests {
    // Git base ref to diff against (default: "origin/master")
    baseRef = "origin/main"

    // Include uncommitted/staged changes (default: false — committed-only).
    // The plugin ships COMMITTED-ONLY so a local run matches the MR diff
    // CI will pick up on the same HEAD, and running the task twice in a
    // row produces the same test selection regardless of workstation state.
    // Flip to `true` locally when you are iterating on tests and want
    // WIP to seed the diff. Never enable these in CI — the tree is
    // already clean after checkout there, so they are pure noise.
    includeUncommitted = false
    includeStaged = false

    // v2 profile. "auto" is the recommended migration target.
    // See the "Mode profiles" table above.
    // (default: unset — preserves pre-v2 defaults)
    mode = "auto"

    // ---------------- Path buckets (v2) ----------------

    // Files that must not influence test selection (docs, LICENSE,
    // CHANGELOG, images, generated sources). When every file in the
    // diff matches ignorePaths, the engine lands on ALL_FILES_IGNORED.
    // The defaults already cover markdown/text/LICENSE/CHANGELOG/images
    // at both the repo root and nested paths — you usually only extend this.
    ignorePaths = ["**/*Dto.java"]

    // Test source sets the plugin must not dispatch via the affectedTest
    // task (e.g. Cucumber, Gatling). A diff entirely under these dirs
    // routes to ALL_FILES_OUT_OF_SCOPE → SKIPPED by default.
    //
    // Entries may be either:
    //   • literal directory prefixes — "api-test/src/test/java" matches
    //     that path at the repo root or under any module
    //     (e.g. "services/orders/api-test/src/test/java/..."), and
    //     "api-test" (no source-dir suffix) never claims sibling names
    //     like "api-test-utils/...";
    //   • Ant-style globs — "api-test/**" or "**/api-test/**" — using
    //     the standard JVM glob syntax ("*", "**", "?", "[abc]", "{a,b}").
    //
    // Mix both shapes freely; the plugin picks the right semantics per
    // entry. If you configure this knob but see "Hint:" on --explain
    // saying zero files matched, your paths/globs don't bite — double
    // check them (that's the silent-failure trap sanity testing caught).
    outOfScopeTestDirs = ["api-test/**", "performance-test/**"]

    // Production source sets the plugin must treat as out-of-scope.
    // Accepts the same literal-prefix / glob shapes as outOfScopeTestDirs.
    outOfScopeSourceDirs = []

    // ---------------- Per-situation actions (v2) ----------------

    // Each takes one of "selected" | "full_suite" | "skipped".
    // Any value left unset falls back through mode → pre-v2 default.
    onEmptyDiff            = "skipped"
    onAllFilesIgnored      = "skipped"
    onAllFilesOutOfScope   = "skipped"
    onUnmappedFile         = "full_suite"  // the key MR-safety knob
    onDiscoveryEmpty       = "full_suite"  // belt-and-braces for CI
    // Fires when any scanned Java file failed to parse (malformed source,
    // half-committed refactor, encoding glitch). Mode defaults: CI / STRICT
    // escalate to full_suite (selection is known to be under-reported so
    // safety wins); LOCAL stays on selected (the developer sees the WARN
    // and values iteration speed on a branch they're actively editing).
    onDiscoveryIncomplete  = "full_suite"

    // ---------------- Child-process deadline (v1.9.22) ----------------

    // Wall-clock timeout in seconds for the nested `./gradlew :module:test`
    // invocation. 0 (the default) disables the watchdog and matches
    // pre-v1.9.22 behaviour — the task waits forever. Positive values
    // spawn the child under a ProcessBuilder watchdog: on timeout the
    // task runs destroy() → 10s grace → destroyForcibly() → 5s reap and
    // then fails the build so a hung test never holds the CI worker
    // hostage. Note: the watchdog path uses inheritIO for the child's
    // stdio, so Develocity build-scan stream capture of the nested
    // output is not available when a timeout is set — leave at 0 and
    // enforce the deadline at the CI-job level if you rely on scan
    // ingestion of child-process output.
    //
    // Negative values are rejected. Since v2.1 the range check fires at
    // configuration time (IDE sync / `./gradlew help`) rather than only
    // when the task executes, so a typo like `-5` is caught without
    // needing to actually launch `affectedTest`.
    gradlewTimeoutSeconds  = 1800  // 30 min; use 3600 for suites with integration tests

    // ---------------- Discovery tuning ----------------

    // Discovery strategies: "naming", "usage", "impl", "transitive" (default: all four)
    strategies = ["naming", "usage", "impl", "transitive"]

    // Transitive dependency depth — used when the "transitive" strategy is enabled.
    // Raised from 2 → 4 in v2 because typical Spring controller→service→repo
    // chains are 2–3 deep; 4 gives a margin without producing runaway sets.
    // (default: 4, max: 5, 0 = disabled)
    transitiveDepth = 4

    // Test class suffixes (default: ["Test", "IT", "ITTest", "IntegrationTest"])
    testSuffixes = ["Test", "IT", "ITTest", "IntegrationTest"]

    // Source directories (default: ["src/main/java"])
    sourceDirs = ["src/main/java"]

    // Test directories (default: ["src/test/java"])
    testDirs = ["src/test/java"]

    // Include tests for implementations of changed interfaces (default: true)
    includeImplementationTests = true

    // Implementation naming prefixes/suffixes — "Impl" matches FooImpl for Foo;
    // "Default" matches DefaultFoo for Foo, which is idiomatic in Spring code.
    // (default: ["Impl", "Default"])
    implementationNaming = ["Impl", "Default"]
}

How It Works

The pipeline is five stages: detect what changed, bucket each path (ignored / out-of-scope / production / test / unmapped), resolve the Situation, discover the tests impacted by the in-scope Java classes, and execute only that subset — or the full suite, or nothing at all — based on the Action the Situation maps to.

Affected Tests v2 architecture: git diff feeds PathToClassMapper which buckets each changed file as ignored, out-of-scope, production Java, test Java, or unmapped; the resulting Situation (EMPTY_DIFF, ALL_FILES_IGNORED, ALL_FILES_OUT_OF_SCOPE, UNMAPPED_FILE, DISCOVERY_EMPTY, DISCOVERY_SUCCESS) maps to an Action (SELECTED, FULL_SUITE, SKIPPED) resolved in priority order from explicit onXxx settings, legacy booleans, mode defaults, and pre-v2 hardcoded defaults

Source: docs/architecture.mmd · regenerate with npx --yes @mermaid-js/mermaid-cli -i docs/architecture.mmd -o docs/architecture.svg -b transparent

Discovery Strategies

All four strategies run against every changed production class. Their results are merged (union), so a test is run if any strategy identifies it. The goal is maximum coverage — running a few extra tests is always preferable to missing one.

Strategy What it does Example
naming For each changed class Foo, looks for test files named FooTest, FooIT, FooITTest, FooIntegrationTest (configurable suffixes). Purely file-name based — no parsing required. FooService changed → finds FooServiceTest, FooServiceIT
usage Parses every test file with JavaParser and checks whether it references any changed class. Uses a two-tier approach: (1) direct import match — if the test has import com.example.FooService;, it's affected regardless of how it uses the class; (2) type-reference scan for wildcard imports (import com.example.*) and same-package usage (no import needed). Catches fields, method parameters, return types, constructor calls, generics, and casts. BarModel changed → finds BarValidatorTest (imports it), BazMapperTest (same package, uses it as field type)
impl When an interface or base class changes, scans all production source files to find classes that extends or implements the changed type (via AST) and classes following the *Impl or Default* naming convention. Then re-runs the naming and usage strategies on those implementations. FooService (interface) changed → finds FooServiceImpl and DefaultFooService → finds FooServiceImplTest / DefaultFooServiceTest
transitive Builds a reverse dependency map of all production classes: for each class, which other classes depend on it (via field types). When a class changes, walks this "used-by" graph N levels deep (configurable, default 4, max 5) to find consumers. Then runs naming + usage on those consumers. BazGateway changed → FooService uses it (depth 1) → OrdersController uses FooService (depth 2) → finds both tests via naming

How scanning works

The plugin scans the project tree recursively at any depth to find source and test directories. It is completely project-structure agnostic — it does not assume any particular module layout. Whether your modules are flat (api/src/test/java), nested (services/orders/src/test/java), or deeply nested (platform/services/orders/src/test/java), all test files are discovered.

Directories like .git, build, .gradle, and node_modules are automatically skipped during the walk. outOfScopeTestDirs and outOfScopeSourceDirs are additionally filtered at index time so discovery never picks up tests living there.

Directly changed tests

Any test file that is itself modified in the diff is always included in the run, regardless of strategy results.

Multi-Module Support

The plugin works out of the box with multi-module projects — it recursively scans all modules at any nesting depth, so no configuration is needed.

Internally, each discovered test FQN is traced back to the Gradle subproject that owns its file, and tests are then dispatched per module:

./gradlew :moduleA:test --tests com.example.FooTest \
          :moduleB:test --tests com.example.BarTest

This makes --tests filters scope cleanly to their owning module, instead of being applied globally and failing on any subproject that doesn't happen to contain the FQN. Cross-module imports (e.g. a test in application that imports a class from api) are still detected correctly via the usage and impl strategies.

Behaviour reference

Every row below shows the situation the engine resolved, and the action applied with the default configuration (no mode set, no explicit onXxx).

Diff contents Resolved Situation Default Action Override knob
Only mapped production/test .java files DISCOVERY_SUCCESS (or DISCOVERY_EMPTY if no tests map) SELECTED discovery tuning
Only files matching ignorePaths (docs, LICENSE, CHANGELOG, images, generated) ALL_FILES_IGNORED SKIPPED onAllFilesIgnored or mode=strict
Only files under outOfScopeTestDirs / outOfScopeSourceDirs (e.g. api-test only) ALL_FILES_OUT_OF_SCOPE SKIPPED onAllFilesOutOfScope
Any YAML / Gradle / Liquibase / .java outside configured dirs UNMAPPED_FILE FULL_SUITE (via onUnmappedFile = "full_suite") onUnmappedFile = "selected"
No changed files at all EMPTY_DIFF SKIPPED onEmptyDiff = "full_suite" / mode = strict
Mapping succeeds but discovery returns zero tests DISCOVERY_EMPTY SKIPPED — or FULL_SUITE if mode=ci/strict onDiscoveryEmpty / mode
At least one scanned .java file failed to parse (malformed / half-committed / encoding glitch) DISCOVERY_INCOMPLETE (takes precedence over both DISCOVERY_EMPTY and DISCOVERY_SUCCESS) SELECTED — or FULL_SUITE if mode=ci/strict onDiscoveryIncomplete / mode
Mixed diff: Java + unmapped file UNMAPPED_FILE (takes precedence) FULL_SUITE onUnmappedFile — set to "selected" to fall through to discovery
baseRef not resolvable FAILED Hard error (prevents silent test skipping in CI)
Not a git work tree / JGit I/O error FAILED Hard error

The onUnmappedFile = "full_suite" default follows the "run more, never run less" principle: a change to application.yml can alter production behaviour just as surely as a change to a .java file, so the plugin cannot safely pick a subset from an empty Java mapping.

Migrating from v1 config

v2.0.0 removed the three v1 legacy knobs. If any of these still appear in your build.gradle, Gradle configuration will fail before the affectedTest task runs:

  • runAllIfNoMatches
  • runAllOnNonJavaChange
  • excludePaths

Since v2.1.0, the error is targeted: instead of Gradle's generic "unknown property" message, you'll see a migration hint naming the exact v2 replacement knob. For example, runAllIfNoMatches = false now fails with:

> affectedTests.runAllIfNoMatches was removed in v2.0.0. Use
  onEmptyDiff = "full_suite" and/or onDiscoveryEmpty = "full_suite"
  instead (or set mode = "ci" / "strict" to get those defaults). See
  CHANGELOG.md v2.0 for the full migration table.

Shims exist for all three legacy knobs; Kotlin DSL callers already get a compile error naming the removed property.

Deprecation timeline

Release What happens
v1.9.x and earlier Legacy knobs worked silently. No warnings.
v1.10.x – v1.11.x Legacy knobs still worked. A per-run WARNING: [affected-tests] '<knob>' is deprecated… named each one and its replacement. Zero-config users saw nothing.
v2.0.0 (this release) Legacy knobs removed. excludePaths, runAllIfNoMatches, runAllOnNonJavaChange are unknown properties — Gradle configuration fails. Migrate using the table below.

Before / after

v1 config v2 equivalent Why
runAllIfNoMatches = true onEmptyDiff = "full_suite" and onDiscoveryEmpty = "full_suite" The v1 flag conflated two different situations ("git diff is empty" vs "discovery found nothing"). v2 splits them so you can e.g. skip empty-diff runs but still fall back to full suite when discovery fails.
runAllIfNoMatches = false (explicit) Set mode = "local", or pin onEmptyDiff = "skipped" + onDiscoveryEmpty = "skipped" explicitly. Do not just delete the line — in v2 the zero-config mode = "auto" resolves to ci in a CI runner, and ci escalates DISCOVERY_EMPTY to FULL_SUITE. A v1 pipeline that set runAllIfNoMatches = false specifically to prevent the discovery-empty branch from flipping to full suite will start running the full suite on every no-match MR unless one of these two knobs is pinned.
runAllOnNonJavaChange = true onUnmappedFile = "full_suite" Single-situation knob, same semantics.
runAllOnNonJavaChange = false onUnmappedFile = "selected" Plugin treats the unmapped file as if absent and continues to discovery.
excludePaths = ["**/generated/**"] ignorePaths = ["**/generated/**"] Identical semantics — just a rename.
excludePaths = [] (explicit empty) Delete the line v2's default ignorePaths list is broader (markdown, licence, changelog, images, generated). Explicitly empty discards all of it.

Worked example

Before (v1):

affectedTests {
    baseRef = "origin/master"
    runAllIfNoMatches = true
    runAllOnNonJavaChange = true
    excludePaths = ["**/generated/**"]
    transitiveDepth = 4
}

After (v2):

affectedTests {
    baseRef = "origin/master"
    mode = "ci"                                // one line replaces both booleans in 95% of cases
    // excludePaths / ignorePaths unset — v2 default already covers generated/
    // transitiveDepth = 4 now the default, no need to set it
}

Or if you want every situation explicit (recommended for production pipelines where you care about each edge case):

affectedTests {
    baseRef = "origin/master"
    onEmptyDiff          = "full_suite"
    onAllFilesIgnored    = "skipped"
    onAllFilesOutOfScope = "skipped"
    onUnmappedFile       = "full_suite"
    onDiscoveryEmpty     = "full_suite"
}

Decision tree — "which replacement do I need?"

Did you set runAllIfNoMatches?
├─ No                    → nothing to do for this knob
├─ runAllIfNoMatches = true
│  └─ set onEmptyDiff = "full_suite" AND onDiscoveryEmpty = "full_suite"
│     (or just set mode = "ci" / mode = "strict" — both imply it)
└─ runAllIfNoMatches = false
   └─ DO NOT just delete the line. In v2, zero-config AUTO-in-CI
      escalates DISCOVERY_EMPTY to FULL_SUITE. Either:
      · pin mode = "local", OR
      · pin onEmptyDiff = "skipped" + onDiscoveryEmpty = "skipped"

Did you set runAllOnNonJavaChange?
├─ No                           → nothing to do
├─ runAllOnNonJavaChange = true → set onUnmappedFile = "full_suite"
│                                  (or just delete the line — it's the default)
└─ runAllOnNonJavaChange = false → set onUnmappedFile = "selected"

Did you set excludePaths?
├─ No                → nothing to do
├─ excludePaths = [] → delete the line (you probably want the broader v2 default)
└─ excludePaths = [...] → rename to ignorePaths with the same list

What the summary log tells you after migration

Every affectedTest run prints the outcome + situation + the v2 knob that fired on one line, so CI dashboards can key off a stable vocabulary:

Affected Tests: FULL_SUITE (UNMAPPED_FILE) — 1 changed file(s); running full suite (onUnmappedFile=FULL_SUITE — non-Java or unmapped file in diff).

Breaking-change note for grep-based alerting: v1 summary lines carried both the v1 name (runAllOnNonJavaChange=true) and the v2 name (onUnmappedFile=FULL_SUITE) to ease migration. v2.0 drops the v1 vocabulary. Any CI alert rules keyed on runAllIfNoMatches=true, runAllOnNonJavaChange=true, or the [affected-tests] '…' is deprecated warning must be updated to the v2 knob names.

Project Structure

affected-tests/
├── affected-tests-core/          # Git integration, change detection, test discovery
├── affected-tests-gradle/        # Gradle plugin (io.github.vedanthvdev.affectedtests)
├── docs/
│   ├── DESIGN-v2.md              # v2 design document (situation/action/mode model)
│   ├── architecture.mmd          # Mermaid source for the architecture diagram
│   └── architecture.svg          # Rendered diagram embedded in README
├── build.gradle
├── settings.gradle
└── README.md

Requirements

Component Version
Gradle 8.x+, primary support on 9.x
Java 21+
JUnit 5.x
Spring Boot Compatible (no Boot-specific code)

Dependencies

  • JGit — Git change detection (no native git binary required)
  • JavaParser — Source-level test discovery (usage + implementation strategies)
  • SLF4J — Logging

Changelog

See CHANGELOG.md for the release history. GitHub's auto-generated release notes (one entry per tag, backed by the merged PR titles) are on the Releases page.

Versioning

Versions are managed automatically via axion-release — derived from git tags, never hardcoded in source. The release workflow (.github/workflows/release.yml) runs on every push to master and cuts a patch release by default. For minor or major bumps, trigger the workflow manually with a version input.

Goal How
Check what version this branch is ./gradlew currentVersion
Auto patch release (e.g. 1.9.121.9.13) Merge to master — the release workflow does the rest
Force a minor or major release (e.g. 1.9.x1.10.0) GitHub → Actions → ReleaseRun workflow → fill version (e.g. 1.10.0), or run gh workflow run release.yml --ref master -f version=1.10.0
Pin a minor/major on a merge-to-master release (no dispatch) Commit a .release-version file at repo root containing the SemVer (e.g. 2.1.0) in the same PR whose merge should ship that version. The release workflow tags that version, publishes it, then auto-deletes the file in a [skip ci] follow-up commit so the next push falls back to auto-patch-increment.
Release-candidate / pre-release Trigger Run workflow with version: 1.10.0-RC1, or locally: ./gradlew release -Prelease.versionIncrementer=incrementPrerelease
Manually re-run a failed publish Re-trigger the workflow on the already-tagged commit — portal-check + release-check steps are idempotent

Note: ./gradlew markNextVersion -Prelease.version=X.Y.0 influences only what currentVersion reports on that branch. It does not change what ./gradlew release picks, because the release task still follows the default patch incrementer unless -Prelease.version=X.Y.Z is passed at release time. Use the workflow dispatch above for minor/major bumps instead.

License

Apache 2.0

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors