Skip to content

ci: enrich cherry-pick PRs with Linear refs + future-proof sync workflow#30639

Merged
ashleeradka merged 4 commits into
mainfrom
devin/1778709802-linear-release-cherry-pick-enrichment
May 13, 2026
Merged

ci: enrich cherry-pick PRs with Linear refs + future-proof sync workflow#30639
ashleeradka merged 4 commits into
mainfrom
devin/1778709802-linear-release-cherry-pick-enrichment

Conversation

@ashleeradka
Copy link
Copy Markdown
Contributor

@ashleeradka ashleeradka commented May 13, 2026

Prompt / plan

Linear Releases sync (PR #30622, merged) is dormant for this repo because of three structural gaps in how Linear's Release CLI consumes commit history when the source repo ships via cherry-pick PRs. This PR closes all three, plus adds future-proofing.

The three gaps

  1. Cherry-pick squash commits drop Linear refs. cherry-pick-to-release.yml accumulates multiple cherry-picks into one PR per release branch. When that PR squash-merges, the squash body concatenates only the titles of the cherry-picked commits — not their bodies. Any Closes LUM-XXX in the original main PRs is lost.

  2. Release branches aren't ancestors of each other. git merge-base --is-ancestor v0.8.0 v0.8.1 returns false. The Linear CLI uses merge-base --is-ancestor to filter candidate base SHAs; when none qualifies it falls back to inspecting only HEAD. Combined with (1), scheduled releases would stamp 0 issues even after backfill.

  3. Magic-word requirement. Linear's extractor only catches LUM-NNNN when preceded by Closes / Fixes / Resolves / Part of / Refs. Bare mentions are intentionally ignored.

What this changes

.github/scripts/enrich-cherry-pick-pr.py walks origin/release/vX.Y.Z..HEAD, fetches each cherry-picked main PR via gh pr view, and rebuilds a structured Linear-refs block in the cherry-pick PR's body. When the cherry-pick PR squash-merges, the Closes LUM-XXX lines land in the squash commit body — exactly what Linear's HEAD-only fallback scans for.

Discovery order:

  1. Linear attachmentsForURL GraphQL (docs) — optional, runs only when LINEAR_API_KEY is configured. Catches PRs linked via Linear's Development panel even with no identifier in the body.
  2. Regex scan — restricted to Vellum's three team prefixes (LUM, ATL, JARVIS). One-line constant; adding a fourth team is trivial.

Block uses HTML-comment delimiters so subsequent cherry-picks update in place.

.github/workflows/cherry-pick-to-release.yml exports the cherry-pick PR number and adds a continue-on-error: true step invoking the script. Enrichment cannot fail the cherry-pick.

.github/workflows/linear-release-sync.yml:

  • workflow_dispatch with a tag input for backfill / re-sync. Pre-release filter is gated on event_name == 'release'.
  • Second action invocation with command: complete (same pinned SHA) that moves scheduled releases into the terminal stage automatically.

What this no longer changes (revised after team feedback)

An earlier revision added a comment block to .github/PULL_REQUEST_TEMPLATE.md encouraging Linear closing keywords. Removed in 5698b8a.

Team workflow already gets full Linear-issue linkage via bare mentions (Linear's GitHub integration links issues via the Development panel from any LUM-NNNN reference, and the workspace's auto-close-on-merge setting closes them when the PR merges). The closing-keyword hint was redundant for that purpose and wouldn't be adopted organically.

The Linear Release CLI's separate magic-word requirement is the real problem this PR solves — but via the enrichment script (which queries attachmentsForURL to discover Dev-panel-linked issues and writes the closing-keyword form into the cherry-pick PR body before squash-merge) — not by pushing a PR-template convention onto engineers.

Alternatives considered and rejected

  • C: merge release branches back to main — solves ancestry but touches the release process itself.
  • D: replace linear/linear-release-action with a custom GraphQL caller — re-implements logic the upstream CLI already handles.
  • E: upstream a --from-sha flag — slow, uncertain, doesn't fix the squash-strips-refs problem.

Option B (this PR) preserves the existing release workflow entirely, requires no infrastructure changes, and is reversible by deleting the enrichment step.

Why it's safe

  • Enrichment is best-effort (continue-on-error: true). Failures cannot fail the cherry-pick or any downstream job.
  • The script only edits the cherry-pick PR body via gh pr edit; never pushes commits, never modifies workflow state, never touches other PRs.
  • complete step uses the same pinned action SHA + CLI version as sync.
  • workflow_dispatch is purely additive.

References

Test plan

  • Regex smoke test confirms LUM-, ATL-, JARVIS- are extracted and HTTP-200, AWS-1, lowercase, and future-team prefixes are rejected.
  • yaml.safe_load confirms both workflows parse.
  • Once merged, the next cherry-pick will run enrichment and the resulting cherry-pick PR will show a structured Linear refs block. The next production tag publish will then sync correctly to Linear.
  • Backfill of v0.6.0v0.8.1 is available via workflow_dispatch.

CLI verb checklist

N/A — no IPC routes touched.

Link to Devin session: https://app.devin.ai/sessions/8a3ce390e6e24deba0fac01d1b4a001e
Requested by: @ashleeradka


Open in Devin Review

Solves three structural gaps in the Linear release sync for this repo:

1. cherry-pick squash commits previously dropped per-PR Linear refs, so the
   Release CLI's HEAD-only scan stamped 0 issues per scheduled release.
2. linear-release-sync.yml had no manual trigger, blocking backfill and
   re-sync.
3. scheduled-pipeline releases stayed in 'Planned' / 'Started' indefinitely
   because no step moved them to the terminal stage.

This commit:
- Adds .github/scripts/enrich-cherry-pick-pr.py which rebuilds the
  cherry-pick PR body with a structured Linear refs block. Uses an
  allowlist of Vellum's three team prefixes (LUM, ATL, JARVIS) and
  optionally Linear's attachmentsForURL GraphQL when LINEAR_API_KEY is
  configured. Block uses HTML-comment delimiters so subsequent
  cherry-picks update it in place.
- Updates cherry-pick-to-release.yml to invoke the enrichment script as
  a best-effort (continue-on-error) step after each successful cherry-pick.
- Updates linear-release-sync.yml to add workflow_dispatch with a tag
  input, and a 'complete' step that moves the scheduled release to the
  terminal stage automatically.
- Adds Linear closing-keyword guidance to PULL_REQUEST_TEMPLATE.md.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b41375bdde

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread .github/PULL_REQUEST_TEMPLATE.md Outdated
<!-- Link Linear issues with a closing keyword anywhere in this body so they're -->
<!-- picked up by the Linear Release CLI when this ships. Closes / Fixes / -->
<!-- Resolves all work; bare `LUM-1234` mentions do not. -->
<!-- e.g. `Closes LUM-1234`, `Part of ATL-539`. -->
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Remove concrete Linear IDs from template comments

Because the new enrichment script scans the raw PR title/body with regex_linear_ids(...) and does not strip HTML comments, any PR opened from this template that leaves the hidden comments intact will contribute the example IDs here. When such a PR is cherry-picked, the release PR will get generated Closes LUM-1234 / Closes ATL-539 lines, so Linear release sync can attach bogus issues to every affected release; use non-matching placeholders or strip comments before scanning.

Useful? React with 👍 / 👎.

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.

Caught at the same moment by the Linear linkback bot itself — it attached this PR to the real LUM-NNNN and ATL-NNN issues. Pushed 7f76dcc which:

  • Switches the template's example identifiers from LUM-1234 / ATL-539 (digits → matched by Linear's regex) to LUM-NNNN / ATL-NNN (letters → not matched).
  • Same swap in the enrichment script's docstring for consistency.
  • Updates this PR's description so it stops cross-linking to unrelated existing issues.

