A Rust tool for automatically marking Linear tickets as completed when a GitHub release is published.
This tool processes GitHub release notes to find associated Pull Requests, extracts Linear ticket IDs from those PRs, and marks them as completed in Linear. It can be used as individual pipeline commands or as a single orchestrated workflow.
The tool relies on these external commands being available:
gh(GitHub CLI)curl(for Linear API requests)grep(for pattern matching)jq(for JSON manipulation)
No other dependencies are assumed.
cargo build --releaseThe binary will be available at target/release/release-linear-ticket-update
Extracts Pull Request numbers from release notes.
Usage:
# From a release tag
release-linear-ticket-update parse-notes --release-tag v1.2.3
# From stdin
echo "Fixed #123 and #456" | release-linear-ticket-update parse-notesOutput: List of PR numbers (one per line)
Finds Linear ticket IDs from Pull Requests by examining PR title, body, comments, and commit messages.
Usage:
# From file
release-linear-ticket-update extract-tickets pr_numbers.txt
# From stdin
echo "123" | release-linear-ticket-update extract-tickets
# Explicit stdin with files
release-linear-ticket-update extract-tickets - other_prs.txt
# Chained from parse-notes
release-linear-ticket-update parse-notes --release-tag v1.2.3 | release-linear-ticket-update extract-ticketsOutput: List of Linear ticket IDs (one per line), e.g. ABC-123
Marks Linear tickets as completed using the Linear GraphQL API.
Usage:
# From file (with env var)
LINEAR_API_KEY=your_key release-linear-ticket-update update-tickets tickets.txt
# With flag
release-linear-ticket-update update-tickets --linear-api-key your_key --linear-org myorg tickets.txt
# From stdin
echo "ABC-123" | release-linear-ticket-update update-tickets --linear-api-key your_key --linear-org myorg
# Chained pipeline
release-linear-ticket-update extract-tickets prs.txt | release-linear-ticket-update update-tickets --linear-api-key your_key --linear-org myorg
# Dry-run mode (check what would be updated without making changes)
release-linear-ticket-update update-tickets --dry-run --linear-api-key your_key --linear-org myorg tickets.txt
# Update regardless of current Linear state
release-linear-ticket-update update-tickets --update-all-statuses --linear-api-key your_key --linear-org myorg tickets.txtRequired:
--linear-api-keyflag orLINEAR_API_KEYenvironment variable--linear-orgflag orLINEAR_ORGenvironment variable
Optional:
--dry-runflag: Preview which tickets would be updated without actually updating them--update-all-statusesflag: Update tickets even if they are not currently in "Passing" state
Output:
- stdout: Successfully updated ticket URLs (or URLs that would be updated in dry-run mode)
- stderr: Failed ticket URLs and error messages
Dry-run Mode:
When --dry-run is specified:
- Still queries Linear API to check each ticket's current state
- Outputs only tickets that would be updated
- Skips the actual mutation (does not update tickets)
- Useful for previewing changes before running the actual update
Workflow State Filtering:
By default, tickets are only updated if their current state name is "Passing" (case-insensitive). Use --update-all-statuses to update any ticket that is not already Done/Completed.
Runs the complete pipeline: parse-notes → extract-tickets → update-tickets
Usage:
# Full workflow
release-linear-ticket-update --release-tag v1.2.3
# With environment variables
LINEAR_API_KEY=key LINEAR_ORG=myorg release-linear-ticket-update --release-tag v1.2.3
# With flags
release-linear-ticket-update --release-tag v1.2.3 --linear-api-key key --linear-org myorg
# Dry-run mode
release-linear-ticket-update --dry-run --release-tag v1.2.3 --linear-api-key key --linear-org myorg
# Update regardless of current Linear state
release-linear-ticket-update --update-all-statuses --release-tag v1.2.3 --linear-api-key key --linear-org myorgRequired:
--release-tagflagLINEAR_API_KEY(via flag or env var)LINEAR_ORG(via flag or env var)
Optional:
--dry-runflag: Preview which tickets would be updated without making changes--update-all-statusesflag: Update tickets even if they are not currently in "Passing" state
# Set environment variables
export LINEAR_API_KEY="lin_api_xxx"
export LINEAR_ORG="myorg"
# Run complete workflow
release-linear-ticket-update --release-tag v2.0.0# Step 1: Extract PR numbers from release
release-linear-ticket-update parse-notes --release-tag v2.0.0 > prs.txt
# Step 2: Find Linear tickets in those PRs
release-linear-ticket-update extract-tickets prs.txt > tickets.txt
# Step 3: Mark tickets as completed
release-linear-ticket-update update-tickets --linear-api-key "$LINEAR_API_KEY" tickets.txtrelease-linear-ticket-update parse-notes --release-tag v2.0.0 | \
release-linear-ticket-update extract-tickets | \
release-linear-ticket-update update-tickets --linear-api-key "$LINEAR_API_KEY" --linear-org "$LINEAR_ORG"Progress output is written to stderr and prefixed with a fixed-width stage name for easy scanning:
parse-notes : ...
extract-tickets : ...
update-tickets : ...
All three pipeline stages are streaming (they do not read all stdin before starting work), so this works as expected:
release-linear-ticket-update parse-notes --release-tag v2.0.0 | \
release-linear-ticket-update extract-tickets | \
release-linear-ticket-update update-tickets --linear-api-key "$LINEAR_API_KEY" --linear-org "$LINEAR_ORG"name: Complete Linear Tickets on Release
on:
release:
types: [published]
jobs:
complete-tickets:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
- name: Install release-linear-ticket-update
run: |
cargo install --locked --git https://github.com/mkpro118/release-linear-ticket-update --bin release-linear-ticket-update
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Complete Linear tickets
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_ORG: hipphealth
run: |
release-linear-ticket-update --release-tag ${{ github.event.release.tag_name }}- No external dependencies (uses stdlib only)
- Relies on delegation to system commands (gh, curl, grep, jq) rather than bundling libraries