Skip to content

ci: release from main, enforce zero deprecations, remove duplicate work - #26

Merged
serialexperimentslainnnn merged 5 commits into
developfrom
feature/update-pipelines
Aug 5, 2026
Merged

serialexperimentslainnnn merged 5 commits into
developfrom
feature/update-pipelines

Conversation

@serialexperimentslainnnn

Copy link
Copy Markdown
Owner

Two separate concerns, kept as two commits so either can be reverted without the other.

1. Release on merge to main (b5f97ab)

Merging develop → main now publishes. The tag is derived from version in build.gradle.kts
rather than supplied alongside it, so the two can no longer disagree.

  • A merge that does not bump the version publishes nothing — the workflow finds the tag, logs a
    notice and stops. It does not fail: main legitimately receives merges that are not releases, and a
    red run on each is an alarm people learn to ignore.
  • Pushing a tag by hand still works, as the escape hatch for re-cutting after a failed publish.
  • The tag is cut after the approval and the publish. Created earlier it would name a version that
    was never published when a build fails or an approval is declined — and since tags here are immutable,
    that would block the next attempt.

What this costs

The tag is signed by the CI key, not the YubiKey — which cannot sign inside a runner, and whose
non-exportability is what makes it worth trusting. The chain still ends in hardware, because the CI key
is certified by it.

But no signature on a release now asserts that a person authorised it. That claim moves entirely to
the two gates around publication: main accepts only reviewed PRs, and publishing requires an approval
from a named reviewer on a protected environment. BRANCHING.md and SECURITY.md said otherwise and
are corrected — a verification instruction that overstates what it proves is worse than none.

Still open: ADR 0001 describes the tag-triggered flow and needs superseding.

2. Stricter, and four duplications removed (9ad75f5)

Stricter. CLAUDE.md says "never ship a deprecated or scheduled-for-removal API — treat it as a
blocker, not a warning", and nothing enforced it: the verifier's default failure level omits
DEPRECATED_API_USAGES, so such a usage was reported and the job went green. Now it fails. Verified
green today, so it lands with no debt to forgive. EXPERIMENTAL_API_USAGES is deliberately excluded —
DiffTabCleanup uses ProjectCloseListener.projectClosingBeforeSave knowingly, and it is the only hook
that runs before the workspace is written.

Leaner, measured rather than assumed:

Duplication Cause
Every commit ran the pipeline twice push and pull_request both fire; the concurrency group keyed on github.ref differs between them, so neither cancelled the other
Superseded runs kept going cancel-in-progress applied only to PRs
The JVM suite ran twice koverVerify depends on :test, and lived in a different job
The plugin was built twice verifyPlugin already builds it

The last one was not only wasteful but subtly wrong: the artifact assertions ran against bytes that were
never verified. They now run on the verified artifact.

Job display names are unchanged on purpose — a ruleset references a required check by name, so
renaming one does not fail the gate, it silently stops applying it. No rulesets need reapplying.

Verified locally

verifyPlugin Compatible on IC-251, IC-252, IU-253, IU-261, IU-262 with the stricter failure level.
All four workflow files parse. The actions/download-artifact SHA was checked against the API — the one
I first wrote was invented, and pinning by SHA protects nothing if the SHA is made up.

Merging `develop` into `main` now publishes. The tag is DERIVED from the version
in build.gradle.kts rather than supplied alongside it, so the two can no longer
disagree — the mismatch the old flow guarded against with a comparison simply
cannot occur.

A merge that does not bump the version publishes nothing: the workflow finds the
tag present, logs a notice and stops. It does not fail. `main` legitimately
receives merges that are not releases, and a red run on each of those is an alarm
people learn to ignore. Published tags stay immutable, which is the correction
ADR 0001 records after v4.3.2 and v4.4.1 were each force-re-cut three times.

Pushing a tag by hand still works, as the escape hatch for re-cutting after a
failed publish without an empty commit on main. A tag this workflow creates does
not re-trigger it, so there is no loop.

The tag is cut AFTER the approval and the publish, not in the guard. Created
earlier it would name a version that was never published when a build fails or an
approval is declined — and since tags here are immutable, that would block the
next attempt. Cutting it last makes it mean "this was published", which is the
only claim it can honestly make once the version, not the tag, is the input.

WHAT THIS COSTS, STATED RATHER THAN GLOSSED

The tag is signed by the CI key, not the maintainer's YubiKey — which cannot sign
inside a runner, and whose non-exportability is exactly what makes it worth
trusting. The chain still ends in hardware because the CI key is certified by it.