Verified my Vellum-team-prefix regex is \\b(LUM|ATL|JARVIS)-(\\d{1,9})\\bNNNN/NNN have no digits and don't match, confirming the bot's analysis was correct.

Resolved.

Real-looking IDs like LUM-1234 in the PR template's hidden comments
would survive into squash commit bodies if authors don't delete the
section, and would be matched by Linear's extractor even though they
live inside HTML comments. Switching to LUM-NNNN / ATL-NNN placeholders
keeps the example readable while ensuring no spurious cross-linking to
real issues.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 4 additional findings in Devin Review.

Open in Devin Review

# a workspace-scoped Linear API key, distinct from the pipeline
# access key.
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
run: python3 .github/scripts/enrich-cherry-pick-pr.py
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.

🔴 Enrichment script unreachable: working tree is on cherry-pick branch, not main

After the "Cherry-pick and create PR" step, the working tree is on the cherry-pick branch (via git checkout "$CHERRY_PICK_BRANCH" at .github/workflows/cherry-pick-to-release.yml:74 or :76-77). The cherry-pick branch is based on the release branch, which was created before this PR merged to main. Therefore .github/scripts/enrich-cherry-pick-pr.py does not exist in the working tree when the enrichment step tries to run it. The command python3 .github/scripts/enrich-cherry-pick-pr.py will fail with FileNotFoundError on every invocation until a release branch is cut from a main commit that includes this script. continue-on-error: true prevents this from blocking the cherry-pick, but the entire enrichment feature is non-functional for all existing release branches.

