fix: Sync WebGUI tags to OS Linear releases#2646
Conversation
Purpose of the change: - Align the WebGUI tag workflow with the shared Unraid OS Linear release model. Previous behavior: - The workflow delegated to linear/linear-release-action with separate internal and public access keys. - It created or updated Linear releases without syncing issue membership from the WebGUI release diff. - The workflow used actions/checkout@v4, which now emits Node 20 deprecation warnings. Why that was a problem: - WebGUI tag automation could drift from release-diff and notification-worker release semantics. - Linear tickets referenced by WebGUI PRs were not automatically attached to the OS Linear Release. - The workflow warning obscured whether the run itself needed attention. What this changes: - Replaces the Linear release action with an explicit GraphQL sync script. - Resolves the correct OS prerelease or stable release pipeline by tag channel. - Syncs release metadata and preserves terminal Linear release stages instead of backtracking them. - Scans the tag range and merged PR metadata for Linear issue IDs and FeatureOS links, then attaches linked Linear issues to the release. - Updates checkout to actions/checkout@v6. How it works: - The workflow computes the previous semver tag and scans that git range. - The script upserts the Linear Release in OS Prereleases or OS Stable Releases using LINEAR_API_KEY. - Issue membership is added through Linear issueUpdate with addedReleaseIds; no issue state changes are made.
WalkthroughThis PR introduces a new Node.js CLI script for syncing Linear issues to releases via GraphQL, and updates the GitHub Actions workflow to extract issue identifiers from commits and PRs, then invoke the script with computed metadata and generated issue file paths. ChangesLinear Release Sync Integration
Sequence DiagramsequenceDiagram
participant Workflow
participant SyncScript as sync-linear-release.mjs
participant LinearAPI as Linear GraphQL API
Workflow->>SyncScript: node script with env vars<br/>(LINEAR_KEY, RELEASE_CHANNEL, issue file paths)
SyncScript->>LinearAPI: query release pipeline by channel
LinearAPI-->>SyncScript: pipeline metadata
SyncScript->>LinearAPI: query existing release by tag/name
LinearAPI-->>SyncScript: release (or none)
SyncScript->>LinearAPI: create or update release<br/>(name, commit SHA, stage)
LinearAPI-->>SyncScript: release object
SyncScript->>SyncScript: read issue IDs and feature URLs from files
SyncScript->>LinearAPI: query issue by ID / URL attachments
LinearAPI-->>SyncScript: issue objects
SyncScript->>LinearAPI: attach each issue to release<br/>(skip archived/missing)
LinearAPI-->>SyncScript: success
SyncScript->>Workflow: output release_id, release_url,<br/>synced/skipped lists
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/linear-release.yml:
- Around line 120-134: The PR extraction only captures "Merge pull request `#123`"
commits and misses squash-merge notes like "(`#123`)"; update the PR_NUMBERS
assignment (the grep pipeline that reads LOG_PATH and populates PR_NUMBERS) to
also match squash-merge patterns (e.g., "(`#123`)" or just "`#123`" inside
parentheses) so those PR numbers are included, then keep the existing
dedupe/sort and the for-loop that queries each PR and appends to PR_TEXT_PATH
unchanged. Ensure the updated grep/regex still ignores non-numeric matches and
preserves the sort -u || true behavior.
- Around line 85-89: When PREVIOUS_TAG is empty the script sets
RANGE_SPEC="$TAG_NAME" which makes git log walk the entire tag ancestry; replace
that branch so RANGE_SPEC is set to the single-tag range like
"${TAG_NAME}^..${TAG_NAME}" (i.e., use TAG_NAME^..TAG_NAME) to limit git log to
commits introduced by the tag, and ensure the later git log "$RANGE_SPEC" call
uses this new range; if you want to be defensive, fall back to "$TAG_NAME" only
if the "^" parent ref does not exist.
- Line 34: Pin the GitHub Action checkout step by replacing uses:
actions/checkout@v6 with a specific immutable commit SHA for actions/checkout to
prevent upstream retags; also adjust the tag-range logic around PREVIOUS_TAG and
RANGE_SPEC so that when PREVIOUS_TAG is empty you compute a delta (e.g., use git
rev-list or explicitly set RANGE_SPEC to "$PREVIOUS_TAG..$TAG_NAME" only when
PREVIOUS_TAG is set) to avoid including all history, and broaden the PR parsing
used for git log output (the code that inspects git log "$RANGE_SPEC") to also
detect squash-merge and commit-message patterns (not just "Merge pull request
#...") so PR/Linear metadata is captured reliably.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: b36b9307-2a55-49bf-82e0-116169078f79
📒 Files selected for processing (2)
.github/scripts/sync-linear-release.mjs.github/workflows/linear-release.yml
| steps: | ||
| - name: Checkout tag | ||
| uses: actions/checkout@v4 | ||
| uses: actions/checkout@v6 |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Locate and inspect the workflow
FILE=".github/workflows/linear-release.yml"
if [ ! -f "$FILE" ]; then
echo "Missing $FILE"
exit 1
fi
echo "=== File: $FILE ==="
nl -ba "$FILE" | sed -n '1,140p'Repository: unraid/webgui
Length of output: 170
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/linear-release.yml"
echo "=== Sanity: does $FILE exist? ==="
ls -la "$FILE"
echo "=== Line context around 34 (25-45) ==="
awk 'NR>=25 && NR<=45 {printf "%5d %s\n", NR, $0}' "$FILE"
echo "=== All actions/checkout usages in this workflow file ==="
rg -n --fixed-string "actions/checkout@" "$FILE" || true
echo "=== Any checkout pinned elsewhere in the repo? ==="
rg -n --fixed-string "actions/checkout@" .github/workflows || trueRepository: unraid/webgui
Length of output: 1917
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/linear-release.yml"
echo "=== All actions/checkout usages in this workflow file ==="
rg -n --fixed-strings "actions/checkout@" "$FILE" || true
echo "=== Any actions/checkout usages elsewhere (to see if pinned) ==="
rg -n --fixed-strings "actions/checkout@" .github/workflows || trueRepository: unraid/webgui
Length of output: 599
🏁 Script executed:
#!/bin/bash
set -euo pipefail
FILE=".github/workflows/linear-release.yml"
echo "=== Line range 70-110 ==="
awk 'NR>=70 && NR<=110 {printf "%5d %s\n", NR, $0}' "$FILE"
echo "=== Line range 110-160 ==="
awk 'NR>=110 && NR<=160 {printf "%5d %s\n", NR, $0}' "$FILE"
echo "=== Full file (for any other uses of RANGE_SPEC / PR parsing) ==="
cat "$FILE"Repository: unraid/webgui
Length of output: 12174
Pin actions/checkout to an immutable commit SHA (line 34).
actions/checkout@v6is still using a mutable tag; pin to a specific SHA to avoid upstream retags/supply-chain drift.- When
PREVIOUS_TAGis empty,RANGE_SPEC="$TAG_NAME"makesgit log "$RANGE_SPEC"include all history up to the tag rather than a tag delta. - PR number extraction only matches
Merge pull request #..., so squash merges can miss PR/Linear metadata.
🧰 Tools
🪛 zizmor (1.25.2)
[error] 34-34: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)
(unpinned-uses)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/linear-release.yml at line 34, Pin the GitHub Action
checkout step by replacing uses: actions/checkout@v6 with a specific immutable
commit SHA for actions/checkout to prevent upstream retags; also adjust the
tag-range logic around PREVIOUS_TAG and RANGE_SPEC so that when PREVIOUS_TAG is
empty you compute a delta (e.g., use git rev-list or explicitly set RANGE_SPEC
to "$PREVIOUS_TAG..$TAG_NAME" only when PREVIOUS_TAG is set) to avoid including
all history, and broaden the PR parsing used for git log output (the code that
inspects git log "$RANGE_SPEC") to also detect squash-merge and commit-message
patterns (not just "Merge pull request #...") so PR/Linear metadata is captured
reliably.
| if [ -n "$PREVIOUS_TAG" ]; then | ||
| RANGE_SPEC="${PREVIOUS_TAG}..${TAG_NAME}" | ||
| else | ||
| RANGE_SPEC="$TAG_NAME" | ||
| fi |
There was a problem hiding this comment.
Avoid full-history scans when no previous tag exists.
Line 88 sets RANGE_SPEC to just ${TAG_NAME}. The later git log "$RANGE_SPEC" then walks the entire ancestry of that tag, so syncing the earliest semver tag would attach every historical issue/PR in the repository to a single Linear release.
Suggested fix
if [ -n "$PREVIOUS_TAG" ]; then
RANGE_SPEC="${PREVIOUS_TAG}..${TAG_NAME}"
else
- RANGE_SPEC="$TAG_NAME"
+ RANGE_SPEC="${TAG_NAME}^!"
fi🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/linear-release.yml around lines 85 - 89, When PREVIOUS_TAG
is empty the script sets RANGE_SPEC="$TAG_NAME" which makes git log walk the
entire tag ancestry; replace that branch so RANGE_SPEC is set to the single-tag
range like "${TAG_NAME}^..${TAG_NAME}" (i.e., use TAG_NAME^..TAG_NAME) to limit
git log to commits introduced by the tag, and ensure the later git log
"$RANGE_SPEC" call uses this new range; if you want to be defensive, fall back
to "$TAG_NAME" only if the "^" parent ref does not exist.
| PR_NUMBERS="$( | ||
| grep -Eo 'Merge pull request #[0-9]+' "$LOG_PATH" \ | ||
| | grep -Eo '[0-9]+' \ | ||
| | sort -u || true | ||
| )" | ||
|
|
||
| for PR_NUMBER in $PR_NUMBERS; do | ||
| curl -fsSL \ | ||
| -H "Accept: application/vnd.github+json" \ | ||
| -H "Authorization: Bearer ${GH_TOKEN}" \ | ||
| -H "X-GitHub-Api-Version: 2022-11-28" \ | ||
| "${GITHUB_API_URL:-https://api.github.com}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ | ||
| | jq -r '[.title, .body, .head.ref, .base.ref] | map(select(. != null and . != "")) | .[]' \ | ||
| >> "$PR_TEXT_PATH" | ||
| done |
There was a problem hiding this comment.
Include squash-merge PRs in the metadata scan.
Lines 120-123 only recognize Merge pull request #123`` commits. Squash merges land as ... (#123`)`, so their PR title/body never gets queried and any Linear IDs or FeatureOS URLs that only exist there are silently omitted from the release sync.
Suggested fix
PR_NUMBERS="$(
- grep -Eo 'Merge pull request #[0-9]+' "$LOG_PATH" \
+ grep -Eo 'Merge pull request #[0-9]+|\(#[0-9]+\)' "$LOG_PATH" \
| grep -Eo '[0-9]+' \
| sort -u || true
)"📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| PR_NUMBERS="$( | |
| grep -Eo 'Merge pull request #[0-9]+' "$LOG_PATH" \ | |
| | grep -Eo '[0-9]+' \ | |
| | sort -u || true | |
| )" | |
| for PR_NUMBER in $PR_NUMBERS; do | |
| curl -fsSL \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "${GITHUB_API_URL:-https://api.github.com}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ | |
| | jq -r '[.title, .body, .head.ref, .base.ref] | map(select(. != null and . != "")) | .[]' \ | |
| >> "$PR_TEXT_PATH" | |
| done | |
| PR_NUMBERS="$( | |
| grep -Eo 'Merge pull request #[0-9]+|\(#[0-9]+\)' "$LOG_PATH" \ | |
| | grep -Eo '[0-9]+' \ | |
| | sort -u || true | |
| )" | |
| for PR_NUMBER in $PR_NUMBERS; do | |
| curl -fsSL \ | |
| -H "Accept: application/vnd.github+json" \ | |
| -H "Authorization: Bearer ${GH_TOKEN}" \ | |
| -H "X-GitHub-Api-Version: 2022-11-28" \ | |
| "${GITHUB_API_URL:-https://api.github.com}/repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ | |
| | jq -r '[.title, .body, .head.ref, .base.ref] | map(select(. != null and . != "")) | .[]' \ | |
| >> "$PR_TEXT_PATH" | |
| done |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/linear-release.yml around lines 120 - 134, The PR
extraction only captures "Merge pull request `#123`" commits and misses
squash-merge notes like "(`#123`)"; update the PR_NUMBERS assignment (the grep
pipeline that reads LOG_PATH and populates PR_NUMBERS) to also match
squash-merge patterns (e.g., "(`#123`)" or just "`#123`" inside parentheses) so
those PR numbers are included, then keep the existing dedupe/sort and the
for-loop that queries each PR and appends to PR_TEXT_PATH unchanged. Ensure the
updated grep/regex still ignores non-numeric matches and preserves the sort -u
|| true behavior.
## Summary - Pins `actions/checkout` to the immutable commit currently referenced by `v6.0.2`. - Bounds the no-previous-tag path to `TAG^..TAG` when possible so the workflow does not scan full tag ancestry. - Restricts PR number extraction to explicit merge commit messages like `Merge pull request #123`, avoiding accidental issue references like `(#123)`. - Adds Linear-side GitHub PR link resolution: discovered PR URLs are passed to the sync script, which resolves Linear attachments for those URLs and attaches the linked issues to the release. - Fixes manual `workflow_dispatch` testing against historical tags by checking out the workflow branch while still resolving the requested `tag_name`. - Carries active QA work across OS prereleases: internal tag sync now attaches active issues to the exact prerelease, the stable companion release, and the next planned prerelease. It removes next-prerelease membership only after the issue reaches an internal/completed state. - Sweeps existing issues already attached to the exact prerelease through the same policy, so tickets are carried forward even when they were not mentioned by the latest WebGUI tag diff. ## Review Notes - Valid: mutable checkout tag. Fixed by pinning to `de0fac2e4500dabe0009e67214ff5f5447ce83dd`. - Valid: empty `PREVIOUS_TAG` range walked too much history. Fixed with parent-bounded range and a parentless fallback. - Valid: broad parenthetical PR extraction could treat issue references as PR numbers. Fixed by restricting extraction to `Merge pull request #[0-9]+` while preserving numeric extraction, `sort -u`, and `|| true` behavior. - Follow-up support: if the GitHub PR only links to Linear from the Linear side, `attachmentsForURL` now resolves the issue from the PR URL. - Manual test finding: checking out `tag_name` during `workflow_dispatch` failed for historical tags because the tag did not contain the new script. Fixed by checking out `github.ref` for manual runs. - Skipped: none. All reported findings applied to the current code. ## Validation - `git diff --check` - `node --check .github/scripts/sync-linear-release.mjs` - `ruby -e 'require "yaml"; YAML.load_file(".github/workflows/linear-release.yml"); puts "yaml ok"'` - Shell probe confirmed PR extraction returns `2645` from `Merge pull request #2645 ...` and ignores parenthetical `(#2646)` / `(#123)` references. - Shell probe confirmed the first-tag fallback uses a single-commit style range such as `6.2.2^..6.2.2` when a parent exists. - Live Linear validation: `attachmentsForURL(https://github.com/unraid/webgui/pull/2645)` resolved `OS-239`, and the sync script attached `OS-239` to `7.3.1-rc.0.3` without changing its `QA Ready` state. - Manual workflow test passed: https://github.com/unraid/webgui/actions/runs/26299583415 for `tag_name=7.3.1-rc.0.3`; it synced the existing Linear Release and attached `OS-239` idempotently. - Local Linear validation after carry-forward change: `OS-239` is `QA Ready` and attached to `7.3.1-rc.0.3`, `7.3.1-rc.0.4`, and `Unraid OS 7.3.1 Stable`. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Chores * Improved release automation and synchronization between GitHub PRs and Linear issues, ensuring PR links are properly captured and tracked throughout the release cycle * Strengthened issue state management across multiple release versions, providing better visibility into which changes are included in each release * Enhanced overall reliability of the release workflow <!-- review_stack_entry_start --> [](https://app.coderabbit.ai/change-stack/unraid/webgui/pull/2647?utm_source=github_walkthrough&utm_medium=github&utm_campaign=change_stack) <!-- review_stack_entry_end --> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
Summary
linear/linear-release-action@v0usage with a direct Linear GraphQL sync.OS Prereleases, stable tags sync toOS Stable Releases.product.unraid.net/p/...links through Linear attachments so FeatureOS-linked Linear issues can be added to the release.Releasedby release-diff.actions/checkout@v6to avoid the Node 20 action warning.Validation
node --check .github/scripts/sync-linear-release.mjsgit diff --check7.3.1-rc.0.3,7.3.1-rc.0.1,7.3.0, and7.2.7.7.3.1-rc.0.3; it kept the releaseReleased, updated the WebGUI commit SHA to the tag commit, and did not remove existing issue memberships.Summary by CodeRabbit
Release Notes
Note: This release contains no user-facing changes.