Skip to content

fix(linear-release-sync): classify release stability by github prerelease flag#156

Open
Piotr1215 wants to merge 3 commits into
mainfrom
devops-1006/release-sync-dedup
Open

fix(linear-release-sync): classify release stability by github prerelease flag#156
Piotr1215 wants to merge 3 commits into
mainfrom
devops-1006/release-sync-dedup

Conversation

@Piotr1215

@Piotr1215 Piotr1215 commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Loft Bot double-commented "Now available in stable release vX.Y.Z" on already-released Linear issues (DEVOPS-1006, e.g. ENG-8307 on the vCluster 0.28 line).

The action decided "is this a shippable stable release?" by parsing the tag string (isStableRelease via Masterminds Prerelease()). vCluster's backport patch tags like v0.28.2-patch.1 are published with prerelease=false on GitHub but read as semver-prereleases because of the -patch.N suffix, so the action misclassified them. (The old inline hack/linear-sync still running on release branches did the opposite via a substring blocklist and had no dedup, which is what actually produced the live duplicates.)

This PR switches the classification to the signal GitHub already records correctly for every vCluster tag type:

Tag type GitHub prerelease treated as
v0.34.4 (stable) false shippable
v0.28.2-patch.1 (patch) false shippable
v0.35.0-rc.9, -alpha.8 true skipped

Key Changes

  • releases.go: add IsPrerelease to the fetched Release (GraphQL isPrerelease).
  • main.go: classify with releaseIsStable(currentRelease) (!IsPrerelease) and pass the result down; log the decision.
  • linear.go: MoveIssueToState takes isStable as a parameter and routes through decideReleaseAction (skip / move-to-released / stable-comment); removed the isStableRelease tag-string classifier.
  • Tests: TestReleaseIsStable and TestDecideReleaseAction drive the real decision functions, so an inverted classification fails (confirmed by mutation testing). Added IsPrerelease parsing assertion in FetchReleaseByTag. Removed dead MockLinearClient.MoveIssueToState.
  • README.

Root cause

ENG-8307 received two identical "Now available in stable release" comments for the same tag:

Tag Comment 1 Comment 2 Gap
v0.28.2-patch.1 2026-06-17 14:40 2026-06-17 16:28 ~1h48m
v0.30.4 2025-12-17 17:20 2025-12-17 17:42 ~21m

Both pairs are the same tag, so this is not the DEVOPS-874 wrong-previous-tag case. The "double" is two repositories (vcluster and vcluster-pro) releasing the same version tag, each running its own Linear sync against the shared Linear issue: one comment per repo.

The release-branch jobs do not use the pinned action binary. A release-triggered workflow runs from the tag's ref, so a patch/backport tag on an old branch runs that branch's old release.yaml and inline hack/linear-sync, never the migrated action on main. That old code has three defects:

  1. isStableRelease is a substring blocklist (-alpha, -beta, -rc, -dev, -pre, -next) that omits -patch, so v0.28.2-patch.1 classifies as stable and fires the "now available in stable" comment on already-released issues. This is Denise's "None"/patch release-type intuition.
  2. No comment deduplication: the old linear.go posts unconditionally, so a second repo releasing the same tag always re-comments. Dedup was only ever added to the extracted action.
  3. Over-broad previous-tag: for v0.28.2-patch.1, predecessor resolved to v0.27.3, a 72-issue compare range that re-touches everything already shipped in 0.28.x.

The deeper issue: both the old and new classifiers re-derive "is this a release?" from the tag string and both get -patch.N wrong in opposite directions (substring blocklist over-fires, Masterminds Prerelease() skips). GitHub already records the author's intent on the release object and it is correct for every tag type vCluster ships, which is what this PR switches to.

Dependencies / follow-ups

This is step 1 of the fix (action-level correctness). It does not fully resolve DEVOPS-1006 on its own:

  • Rebuild the linear-release-sync/v1 release binary to include this change (force-pushing the tag does not rebuild; use workflow_dispatch of release-linear-release-sync.yaml).
  • Migrate the release-branch sync_linear job to this shared action on active vcluster + vcluster-pro release branches (deletes the no-dedup / substring-blocklist inline code; tag-scoped dedup then kills the cross-repo duplicate).
  • Scope the compare range to the patch delta (pass previous-tag explicitly for patches) so already-released issues are not re-announced.
  • Product decision (Denise): keep the -patch.N scheme, or use real patch versions / build metadata to avoid the semver-prerelease trap.

