ci: release from main, enforce zero deprecations, remove duplicate work - #26
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two separate concerns, kept as two commits so either can be reverted without the other.
1. Release on merge to
main(b5f97ab)Merging
develop→mainnow publishes. The tag is derived fromversioninbuild.gradle.ktsrather than supplied alongside it, so the two can no longer disagree.
notice and stops. It does not fail:
mainlegitimately receives merges that are not releases, and ared run on each is an alarm people learn to ignore.
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:
mainaccepts only reviewed PRs, and publishing requires an approvalfrom a named reviewer on a protected environment.
BRANCHING.mdandSECURITY.mdsaid otherwise andare 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.mdsays "never ship a deprecated or scheduled-for-removal API — treat it as ablocker, 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. Verifiedgreen today, so it lands with no debt to forgive.
EXPERIMENTAL_API_USAGESis deliberately excluded —DiffTabCleanupusesProjectCloseListener.projectClosingBeforeSaveknowingly, and it is the only hookthat runs before the workspace is written.
Leaner, measured rather than assumed:
pushandpull_requestboth fire; the concurrency group keyed ongithub.refdiffers between them, so neither cancelled the othercancel-in-progressapplied only to PRskoverVerifydepends on:test, and lived in a different jobverifyPluginalready builds itThe 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
verifyPluginCompatible on IC-251, IC-252, IU-253, IU-261, IU-262 with the stricter failure level.All four workflow files parse. The
actions/download-artifactSHA was checked against the API — the oneI first wrote was invented, and pinning by SHA protects nothing if the SHA is made up.