Things I Automated That Probably Didn't Need Automation

  • java
  • automation
  • lessons-learned
  • backend
  • devops

I love automating things. I've been automating things for over a decade. I've automated things that definitely needed automating, and I've automated things that had been doing fine on their own for years without my help.

Here are the latter.


The Slack Deployment Notification Bot

What I built: A Spring Boot scheduled task that polled our GitLab CI API every minute, detected new deployments, and posted formatted messages to a Slack channel with deployment details, committer, and a link to the pipeline.

Why it seemed like a good idea: The team kept asking "did the staging deploy happen yet?" The answer was always "check GitLab." A bot would eliminate that question.

What actually happened: The bot worked for three weeks. Then the GitLab API token expired. Nobody noticed because the Slack channel went quiet and we assumed there just hadn't been any deployments.

When I fixed the token, I added retry logic so transient failures wouldn't cause silent gaps. Then I added an alert when retries were exhausted, because silent failures were the original problem. Then I had to monitor the alerting, because the alerting was also a service that could fail silently.

By the end, I had a service that monitored a service that posted about a service, with its own logs and its own restart policy and its own on-call concern. It was more complex than the deployments it was monitoring.

GitLab CI has native Slack integration. It took 90 seconds to configure. I deleted the bot.


Auto-Rotating Log Files in the Application

What I built: A Spring @Scheduled task that ran every midnight and moved the current application.log to application-YYYY-MM-DD.log, then deleted logs older than 14 days.

Why it seemed like a good idea: Log files were growing without bound on a long-running service. Something needed to manage them.

What actually happened: It worked. For months. Until someone noticed that the log files weren't actually rotating — they were being renamed but the application still had the file descriptor open, so new log entries kept going to the now-renamed file. The "current" log was empty, all the logs were in the dated files, and nobody's log parser was looking in the right place.

Then I discovered that Logback, the logging framework already bundled in every Spring Boot application, has had configurable rolling file appenders since before I started writing Java:

<appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
  <file>logs/application.log</file>
  <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
    <fileNamePattern>logs/application-%d{yyyy-MM-dd}.log</fileNamePattern>
    <maxHistory>14</maxHistory>
    <totalSizeCap>500MB</totalSizeCap>
  </rollingPolicy>
  <encoder>
    <pattern>%d{ISO8601} %-5level [%t] %logger{36} - %msg%n</pattern>
  </encoder>
</appender>

Three days of development, replaced by nine lines of XML that had been available all along. I now check the docs of whatever framework or library I'm using before writing any infrastructure code. It's almost always there.


The Monorepo Version Bumper Script

What I built: A 200-line Python script that traversed a multi-module Maven monorepo, found every pom.xml, and updated version strings from 1.x.x-SNAPSHOT to 1.y.0-SNAPSHOT according to a provided target version.

Why it seemed like a good idea: Manual version bumping across 12 modules meant 12 pom.xml files to edit, plus the parent pom.xml, plus the dependency declarations between modules. It was tedious and error-prone.

What actually happened: The script worked for the common case. For the 80% of version bumps where all modules were in sync, it was great. For the 20% where individual modules had independent versioning, or where a pom.xml had a non-standard namespace declaration, or where a module referenced a third-party BOM with a similar version pattern, the script silently produced wrong versions. We shipped two releases with incorrect module dependency versions before we found the pattern.

The fix that should have been the first approach:

mvn versions:set -DnewVersion=1.5.0-SNAPSHOT -DprocessAllModules=true
mvn versions:commit

Two commands. Part of the standard Maven plugins. Handles parent-child relationships, dependency declarations, and edge cases correctly. The Maven Versions Plugin has been doing this for years.

My script was worse than the thing it replaced.


The Stale PR Labeler

What I built: A GitLab webhook handler (another Spring Boot service, naturally) that listened for merge request events, tracked last-activity timestamps, and added a "stale" label to MRs with no activity in 7 days. It also posted a comment: "@author this MR has been open for 7 days with no activity."

Why it seemed like a good idea: We had a graveyard of half-finished MRs dragging down the review queue. The bot would nudge people into action.

What actually happened: The team started ignoring the bot's comments. After a week of @-mention noise, the pattern was established: if the only comment on an MR is a bot, it's probably not urgent. Then they started ignoring all bot comments. Then they started skimming all comments — because "it might just be the bot."

We had automated ourselves into a state where automated review reminders were counterproductive to actual code review.

GitLab has a built-in "mark as draft" feature. The real solution was a team norm: if an MR isn't ready for review, mark it as draft. If it's been draft for 2 weeks, close it. A Slack message every two weeks was enough to establish the habit. No code required.


The Custom Health Check Aggregator

What I built: A Spring Boot service that polled the /health endpoint of 15 downstream services every 5 minutes, aggregated the results into a summary, and emailed the team a health report every morning.

Why it seemed like a good idea: We had 15 services and no unified view of which ones were healthy. The email would be a daily sanity check.

What actually happened: The email became noise that everyone skimmed. When services actually went down, the alert came in the morning email — hours after the incident. Meanwhile, the health aggregator service itself went down twice, and nobody noticed for two days because the absence of morning emails was... peaceful.

Prometheus and Alertmanager solve this correctly. Prometheus scrapes /actuator/prometheus on every service. Alertmanager fires when a service goes down and routes to the right channel based on severity and time of day. Setup takes a couple of hours; I'd spend three weeks on the custom aggregator.

# alertmanager/config.yml
receivers:
  - name: slack-ops
    slack_configs:
      - channel: '#alerts'
        text: '{{ range .Alerts }}{{ .Annotations.summary }}{{ end }}'

route:
  receiver: slack-ops
  group_wait: 30s
  group_interval: 5m
  repeat_interval: 4h

The pattern across all of these is the same: I had a real problem, I looked for a solution in my toolbox (write code), and I didn't check whether the solution already existed in the tools I was already using.

The best automation is the one that already exists. Check the docs before writing the code.

← back to blog