Testing

make test-linear-release-sync (all green), go build, go vet, gofmt clean on changed files. Mutation-checked: inverting releaseIsStable or the isStable branch in decideReleaseAction turns the -patch.N regression rows red.

References DEVOPS-1006

…ease flag

Loft Bot double-commented "Now available in stable release vX.Y.Z" on
already-released Linear issues (DEVOPS-1006, e.g. ENG-8307 on the vCluster 0.28
line). The trigger was vCluster backport patch tags like v0.28.2-patch.1: they
are published with prerelease=false on GitHub but read as semver-prereleases
because of the -patch.N suffix.

The action decided "is this a shippable stable release?" by parsing the tag
string (isStableRelease via Masterminds Prerelease()), which classified -patch.N
as a prerelease and skipped it. The old inline hack/linear-sync still running on
release branches used a substring blocklist that did the opposite and treated
-patch.N as stable with no dedup. Neither matched intent.

GitHub already records the author's intent on the release object, and it is
correct for every vCluster tag type: stable and -patch.N are prerelease=false,
-rc/-alpha are prerelease=true. Drive the decision from that flag instead of the
tag string. This makes the action correct for the patch release type so release
branches can migrate to it, where its tag-scoped dedup prevents the cross-repo
duplicate (vcluster and vcluster-pro cut the same tags).

Removes the isStableRelease tag-string classifier, adds IsPrerelease to the
fetched release, and passes isStable into MoveIssueToState.

References DEVOPS-1006
Comment thread .github/actions/linear-release-sync/src/linear_test.go Outdated
Comment thread .github/actions/linear-release-sync/src/linear_test.go Outdated
Comment thread .github/actions/linear-release-sync/INVESTIGATION-DEVOPS-1006.md Outdated
loft-bot review on #156 flagged that the prerelease-flag tests asserted
tautologies: they computed isStable := !isPrerelease and checked it against
wantStable, which every row set to !isPrerelease, so the identity (!x == !x)
could never fail. If the production decision were ever inverted, the
DEVOPS-1006 regression they document would still pass, guarding nothing.

extract the two decisions into named functions the tests drive directly:
- releaseIsStable(release) in main.go: the !IsPrerelease classification.
- decideReleaseAction(...) in linear.go: the skip / move-to-released /
  stable-comment branch selection MoveIssueToState makes before any Linear
  API call. MoveIssueToState now switches on it; behavior is unchanged.

TestReleaseIsStable and TestDecideReleaseAction now call the real functions,
so an inversion fails (verified by mutation: flipping either decision turns
the -patch.N row red). also drop the dead MockLinearClient.MoveIssueToState
(stale signature, never called) and move the DEVOPS-1006 RCA out of the
versioned action dir into the PR description, where a one-time ticket
investigation belongs.

References DEVOPS-1006
… github prerelease flag

DEVOPS-1006 follow-up. The prior fix on this branch classified a release as
shippable from GitHub's prerelease flag on the release object. Denise's call
(2026-06-19): go by the tag name instead. The flag carries more manual human
error on which release type to pick, and vCluster usually publishes a release as
a prerelease first and promotes it to stable later, so the flag is a moving target
the sync can read at the wrong moment (the workflow fires on release:created,
before promotion).

Classify from the tag: stable when there is no semver prerelease component or it
is the backport patch marker (patch / patch.N), so v0.28.2-patch.1 syncs as a real
release; -rc/-alpha/-beta/-dev/-pre/-next are prereleases and are skipped. Using an
allowlist (no-suffix or patch) rather than a prerelease blocklist means an unknown
future suffix defaults to prerelease, the safe direction. Avoids the Masterminds
Prerelease()=="" trap that skipped -patch.N. Drop the IsPrerelease field added to
the fetched release.

Tag-scoped dedup is unchanged; it remains the fix for the cross-repo double comment
once release branches migrate to this action.

References DEVOPS-1006
@Piotr1215 Piotr1215 requested a review from sydorovdmytro June 26, 2026 07:27
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.

2 participants