jamesfredley opened a new issue, #16544:
URL: https://github.com/apache/grails-core/issues/16544
## Why this issue exists
The Gradle build is the single biggest drag on this project. Across the last
two and a half years we have spent thousands of volunteer hours on it, and that
time did not go into Grails. In other projects Gradle is close to invisible:
you upgrade it, you run it, it works. In grails-core it is a standing
maintenance burden that breaks on its own schedule. These are the problems we
keep hitting.
- **Every Gradle upgrade breaks the build.** Each upgrade touches 25 to 35
files and is followed by fixes to our own plugin code. We do this every few
weeks.
- **Memory is hand-tuned forever.** We raise a heap, an OOM moves somewhere
else, and we raise another heap. Each round is a new PR, a new review cycle,
and a new magic number.
- **CI hammers the dependency repositories until they push back.** Maven
Central and repo.grails.org rate-limit or block us because every CI run
re-downloads and re-checks dependencies from scratch.
- **We pay for caching, then throw it away.** We keep fixing tasks so they
can be cached, then force CI to rerun everything anyway.
- **CI is slow and fragile.** We have stacked timeouts, hang sentinels and
"accept the known hang" scripts on top of the build instead of removing the
reasons it hangs.
- **Contributors can't reason about it.** It is spread over 6 separate
Gradle builds, 12 test-config script variants, about 9,000 lines of custom
Gradle plugin code, and roughly 20 `-P` switches.
Most of this is not anyone's fault. It is legacy structure plus a lot of
reasonable-looking local fixes that each added one more moving part. The
cumulative result is a build that needs constant attention. **The goal of this
issue is simplification:**
- fewer moving parts
- fewer files to edit per change
- fewer breakages when Gradle or a dependency moves
- stock Gradle behavior wherever possible
### What the git history shows
Since 2025-01-01, **3,506 of 8,938 non-merge commits (39%)** touched a
Gradle build file, a build-logic class, `gradle.properties`, or a CI workflow.
1,233 of them touched the core build machinery (`build-logic`, root
`build.gradle`/`settings.gradle`, `dependencies.gradle`, the test config
scripts, or `gradle.yml`). Two caveats: the history includes repositories that
were merged into the monorepo, and some of those commits are ordinary
dependency edits. Even so, the patterns below are unambiguous.
**1. Nine Gradle upgrades in nine months, each followed by repair work**
| Date | Commit | What happened |
|---|---|---|
| 2026-01-23 | b7c9ed4dcf | Upgrade to Gradle 8.14.4 |
| 2026-01-30 | 9fb682661f | Upgrade to Gradle 9.3.1 |
| 2026-01-31 | 7ce8d2b62a | Fix `Task.project` invocation at execution time
deprecations |
| 2026-03-20 | 66f6229306 | Upgrade to Gradle 9.4.1 |
| 2026-03-25 | 90c408651d | Fix overlapping task outputs preventing build
cache usage |
| 2026-05-13 | f4082aa7ee | Revert abstract compile changes |
| 2026-05-20 | 7e4c48e799 | Upgrade to Gradle 9.5.1: **35 files** (6 copies
of the wrapper, 2 profile skeletons, Forge templates) |
| 2026-06-24 | 8af52a37ef, 3d55d78174 | Gradle 9.6.0 and 8.14.5 the same day
|
| 2026-07-22 | f8d8601adc | Gradle 9.6.1 |
| 2026-07-31 | 4fdb88b163 | Defer compile-classpath probes out of
GroovyCompile task configuration |
| 2026-08-07 | 56a8dc4da4, 79504824fd | Gradle 9.7.0 (28 files), and the
same day the Groovy compiler config script had to move into its own task |
| 2026-08-20 | e718a7ba79 | Gradle 9.7.1 |
| 2026-09-25 | ec6ec08873, 6c54b95fcd | Gradle 9.8.0 (25 files), and the
same day a fix for a now no-op `Configuration.visible` call |
| 2026-09-28 | 0c2f05ed06 | Build with a **SNAPSHOT** of our own
`grails-publish` plugin just to support Gradle 9.8.0 |
Earlier, e272f5614f (2025-05-19) had to add a whole new Gradle task "since
newer versions of Gradle isolate classpaths of Gradle tasks". Almost every
break lands in our custom plugin and task code, not in module build files. The
more custom Gradle code we carry, the more every upgrade costs.
**2. Memory whack-a-mole**
- 816a20735b (2024-03) Increase MaxMetaspaceSize. Then d5bade295c,
36b44429ba, 7d8ef50b07 (2025-06) remove the metaspace settings again.
- acdc46ec8e and e5b0471936 (2025-02) increase Gradle memory and set
`--max-workers=2`.
- 6cce089473 (2026-01) increase the build runner root reserve to 6 GB.
- 629e94ad6f, 61818f00f7, 3a0fe5fc89 (2026-04) "increase CI test memory",
"fixed OOM", "analysis of OOM".
- 13c964143f (2026-08-16) stop test forks oversubscribing the machine.
cc7e0c3353 (2026-08-18) bound concurrent JVMs on the macOS runner.
- 1b2cab5ffd, 900dff8160 (2026-09-15) release Groovydoc memory between
documentation tasks, then serialize Groovydoc. 93f00bbed6 (2026-09-22) run
Groovydoc and the user guide outside the Gradle daemon, followed by three
review-round commits (5d347efc38, 725d807fee, dfede9d49d) and eb0b0bcf76
"record the measured basis for the 3G daemon heap".
- f921b366b3, 79d34939e2, 9ad51a9b5c (2026-09-26 to 28) test fork heap
budget, condensed, then documented.
- 626c81dca3 (2026-10-01) keep the dependency-analysis plugin inside the 3G
daemon.
- #16534 (2026-10-05) Hibernate 7 tests OOM after an `8.1.x` merge.
`gradle/hibernate7-test-config.gradle` never applied the shared test config, so
`forkEvery` silently stayed 0 and one JVM ran every test class until the heap
filled.
**3. Making tasks cacheable while forcing CI to rerun everything**
We keep doing careful work to make tasks cacheable:
- ffbf8adb73 make the SBOM timestamp reproducible to prevent cascading cache
misses
- 90c408651d fix overlapping outputs preventing build cache usage
- fc7c9a4885 normalize `APP_BASE_DIR` for relocatability
- bb884fff83 force `base.dir` into the Groovy compiler cache key
- e5f942e93c keep scaffolded pages built on one platform out of another's
build cache
- 5a10bbab5b (2026-10-03) hash dependency cache keys from fixed-depth paths
At the same time, CI is told to ignore the cache:
- a78c022505 (2025-06-03) "always rerun tasks in CI to ensure tests are run"
- cca2abfd77 (2025-06-16) "rerun tasks so all tests always run"
- e6f28c3718 (2026-01-25) "Disable Groovy compilation cache in CI"
- ddf492abe2 (2026-01-27) "use --rerun-tasks flag"
- 5103ee38bc (2026-04-25) add the ability to rerun tests without rerunning
all tasks
- 7bb015970e (2026-08-08) separate rerun build work from its test shards
**4. Repository flip-flops and rate limiting**
- 24d23af351 (2025-03) remove `--refresh-dependencies` "to avoid Premature
end of Content-Length delimited message body"
- 395d9a64ba (2025-04-30) use Maven Central and Apache snapshots instead of
repo.grails.org
- c15f2f21a6 (2025-05-01) simplify back to a single Maven repository
- 2fdcd2a12a (2025-05-02) add repo.grails.org for the Gradle tooling API
dependency
- 9767a171ec (2025-05-07) add repo.grails.org to grails-gradle
- bf38aa59c2 (2025-05-08) use repo.spring.io/milestone instead of
repo.grails.org/core
- 42626fd036 (2025-05-15) use repo.grails.org/grails/restricted
- 967690d86d and 314317b9f0 (2025-06-28) reorder the Maven Central
repositories, and "add IP checks for debugging timeout issues"
- 55c86cc25b (2025-07-06) remove a duplicate repository definition
- 95cafe5ee2 (2025-10) centralize repository configuration
- 5f903f078a (2026-01) rework repository definitions into a settings plugin
- 11a4fc8e85 (2026-04-29) "add check for rate limit for debugging"
Repository lists are still declared in three places today.
**5. CI hangs papered over instead of removed** (most of this in two days,
2026-08-17 and 18)
- f5e7a91157 cap functional jobs at 70 minutes and disable the Gradle daemon
- 699f8c6dd1 give the slower Groovy 6 functional shard 90 minutes
- 403f43d2b4 finish functional shard 1 after Gradle worker teardown hangs
(new init script)
- f6c942b35f write a "functional hang sentinel" when the task graph empties
- 2ef2a57df3 "accept the known functional hang after a green end-of-suite"
(new Python script)
- 9051523827 time out test jobs well before GitHub's six-hour default
- a1bd43ddfc give the Validate Actions job 10 minutes "for a cold Gradle
run". Validating YAML takes a cold Gradle run.
**6. Tooling added, reverted, re-added**
- Code analysis: 6aa1d893f1 (2026-03-29) consolidated violation reporting.
29a24649f1 (2026-05-28) adds a code-analysis convention plugin, then 33412eebc7
(same day) reverts the code-analysis and JaCoCo infrastructure. b08441587d
(2026-06-01) restores the code style config to the 8.0.x baseline. adbe27cde5
(2026-07-20) automates repository conventions and staged analysis. 51367bbe7e
(2026-09-25) runs the report writers only through their aggregate tasks.
- Develocity: 324e880510 then 2f09493f25 (2025-04-14) upgrade the Develocity
plugin to v4, then revert to v3 the same day. 2bb982f3f9 goes back to a prior
Develocity version to work around a retry issue. e5dfb507d6 adds the test-retry
plugin "required after updating to develocity 4.0". bf78a4cbae removes
Develocity test retries, and 7180d65c13 removes Test Distribution.
## How the build is shaped today (9.0.x at c3b2ea8fcf)
- **Separate Gradle builds:** 6 (root, `build-logic`, root `buildSrc`,
`grails-gradle`, `grails-forge`, `end-to-end`), plus `gradle-bootstrap`, whose
only job is copying the wrapper.
- Develocity is already at different versions in different builds (4.5.0
in
[settings.gradle](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/settings.gradle#L36)
vs 4.3.2 in
[grails-gradle/settings.gradle](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/grails-gradle/settings.gradle#L33)).
- **Root subprojects:** about 280, of which 120 are sample apps under
`grails-test-examples`.
- **Test config scripts:** 12 variants. 258 build files `apply from:` one of
them.
- **Main CI workflow:** about 60 jobs and 63 Gradle invocations per PR,
counted from the
[gradle.yml](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.github/workflows/gradle.yml)
matrix. Most of them pass `--rerun-tasks`, and there is also a separate "rerun
everything" lane
([L262-L339](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.github/workflows/gradle.yml#L262-L339)).
- **Cache settings that defeat themselves:**
- [gradle/test-config.gradle
L52-L61](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/gradle/test-config.gradle#L52-L61)
turns off caching for `GroovyCompile` and `Test` on CI.
- [settings.gradle
L69-L75](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/settings.gradle#L69-L75)
turns off the local build cache on CI but still pushes to the remote cache.
- **Zero-hour dependency cache on CI:** [build.gradle
L170-L176](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/build.gradle#L170-L176)
sets the dynamic and changing (snapshot) dependency cache time to 0 hours on
CI. Every invocation on every runner re-checks every snapshot over the network.
- **Unfiltered fallback repository:** [GrailsRepoSettingsPlugin
L84-L89](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsRepoSettingsPlugin.groovy#L84-L89)
lists repo.grails.org/grails/restricted with no content filter, so every
Central miss is retried there.
## Principles for this work
- **Delete before adding.** The fix for a problem should usually remove
code, not add a new switch.
- **Stock Gradle first.** Prefer behavior Gradle ships and keeps stable over
custom plugins and tasks. Our own plugin code is where upgrades break.
- **No fashion-driven migrations.** Configuration cache, isolated projects,
version catalogs, Kotlin DSL, Test Distribution and predictive test selection
are out of scope. They add migration work without removing the problems above.
Some may become free later once the build is simpler, but they are not goals.
- **Define once.** A setting that matters (heap, forks, cache, repositories,
wrapper version) should live in exactly one place.
- **Measure, don't guess.** Each phase records before and after numbers.
Suggested metrics: PR CI wall clock, Gradle invocations per PR, dependency
downloads per run, configuration time for `:grails-core:test`, and files
touched by a Gradle version bump.
## Phase 1: stop discarding work and stop hammering repositories
These are small, independent changes. Each is one PR.
### 1.1 Stop forcing reruns on CI
Remove routine `--rerun-tasks`, remove `outputs.cacheIf { !isCiBuild }` for
`Test`, and enable the local build cache on CI.
- **Benefits:**
- The largest single CI time saving. Unchanged modules stop rebuilding and
retesting on every job.
- Far less memory pressure and fewer OOMs, because far fewer JVMs start.
- The remote cache we already push to finally gets used.
- Deletes the separate rerun lane and its sharding.
- **Downsides:**
- A test whose inputs did not change is not re-executed. That is correct
Gradle behavior, but it means a flaky test or a test with an undeclared input
can hide behind a cache hit.
- Mitigation: keep one scheduled, fully uncached run (nightly) that runs
everything.
- Release builds keep running without the cache.
- **Done when:** PR jobs no longer pass `--rerun-tasks`, a nightly uncached
run exists, and the before and after CI times are recorded.
### 1.2 Re-enable the build cache for Groovy compilation, after proving it
is safe
`GroovyCompile` was taken out of the CI cache (e6f28c3718) out of concern
that a changed AST transform might not invalidate a cached compile.
- **Benefits:** Compilation is the other big repeated cost on CI. Cached
compiles also make every downstream task cheaper.
- **Downsides:**
- If an AST transform's classes are not part of the compile task's inputs,
we would get stale bytecode. That is the exact bug the current setting guards
against.
- This item therefore depends on proving the inputs are modeled correctly,
and it may uncover input-modeling work first.
- **Gate:** add a test that changes an AST transform, recompiles a module
that uses it with the build cache enabled, and asserts a cache miss and the new
behavior. Only remove `cacheIf { !isCiBuild }` from `GroovyCompile` once that
test passes.
### 1.3 Restore normal dependency cache times on CI
Remove the 0-hour `cacheDynamicVersionsFor` and `cacheChangingModulesFor`
override on CI ([build.gradle
L170-L176](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/build.gradle#L170-L176),
and the same pattern in `grails-forge/build.gradle`).
- **Benefits:** This is the most direct cause of hammering the repositories.
Every Gradle invocation on every runner currently re-checks every snapshot and
dynamic version over the network.
- **Downsides:**
- Snapshots of our own libraries (asset-pipeline, SiteMesh,
grails-publish) can be up to the cache window old on CI.
- Mitigation: use a short window (a few hours) rather than 24, and keep 0
for release and reproducible builds.
### 1.4 Put a content filter on repo.grails.org
- **Benefits:**
- Every lookup Central does not answer currently falls through to
repo.grails.org/grails/restricted.
- A filter limited to the coordinates we really need from there stops the
fallback traffic and makes resolution predictable.
- Repository lists are declared in three places today
(`gradle/plugin-repositories.gradle`, `GrailsRepoSettingsPlugin`,
`build-logic/settings.gradle`). This item collapses them into one.
- **Downsides:**
- We have to find out which coordinates actually come from
repo.grails.org, for example with a resolution run against Central only.
- If the filter is too narrow, resolution fails loudly. That is better
than silent fallback, but it can briefly break a branch.
### 1.5 Give the Gradle cache on CI one owner
Let `gradle/actions/setup-gradle` manage the Gradle user home cache, write
it only from default branches, and delete the hand-rolled `actions/cache`
restore and save steps in `gradle.yml`.
- **Benefits:**
- Today unrelated jobs race to save the same key, and the key only hashes
`dependencies.gradle`, so most changes produce a stale or useless cache.
- One owner means a warm dependency cache on every job, fewer downloads,
and a lot less workflow YAML.
- **Downsides:**
- Less fine-grained control over cache keys.
- GitHub's per-repository cache size limit means we need to watch cache
churn after the switch.
### 1.6 Build Groovydoc only in the docs and publish jobs
Groovydoc currently runs in every main build matrix leg ([gradle.yml
L175-L199](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.github/workflows/gradle.yml#L175-L199)).
All 123 Groovydoc tasks run one at a time in forked JVMs.
- **Benefits:** Removes one of the slowest and most memory-hungry steps from
every PR leg, and most of the recent memory tuning was about Groovydoc.
- **Downsides:** A Groovydoc-only breakage on one OS or JDK is caught later,
in the docs job instead of every matrix leg.
### 1.7 Collect coverage from the existing test jobs
The coverage workflow schedules the tests again
([coverage.yml](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.github/workflows/coverage.yml)).
- **Benefits:** Coverage stops costing a second full test run.
- **Downsides:**
- The test jobs must upload their JaCoCo execution data, and a small
aggregation job merges it.
- Coverage then depends on the test jobs succeeding.
### 1.8 Run the Groovy snapshot canary on a schedule
Change
[groovy-snapshot-canary.yml](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.github/workflows/groovy-snapshot-canary.yml#L21)
from every PR to a nightly schedule plus manual dispatch.
- **Benefits:** Every PR stops rebuilding upstream Groovy and downloading
its dependencies.
- **Downsides:** A Groovy snapshot regression is noticed up to a day later
and is no longer tied to the PR that hit it.
- **Alerting:** failures are still emailed.
[.asf.yaml](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/.asf.yaml#L86)
sets `jobs: [email protected]`, which sends an email whenever a
workflow run fails, including scheduled runs, and again when it recovers.
## Phase 2: one definition of a module
### 2.1 Explore: move the sample apps out of the default project set
This is an investigation, not a commitment. The output is a measured
recommendation.
- **Today:**
- The 120 apps in `grails-test-examples` are root subprojects.
- Each applies
[functional-test-config.gradle](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/gradle/functional-test-config.gradle#L20-L38),
which calls `evaluationDependsOn` on every other subproject.
- It then hand-builds a substitution table so `org.apache.grails:*`
coordinates resolve to local projects.
- As a result, even `:grails-core:test` configures all of about 280
projects.
- **Idea:** give `grails-test-examples` its own `settings.gradle` with
`includeBuild('..')`. Gradle's built-in composite substitution then maps those
coordinates to the root projects, so the custom substitution code goes away.
- **Benefits if it pans out:**
- Framework work configures about 160 projects instead of about 280.
- Fewer cross-project hooks.
- The sample apps consume Grails the way a real application does, through
stock Gradle behavior.
- **Downsides:**
- The examples become a separate entry point (`cd grails-test-examples &&
../gradlew ...`).
- IDE import becomes a composite of two builds.
- CI functional jobs need new commands.
- **Deliverable:** a spike branch that records configuration time for
`:grails-core:test --dry-run` before and after, notes IDE import behavior, and
states a go or no-go recommendation.
### 2.2 Do: one `grails-library` convention plugin and one test configuration
- **Today:**
- A typical module, for example
[grails-common/build.gradle](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/grails-common/build.gradle#L18-L72),
lists 11 plugin ids and then `apply from:` the docs and test config scripts.
- That block is pasted, with small variations, into most module build
files.
- Test settings are split across 12 script variants that are free to
drift. #16534 is the latest example of that drift.
- **Change:**
- Modules apply a single `id 'org.apache.grails.buildsrc.library'`.
- That plugin applies the common plugins, the docs config, and one test
configuration.
- The test-config variants collapse into one, with a few explicit, named
settings where a module genuinely differs (for example a heap size or a forked
database).
- The memory policy lives here too, in one place: fork count, heap per
fork, `forkEvery`, and a worker cap sized to the runner.
- **Benefits:**
- A fix to test, cache, or memory settings lands in every module at once.
- New modules get correct settings by default.
- Module build files shrink to what is actually unique about them.
- The class of bug behind #16534 goes away.
- **Downsides:**
- A mechanical edit across many build files, which is a large diff to
review.
- Risk of silently changing a module's effective settings.
- Mitigation: before and after, dump the effective settings of every
`Test` and `GroovyCompile` task (heap, forks, `forkEvery`, JVM args,
cacheability) and diff them in the PR.
## Phase 3: collapse the build sprawl from 7 builds to 3
Not everything can be one build. The root build applies the Grails Gradle
plugins (every sample app uses `org.apache.grails.gradle.grails-web` or
`grails-plugin`). Gradle only allows a build to apply plugins it produces
through an included build. So `grails-gradle` and `build-logic` stay as
included builds, and the rest goes.
| Build | Today | After |
|---|---|---|
| root | main build | main build, with Forge folded in via #16337 |
| `grails-gradle` | own wrapper, own `buildSrc`, duplicated settings, built
standalone in CI | included build only; no wrapper, no `buildSrc`, no
duplicated settings; built from root |
| `build-logic` | own wrapper; included by root, root `buildSrc` and
`grails-gradle` | the single home for build plugins |
| root `buildSrc` | [no source
code](https://github.com/apache/grails-core/blob/c3b2ea8fcf3d6104643787d70e00eb3c261e8ad1/buildSrc/build.gradle#L27-L36),
only puts 8 third-party plugins on the classpath | deleted; dependencies move
to `build-logic` or versioned `plugins {}` blocks |
| `grails-gradle/buildSrc`, `grails-forge/buildSrc` | classpath-only shims |
deleted |
| `gradle-bootstrap` | copies the wrapper into the other builds | deleted |
| `end-to-end` | consumes published artifacts | stays separate, which is its
purpose |
The settings boilerplate (Develocity, build cache, repositories, the
`DirectoryScanner` workaround) moves into the existing
`org.apache.grails.buildsrc.repo` settings plugin. The property-sharing code
(`SharedPropertyPlugin`, plus the root lookup that walks up to `.asf.yaml`)
shrinks to one small helper. Included builds genuinely cannot read the root
`gradle.properties`, so a little of this has to stay.
- **Benefits:**
- A Gradle version bump touches one build wrapper instead of six. The
user-facing templates (profile skeletons, Forge templates) still need it, as
they should.
- One settings plugin instead of copy-pasted blocks that drift.
- Much less "which build am I in" code, which is exactly the custom code
that breaks on upgrades.
- **Downsides:**
- `grails-gradle` can no longer be built standalone with its own
`./gradlew build`. Its CI job runs from the root instead.
- Contributors who work only on the Gradle plugins lose that small, fast
entry point.
- Moving the `buildSrc` dependencies requires care with plugin classpath
ordering.
## Explicitly not doing
- **Configuration cache or isolated projects migration:** blocked by
cross-project wiring today; revisit only if it comes cheaply after Phases 2 and
3.
- **Version catalog or Kotlin DSL migration:** moves strings around without
removing any of the problems above.
- **More shards, Develocity Test Distribution, or another cache or mirror
tier:** we already tried and removed Test Distribution (7180d65c13). Use the
cache we already pay for first.
- **More `-P` switches or larger heaps as a fix:** they move the OOM rather
than removing it.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]