Skip to content

ci: skip the pipeline for documentation-only changes - #1949

Merged
efiten merged 1 commit into
Kpa-clawbot:masterfrom
efiten:ci/skip-pipeline-for-docs-only
Sep 3, 2026
Merged

ci: skip the pipeline for documentation-only changes#1949
efiten merged 1 commit into
Kpa-clawbot:masterfrom
efiten:ci/skip-pipeline-for-docs-only

Conversation

@efiten

@efiten efiten commented Sep 3, 2026

Copy link
Copy Markdown
Collaborator

Two documentation-only PRs were running the full pipeline simultaneously this afternoon: #1948 (CHANGELOG.md plus a release note) and #1947 (deleting a stale docs/DEPLOYMENT.md). Each spends about 12 minutes on Go Build & Test and about 16 minutes on Playwright to establish that a text file does not break a browser.

The cost is the queue, not the minutes. On the same afternoon a pull_request run was created at 12:13 and its first job did not start until 16:19. Four hours in the queue. Every unnecessary run pushes the ones that matter further back, and this repository has been merging heavily today.

Checked before adding the filter

Rather than assumed:

  • Nothing reads markdown at build or test time. Grepping every Go and JS source for a runtime read (ReadFile, readFileSync, os.Open) of a .md path returns nothing. The docs/ matches in cmd/ and test-*.js are all comments pointing at documentation.
  • /api/docs serves Swagger UI generated from cmd/server/openapi.go, not from docs/.
  • docs/ holds markdown plus screenshots (png, gif) and no build input.
  • This workflow has no tag trigger, so release tagging is unaffected; that runs from release-fast-path.yml. Worth stating explicitly given a v3.10.0 tag is imminent.

paths-ignore skips only when every changed file matches, so a PR touching both code and documentation still runs the full pipeline.

The trap, stated in the file

If required status checks are ever enabled on master, a skipped workflow never reports, and a docs-only PR would wait forever on a check that cannot arrive. At that point this needs to become a change-detection job with conditional heavy jobs rather than a trigger filter.

Master has no required checks today. Verified: the branch protection endpoint returns 404.

That caveat is in a comment above the on: block, not just in this description, because the person who enables required checks in six months will be reading the workflow and not this PR.

Note

This PR itself changes only .github/workflows/deploy.yml, so it is not documentation-only and will run the full pipeline, as it should.

Two documentation-only PRs were running the full pipeline at the same time
today: one changing CHANGELOG.md plus a release note, one deleting a stale
docs/DEPLOYMENT.md. Each takes about 12 minutes of Go Build & Test and about 16
minutes of Playwright to establish that a text file does not break a browser.

The cost is not the runner minutes, it is the queue. On the same afternoon a
pull_request run was created at 12:13 and its first job did not start until
16:19, four hours later. Every unnecessary run pushes the ones that matter
further back.

Checked before adding the filter, rather than assumed:
- Nothing reads markdown at build or test time. Grepping every Go and JS source
  for a runtime read (ReadFile, readFileSync, os.Open) of a .md path returns
  nothing. The matches on "docs/" in cmd/ and test-*.js are all comments.
- /api/docs serves Swagger UI generated from cmd/server/openapi.go, not from
  docs/.
- docs/ holds markdown plus screenshots (png, gif) and no build input.
- This workflow has no tag trigger, so release tagging is unaffected; that runs
  from release-fast-path.yml.

paths-ignore skips only when EVERY changed file matches, so a PR touching both
code and documentation still runs the full pipeline.

Stated in the file, because it is the trap in this pattern: if required status
checks are ever enabled on master, a skipped workflow never reports and a
docs-only PR waits forever on a check that cannot arrive. It would then need to
become a change-detection job with conditional heavy jobs rather than a trigger
filter. Master has no required checks today, verified: the branch protection
endpoint returns 404.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PQS3XLoPD98yu9pxdRujqg
@efiten
efiten merged commit 55aabaa into Kpa-clawbot:master Sep 3, 2026
6 checks passed
efiten added a commit that referenced this pull request Sep 3, 2026
Follow-up to #1949, correcting a pattern I did not check before
proposing it.