Suggested fix approach

Before running the script, restore it from the original checkout ref (main). For example:

run: |
  git show origin/main:.github/scripts/enrich-cherry-pick-pr.py > /tmp/enrich-cherry-pick-pr.py
  python3 /tmp/enrich-cherry-pick-pr.py
Suggested change
run: python3 .github/scripts/enrich-cherry-pick-pr.py
run: |
git show origin/main:.github/scripts/enrich-cherry-pick-pr.py > /tmp/enrich-cherry-pick-pr.py
python3 /tmp/enrich-cherry-pick-pr.py
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

Real bug, confirmed by walking the workflow's git state:

  1. The initial actions/checkout@v6.0.2 (line 28) puts the working tree on main with full history (fetch-depth: 0).
  2. The "Cherry-pick and create PR" step (line 76 or line 73-74) git checkouts the cherry-pick branch, which is based on release/vX.Y.Z.
  3. That release branch was cut before this PR exists, so it does not have .github/scripts/enrich-cherry-pick-pr.py.
  4. By the time the enrichment step runs, the script file is genuinely missing from the working tree.

Result without a fix: python3 .github/scripts/enrich-cherry-pick-pr.pyNo such file or directory on every cherry-pick. continue-on-error: true hides the failure but the whole enrichment is non-functional.

Applied the suggested fix in 1a8764d with a slight tweak — explicit git fetch origin main --quiet before git show, so the script is always loaded from the freshest origin/main (covers the case where a long-lived runner has stale refs). Script writes to /tmp and runs from there; its internal git operations (git log, git rev-parse, etc.) read from .git, which is the same regardless of working-tree ref, so behavior is unchanged.

Resolved.

The previous step leaves the working tree on the cherry-pick branch,
which is based on the release branch — a branch that pre-dates this
script. Reading the script from origin/main into /tmp makes it
available regardless of which release branch is being cherry-picked
to. The script itself only reads git refs (git log, git rev-parse,
etc.), so running from /tmp doesn't change behavior.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
Copy link
Copy Markdown
Contributor

@vex-assistant-bot vex-assistant-bot Bot left a comment

Choose a reason for hiding this comment

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

Vex review — APPROVED

CI-only: no application code paths. Reviewed all 3 commits at HEAD 1a8764d.

What this fixes: Three structural gaps that made the Linear Release CLI stamp 0 issues on vellum-assistant releases shipped via cherry-pick PR squashes — (1) squash body drops original PR bodies, (2) release branches aren't ancestors of each other (Linear's CLI falls back to HEAD-only scan), (3) Linear requires closing-keyword magic words, not bare mentions.

The two bugs Devin caught and self-fixed:

  1. Template example IDs were real-looking (LUM-1234/ATL-539LUM-NNNN/ATL-NNN). Commit 7f76dcc confirmed correct — regex is \b(LUM|ATL|JARVIS)-(\d{1,9})\b; NNNN has no digits, doesn't match. Linear's linkback bot actually triggered on the original values before the fix landed, confirming the risk was real.

  2. Enrichment script unreachable on existing release branches. After the cherry-pick step, working tree is on the cherry-pick branch (based on release/vX.Y.Z, cut before this PR merged), so .github/scripts/enrich-cherry-pick-pr.py doesn't exist in the tree. Commit 1a8764d fixes this correctly: git fetch origin main --quiet && git show origin/main:.github/scripts/... > /tmp/... — always loads the script from origin/main regardless of working tree state. Git operations inside the script (git log, git rev-parse) read from .git which is unaffected. ✅

