↑ top
← Back to blog
EN | ES

CI That Writes Back to the Repository

The pattern where CI generates artifacts, commits them to main, and uses [skip ci] to avoid infinite loops. When it's the right tool and when it's not.

✦ AI Summary

CI is a one-directional pipeline: code goes in, artifacts go out, Docker images are built, deployments happen. This site's CI runs in the other direction too. When it publishes a post, it commits them, commits them back to `main', and pushes. The repository is both the source and the target of the CI pipeline.

CI That Writes Back to the Repository

Most CI pipelines are one-directional: code goes in, artifacts go out. Tests run, Docker images are built, deployments happen. The repository is the source; CI is a consumer.

This site's CI runs in the other direction too. When it publishes a post, it generates files, commits them back to main, and pushes. The repository is both the source and the target.

Here's why, how it works, and when you shouldn't do this.

Why CI Writes Back

Publishing a post involves two kinds of outputs: things that should be ephemeral (the deployed worker) and things that should be versioned (the publish artifacts).

The artifacts that CI generates:

  • scripts/kv-seed.json — the serialized KV data for all posts (index, content, summaries)
  • Frontmatter updates — publish.js sets image: true in post frontmatter after uploading a hero image

These need to be in the repository for two reasons:

  1. Recovery. If KV data is lost, the seed file is the source of truth. Without it, you'd need to regenerate everything from scratch.

  2. Future CI runs. A subsequent run that only changes src/worker.js shouldn't need to re-process images and re-seed KV. But it should still be able to seed KV if needed. The seed file makes that possible without a full publish.

The [skip ci] Pattern

The dangerous part of writing back to the repository: if CI pushes a commit and that commit triggers CI, you get an infinite loop. Commits keep triggering commits.

The fix is [skip ci] in the commit message:

yaml- name: Commit artifacts
  run: |
    git config user.name "github-actions[bot]"
    git config user.email "github-actions[bot]@users.noreply.github.com"
    git add scripts/kv-seed.json posts/
    git diff --staged --quiet || git commit -m "chore: update publish artifacts [skip ci]"
    git push

GitHub Actions and most CI providers recognize [skip ci] (or [ci skip]) in a commit message and skip the workflow. The commit lands in main but doesn't trigger another run.

git diff --staged --quiet || git commit is the idempotency guard: if nothing changed (no new images, no modified frontmatter), there's no commit. CI doesn't push empty commits.

The Conditional Trigger

Not every push to main should run the full publish pipeline. Source code changes don't need KV reseeding. Post changes don't need a worker deploy.

GitHub Actions path filters handle this:

yamlon:
  push:
    branches: [main]
    paths:
      - "posts/**"
      - "images/**"
      - "src/**"
      - "scripts/**"
      - "static/**"

Within the workflow, each major step checks which files changed:

yaml- name: Publish posts
  if: contains(steps.changed.outputs.files, 'posts/') || contains(steps.changed.outputs.files, 'images/')
  run: npm run publish

- name: Deploy worker
  if: contains(steps.changed.outputs.files, 'src/')
  run: npm run deploy

A commit that only changes posts/my-post.md runs publish but not deploy. A commit that changes src/worker.js runs deploy but not publish. The [skip ci] auto-commit triggers nothing.

The Merge Conflict Risk

Writing back to a branch that's also being pushed to by developers creates a race condition. If I push to main at the same time CI is trying to push its auto-commit, one of them will fail with a non-fast-forward error.

Mitigations:

  • Use git pull --rebase before the CI push
  • The conflict window is small (seconds between the original push and the auto-commit)
  • For a single-developer repo, it's rarely a problem in practice

If you have a team pushing frequently to main, this pattern becomes riskier. A queue or lock mechanism would be needed.

When Not to Do This

Don't write back if the generated files are large. KV seed files for a blog are small. If you're generating megabytes of artifacts on every commit, the repository grows quickly and git clone becomes slow.

Don't write back if the files change on every run. Build timestamps, checksums that include dates, anything non-deterministic will create a commit on every CI run even when nothing actually changed.

Don't write back if you have strict branch protection. Many teams require PRs for all changes to main. An auto-commit from CI bypasses that. Either add the CI bot as an exception or use a different artifact storage (S3, Artifacts, a separate branch).

The pattern is a pragmatic shortcut that works well for small projects with simple workflows. It breaks down at scale. Know the limits before you adopt it.

Was this helpful?