But no signature on a release now asserts that a person authorised it. That claim
moves entirely to the two gates around publication: `main` accepts only reviewed
pull requests, and publishing requires an approval from a named reviewer on a
protected environment. BRANCHING.md and SECURITY.md said the opposite and are
corrected — a verification instruction that overstates what it proves is worse
than none, because someone acts on it.

ADR 0001 still describes the tag-triggered flow and needs superseding.
STRICTER — the policy is now a gate rather than a promise

CLAUDE.md says "never ship a deprecated or scheduled-for-removal API — treat it
as a blocker, not a warning". Nothing enforced it. The verifier's default failure
level is COMPATIBILITY_PROBLEMS + INTERNAL_API_USAGES + OVERRIDE_ONLY_API_USAGES,
so a deprecated usage was reported and the job went green anyway. A rule that
lives only in prose is not a rule; DEPRECATED_API_USAGES is what makes the
sentence true. Verified green today, so it lands with no debt to forgive.

EXPERIMENTAL_API_USAGES is deliberately excluded, and that is a decision rather
than an oversight: DiffTabCleanup uses ProjectCloseListener.projectClosingBeforeSave
knowingly, because it is the only hook that runs before the workspace is written.
An experimental API is acceptable with a reason. A deprecated one is not, because
it has an announced removal and this plugin must keep working across 251 → 262.

LEANER — four measured duplications, none of them a weakened gate

- Every commit ran the pipeline TWICE. `push` on topic branches and
  `pull_request` both fire, and the concurrency group keyed on `github.ref`
  differs between them (refs/heads/x vs refs/pull/N/merge), so neither cancelled
  the other. Keyed on the commit now: same SHA, same group, duplicate cancelled.
- Superseded runs were only cancelled for pull requests. Three pushes in a row
  left three full pipelines racing, each spending ten minutes downloading IDEs
  for a commit that had already been replaced.
- The JVM suite ran twice. `koverVerify` depends on `:test`, so putting it in the
  `Static analysis` job re-ran the entire suite on a second runner with a cold
  cache. Coverage is a property OF a test run and now shares its job.
- The plugin was built twice. `verifyPlugin` already produces the distributable;
  `Build plugin` built its own. That was not only wasteful but subtly wrong — the
  bytes being asserted were never the bytes that were verified. It now downloads
  the verified artifact.

Job DISPLAY NAMES are unchanged, deliberately: a ruleset references a required
check by its name, so renaming one does not fail the gate — it silently stops
applying it. No rulesets need reapplying.

Also gives drift.yml the concurrency group it was missing (queue, do not cancel:
a half-written drift report is worse than a late one).

Caught while writing this: the SHA I pinned actions/download-artifact to was
invented. Verified against the API and corrected. Pinning by SHA protects nothing
if the SHA is made up.
Nine open Dependabot PRs, each firing the full pipeline — verifier included, ten
minutes and 1.25 GB of IDE downloads apiece — for dependency bumps that are
reviewed in seconds.

Five of them were SECURITY updates (undici, ip-address, fast-uri, hono, postcss:
exactly the `npm audit` findings). Two things about that stream were not
understood when this file was written, and both are documented at the setting now:

  - security updates ignore `open-pull-requests-limit` entirely, which is how five
    arrived under a limit of three;
  - the existing group did not cover them, because `applies-to` defaults to
    version-updates.

So each ecosystem now has an explicit `applies-to: security-updates` group. These
are transitive devDependencies that are never distributed — `npm audit --omit=dev`
reports 0, and the artifact contains zero node_modules entries — so reviewing them
one at a time bought nothing.

github-actions and gradle had no grouping at all. Actions are pinned by full
commit SHA, so a bump is a one-line change per action; grouping them costs no
review fidelity. Gradle groups minor and patch only: a MAJOR keeps its own PR
deliberately, because that is the ecosystem where a bump can hang the headless
suite — the 2.16 -> 2.18 platform-plugin attempt did exactly that, and it deserved
its own run and its own decision.

Syntax verified against GitHub's Dependabot options reference rather than written
from memory, after inventing an action SHA earlier today.
I edited build.gradle.kts and ran verifyPlugin and `help` against it, but not
spotlessCheck — so `Static analysis` failed on spotlessKotlinGradleCheck for a
purely mechanical reason. The formatter is a gate like any other and running a
subset of the gate is the same as not running it.
@serialexperimentslainnnn
serialexperimentslainnnn merged commit a1997de into develop Aug 5, 2026
15 of 21 checks passed
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