Rest of the review:

  • continue-on-error: true on enrichment step — enrichment failure cannot block cherry-picks ✅
  • Action SHA pinned at both sync and complete steps (755d50b5), same cli_version: v0.10.0
  • workflow_dispatch is additive — existing release: published behavior unchanged ✅
  • Pre-release filter correctly gates on event_name == 'release' so manual dispatch bypasses it ✅
  • Python script is idempotent (HTML comment delimiters replace in-place), handles Linear API errors gracefully, regex restricted to known team prefixes (LUM/ATL/JARVIS) to prevent false positives ✅
  • LINEAR_API_KEY enrichment path is optional — degrades to regex-only if secret not set ✅

Anti-patterns KB: No SwiftUI, React, or application patterns apply — CI scripting only.

Vellum Constitution — Article VI (Accountability): This PR directly strengthens auditability by ensuring every shipped issue shows up correctly on its Linear release. When an issue is stamped "released in v0.8.2" it now actually means it shipped in that build. That's exactly the kind of audit trail Article VI asks for.

No blocking findings. Both Devin-flagged bugs are confirmed fixed at HEAD.

Team workflow links Linear issues via bare mentions (Dev-panel linkage
+ Linear's auto-close-on-merge setting), so the closing-keyword
prescription is redundant for ordinary linkage and won't be adopted.

The Release CLI's magic-word requirement is handled separately by the
cherry-pick enrichment script in this PR, which queries Linear's
attachmentsForURL GraphQL to discover Dev-panel-linked issues and
writes the closing-keyword form into the cherry-pick PR body before
squash-merge. No engineer behavior change needed.

Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
@devin-ai-integration
Copy link
Copy Markdown
Contributor

Thanks @vex-assistant-bot. One revision since your approval: dropped the PR template closing-keyword hint in 5698b8a after the team clarified that bare LUM-NNNN mentions already provide full Linear linkage via the Dev panel + auto-close-on-merge. The closing-keyword convention was redundant for engineers' purposes and wouldn't have been adopted.

The Release CLI's separate magic-word requirement is still solved — by the enrichment script's attachmentsForURL query, which rewrites the cherry-pick PR body just-in-time without asking engineers to change anything. Both the script-unreachable bug (1a8764d) and the placeholder fix (7f76dcc) you confirmed remain in place.

@ashleeradka ashleeradka merged commit d99d359 into main May 13, 2026
2 checks passed
@ashleeradka ashleeradka deleted the devin/1778709802-linear-release-cherry-pick-enrichment branch May 13, 2026 22:24
ashleeradka added a commit that referenced this pull request May 13, 2026
…s) (#30642)

The enrichment script added in #30639 was meant to inject magic-word Linear
identifiers into cherry-pick PR bodies so the Linear Release CLI could extract
them from the squash commit. Empirical testing against Linear's GraphQL API
proved this is unnecessary: when the CLI passes `pullRequestReferences` to
`releaseSyncByAccessKey`, Linear's server resolves each PR to its Development-
panel-linked issues and stamps the release with them, regardless of whether
any Linear identifier appears in commit text.

The cherry-pick squash commit already contains `(#NNN)` references to each
original main PR, which the CLI extracts as `pullRequestReferences`. Linear's
server then finds each main PR's linked issues via the Development panel and
attaches them to the release automatically.

Test case: PR vellum-ai/vellum-assistant-platform#6655 has Dev-panel attachment
ATL-545 and zero LUM/ATL/JARVIS-N mentions in its title, branch, body, or
commit message. Calling `releaseSyncByAccessKey` with only
`pullRequestReferences: [#6655]` (no `issueReferences`) produced a release
stamped with ATL-545. Repeating the test with PR #6708 (LUM-1536 linked)
produced a release stamped with LUM-1536. The resolution is purely server-
side via attachment records.

Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.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