The Time My

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.
I was genuinely shaking. My heart wouldn't stop racing. It was that moment where I realized: "Oh, I'm screwed."

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.


🤖 Developer Blog & Content

You can find more articles on my personal blog here: muttyan.com Blog

Amazon Special Offers and Free Trials