#1949 used `'**/*.md'`. That reads as requiring a directory component,
so it covers `docs/release-notes/v3.10.0.md` but not `CHANGELOG.md` or
`README.md` at the repository root. Since `paths-ignore` skips only when
**every** changed file matches, one uncovered root file is enough to run
the whole pipeline anyway.

GitHub's own example for "any file with this extension" is `'**.js'`,
with no slash. `'**/*.md'` does not appear in their documentation at
all. `'**.md'` covers root and subdirectories both.

## What this does not establish

#1948 (`CHANGELOG.md` plus a release note) did start a full run after
#1949 landed, and that is what prompted this. But there is a second
candidate explanation I did not rule out: that PR's branch predates
#1949, so its workflow file may simply not have carried the filter yet.

I am not claiming to have proven which one it was. The fix is correct
either way, and shipping the documented pattern is better than defending
the first cause I noticed. The real test is the next docs-only PR opened
from a branch that already contains the filter.

The reasoning is in a comment above the `on:` block, not only in this
description.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
efiten added a commit that referenced this pull request Sep 3, 2026
**v3.10.0 produced no container image.** The build job reported
`success` and pushed nothing.

## What happened

`release-fast-path.yml` re-tags `:edge` to `:vX.Y.Z` when the `:edge`
revision label matches the tagged commit, and dispatches `deploy.yml`
when it does not.

For v3.10.0 the tagged commit was documentation-only. The `paths-ignore`
added in #1949 means documentation-only commits skip `deploy.yml`, so no
`:edge` image was ever built for that commit, the labels did not match,
and the fallback ran. **That part worked exactly as designed** and
correctly refused to re-tag an image built from a different commit.

Then `deploy.yml` skipped all five GHCR steps, because each was gated on
`github.event_name == 'push'` and a `workflow_dispatch` is not a push:

```
4. Build Go Docker image (local staging):  success
5. Set up Docker Buildx:                   skipped
7. Log in to GHCR:                         skipped
9. Build and push to GHCR:                 skipped
```

**So the fallback has never been able to publish an image.** It
dispatches a pipeline that cannot push. That stayed invisible for as
long as the fast path kept succeeding, which it did until a release note
happened to be the last commit before the tag.

## Fix

Gate those five steps on a push **or** a tag ref:

```yaml
if: ${{ github.event_name == 'push' || startsWith(github.ref, 'refs/tags/v') }}
```

A dispatch aimed at a tag now publishes. A dispatch aimed at a branch
still does not, so this does not turn every manual run into a release.

## The version stamp needed no change

Verified rather than assumed. `Compute build metadata` keys on
`GITHUB_REF`, not on the event:

```bash
if [[ "$GITHUB_REF" == refs/tags/v* ]]; then APP_VERSION="${GITHUB_REF#refs/tags/}"; else APP_VERSION="edge"; fi
```

The failed v3.10.0 run already logged `Build: version=v3.10.0
commit=5bad23b`. Only the publishing was missing.

## Not covered here

`Release Artifacts` failed on the same run for an unrelated reason: the
GitHub release had been created by hand before the workflow reached it,
and `action-gh-release` cannot update an immutable release. That one is
process, not code. Push the tag and let the workflow create the release.

Once this merges, re-dispatching `deploy.yml` against `v3.10.0`
publishes the images for the existing tag. No re-tagging needed.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
efiten added a commit that referenced this pull request Sep 4, 2026
…#1955)

Same change as #1954, opened from a branch on this repository instead of
from a fork. #1954 never received a workflow run: zero runs and zero
check suites for its head commit, and closing and reopening it changed
nothing. A manual `workflow_dispatch` on master ran immediately, so
Actions itself is working; the `pull_request` event from the fork is
what produces nothing. See the note at the end.

Replaces the trigger-level `paths-ignore` from #1949 and #1950. That
approach was wrong, and it is currently blocking #1953 from merging.

## What was wrong

GitHub documents the distinction I had backwards:

> a workflow skipped by path filtering keeps its checks **pending** and
blocks the merge, while a **job** skipped by an `if:` conditional
reports **Success** and does not.

So the filtering has to live on the jobs, not on the trigger.

I compounded it by claiming, in both the commit and the description of
#1949, that master had no required checks: *"verified: the branch
protection endpoint returns 404"*. **That verification was invalid.** A
404 there means the token cannot read protection details, not that none
exist. The branch reports `protected=true`, and #1953 was refused with
`the base branch policy prohibits the merge`.

## What this does

A `🔎 Change scope` job computes whether anything outside `docs/`, `*.md`
and `LICENSE` changed. `go-test`, `e2e-test`, `build-and-publish` and
`release-artifacts` are gated on its output. A documentation-only pull
request skips those jobs, they report Success, and the PR can merge.

**Only pull requests are scoped.** A push or a dispatch always runs the
full pipeline.

That second part is deliberate and it fixes a separate failure.
`release-fast-path.yml` re-tags `:edge` to `:vX.Y.Z` only when the
`:edge` revision label matches the tagged commit. A master commit with
no image breaks tagging, which is what happened to the v3.10.0 tag: the
tagged commit was documentation-only, the fast path could not re-tag, it
fell back to a dispatch, and the dispatch published nothing (#1951).
Master pushes now always produce an image. The cost is running the
pipeline on documentation commits to master; pull requests are where the
queue pressure was.

Two conservative defaults in the scope check: a non-`pull_request` event
and an empty diff both count as code, so an unexpected shape runs
everything rather than silently skipping.

## Note for whoever has repository settings access

Fork pull requests stopped getting workflow runs between 22:36 and
05:40. #1949, #1950 and #1951 all came from the same fork and each got a
run; #1954 got none, with no check suite created at all, which is
different from a skipped run. `repos/.../actions/permissions` returns
403 for a non-admin token, so this could not be confirmed from the API.
If the "Fork pull request workflows from outside collaborators" setting
was tightened, that would explain it, and it would affect every outside
contributor, not just this branch.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
efiten added a commit that referenced this pull request Sep 4, 2026
v3.10.0 was tagged and then withdrawn. **Nothing was ever available
under that number**: no container image and no release asset was ever
published, so no user could have pulled it.

This renames the notes and the CHANGELOG section. No product code
changes.

## Why it had to be renumbered

Three things, in the order they bit.

**1. The image never built.** `release-fast-path.yml` re-tags `:edge` to
`:vX.Y.Z` when the `:edge` revision label matches the tagged commit, and
dispatches `deploy.yml` when it does not. The tagged commit was
documentation-only, so the `paths-ignore` from #1949 meant no `:edge`
existed for it and the fallback ran. That part behaved correctly. The
fallback then published nothing, because every GHCR step was gated on
`github.event_name == 'push'` and a dispatch is not a push. It built
locally, reported `success`, and pushed nothing.

Fixed in #1951, but that fix is not in the `v3.10.0` tag, and a
`workflow_dispatch` runs the workflow file **from the ref it targets**.
So the existing tag could not be made to publish.

**2. The assets never uploaded.** I created the GitHub release by hand
before the workflow reached it, and `action-gh-release` cannot update an
immutable release. The correct procedure is to push the tag and let the
workflow create the release.

**3. The tag name cannot be reused.** GitHub's immutable releases keep a
tag name reserved even after the release is deleted:

```
remote: - Cannot create ref due to creations being restricted.
```

I established that only after deleting the release, which is the wrong
order. The lesson, written into the commit message so it survives: check
whether a tag can be rewritten before removing anything that depends on
it.

## What is in v3.10.1

The same 111 commits, plus the three CI fixes that landed after the
v3.10.0 tag (#1949, #1950, #1951). Those are listed in their own section
in the notes. **No product code differs** from what was tagged as
v3.10.0.

All 69 SHA references in the notes were re-verified after the rename.

## Procedure for this tag

Push the tag and stop. The workflow creates the release and attaches the
assets. Do not create it by hand.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant