Skip to content

Cancel superseded CI runs and skip image builds PRs cannot affect - #855

Merged
f213 merged 1 commit into
masterfrom
ci-cost-guards
Aug 26, 2026
Merged

Cancel superseded CI runs and skip image builds PRs cannot affect#855
f213 merged 1 commit into
masterfrom
ci-cost-guards

Conversation

@f213

@f213 f213 commented Aug 26, 2026

Copy link
Copy Markdown
Member

Motivation

Modern agent-driven development ("vibe-coding") produces many PRs, and CI minutes scale with them: in zoya, a production project running this template's CI shape, GitHub Actions alone began costing ~$100/month. The two biggest line items were superseded runs of the same PR completing pointlessly, and every PR building all three Docker images even when nothing image-affecting changed. This PR ports both cost guards from zoya's CI.

Concurrency guard (both workflows)

Both the template repo's own .github/workflows/ci.yml and the workflow generated projects inherit get a top-level concurrency group keyed on workflow and ref:

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

Pushing a new revision to a PR cancels the now-obsolete run of the previous one. cancel-in-progress is conditional on the event, so a push to master is never cancelled — every master run completes.

Conditional image build (generated project's workflow)

The lint job gains a step, run only on pull requests, that lists the PR's changed files via the GitHub API and checks them against the set that can affect the image build: Dockerfile, uv.lock, pyproject.toml, .dockerignore. The verdict is exposed as a job output, and build-docker-image now skips when a PR touches none of those files:

needs: [lint, test]
if: github.event_name != 'pull_request' || needs.lint.outputs.image-build-affected == 'true'

Adding lint to needs is required to read the output; execution order is unchanged since test already needs lint. Pushes to master always build all three images — the condition only ever skips PR runs.

The generated workflow lives under _copy_without_render in cookiecutter.json, so the added ${{ }} expressions pass through cookiecutter untouched.

🤖 Generated with Claude Code

Modern agent-driven development ("vibe-coding") produces many PRs, and
CI minutes scale with them: in zoya, a production project running this
template's CI shape, GitHub Actions alone began costing ~$100/month.
The two biggest line items were superseded runs of the same PR
completing pointlessly, and every PR building all three Docker images
even when nothing image-affecting changed.

Port both cost guards from zoya:

- A concurrency group per workflow and ref, in both the template repo's
  own CI and the CI generated projects inherit, cancelling in-progress
  runs only for pull requests — a push to master is never cancelled.

- In the generated project's workflow, the lint job asks the GitHub API
  for the PR's changed files and reports whether any of them can affect
  the image build (Dockerfile, uv.lock, pyproject.toml, .dockerignore).
  build-docker-image now also needs lint (execution order is unchanged,
  test already needs it) and skips on PRs that touch none of those
  files; pushes to master always build.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@f213

f213 commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

Если ни у кого нет возражений, я смёрджу

@nvo87 nvo87 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

да, круто. Мы до этого у себя билд образа делали опциональным. И при авто-мердже, в пулл-реквестах он не отрабатывал. А тут еще круче.

@f213
f213 merged commit 1be5a5b into master Aug 26, 2026
3 checks passed
@f213
f213 deleted the ci-cost-guards branch August 26, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants