The Time My "Simple" GitHub Actions Setup Triggered an Infinite Loop and a Massive Bill
"Welcome to the age of automation!"
That was my internal monologue as I proudly integrated GitHub Actions into my workflow. I envisioned a sparkling future where every test and deployment was fully automated, skyrocketing my productivity as an engineer.
However, reality hit hard. A few hours later, all I saw was a vertical spike on my GitHub Actions usage graph and a cloud cost notification showing numbers that were clearly abnormal.
I had created a hellscape known as the "CI/CD Infinite Loop."
It All Started with "Automated Versioning"
At the time, this was my goal:
"Automatically bump the version in `package.json` upon release and push it to GitHub. Then, let the deployment pipeline kick in automatically. Perfect, right?"
The blueprint seemed flawless. My YAML file looked like this:
name: Auto Version Bump and Deploy
on:
push:
branches:
- main
jobs:
bump-version:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Update version
run: |
npm version patch -m "chore: bump version to %s [skip ci]"
# I intended to include [skip ci] here (this was the crucial part)
- name: Push changes
run: |
git push origin main # ← This is where everything went wrong
At first glance, it seemed fine. By including `[skip ci]` in the commit message, that specific push shouldn't have triggered the workflow again...
...Or so I thought.
Why Did It Fall into an Infinite Loop?
The cause lay in the behavior of the GitHub Actions `token`, the `git push` command, and my own "assumptions."
I was using the default `GITHUB_TOKEN`. In certain conditions—especially depending on permission settings or combinations with other actions—pushes made with this token can still be recognized as "new events," triggering the `on: push` hook again.
To make matters worse, my deployment script included a step that modified other environment variables and configuration files. Because of this, some commits were generated without the `[skip ci]` tag attached.
1. A push is made to `main`.
2. The Workflow starts.
3. The version is bumped and `git push` is executed.
4. A "new push" is detected!
5. The Workflow triggers again... (and the loop continues).
The Terrifying Numbers in Front of My Eyes
Only 30 minutes after starting, I opened the GitHub Actions tab. The number of running jobs was climbing: "10, 20, 30..."
It looked like someone was intentionally trying to stress-test the system.
- Actions Usage Time: Surpassed 450 minutes in just 2 hours (the usual daily average is about 10 minutes).
- CPU Usage: I could practically hear the GitHub runners screaming under the load.
- Cost Projection: At this rate, the deployment costs for this month alone were guaranteed to hit several tens of thousands of yen.
The Solution: How I Should Have Written It
To escape this nightmare, I learned the ironclad rules for a "proper" CI/CD pipeline:
1. Leverage `paths-ignore`
Don't run heavy tests for pushes that don't involve core code (like READMEs or documentation). This is the baseline.2. Control Concurrency
Use the `concurrency` setting to ensure multiple jobs don't run for the same branch simultaneously. This automatically cancels older jobs, minimizing the damage of a potential loop.3. Exclude Specific Users/Tokens
This was the most effective fix. Specify the name of your deployment bot so that the system ignores pushes made by that specific entity.[The Optimized YAML Example]
name: Robust CI/CD Pipeline
on:
push:
branches:
- main
# Do not run for documentation updates (save costs)
paths-ignore:
- '**.md'
- 'docs/**'
# Control concurrency (automatically cancel in-progress jobs)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
test-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1. Test Phase (If this fails, the following steps won't run)
- name: Run Tests
run: npm test
# 2. Deployment Phase
- name: Deploy to Staging
if: github.event_name == 'push'
run: |
echo "Deploying..."
# Insert deployment commands here
Summary: Automation Must Be Paired with Monitoring
The biggest lesson I learned from this experience is: "The more automated a system is, the greater the damage it can cause when it goes haywire."
CI/CD is incredibly convenient, but you cannot leave it unattended.
- Set up alerts for GitHub Actions usage limits.
- Use `concurrency` to prevent overlapping jobs.
- Always use `paths-ignore` to prune unnecessary executions.
If you neglect these, you'll end up like me—staring at a massive bill in the middle of the night and questioning your life choices.
If you’re currently thinking, "I should automate this!", take a moment to pause and consider the possibility of an infinite loop. It’s not worth the headache.