Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,15 @@ Linear Release is a CLI tool that integrates your CI/CD pipeline with [Linear's
- Scans commits for Linear issue identifiers (e.g., `ENG-123`)
- Detects pull request references in commit messages
- Creates and updates releases in Linear
- Tracks deployment stages (staging, production, etc.)
- Tracks deployment stages for scheduled releases

## Pipeline Types

Linear Release supports two pipeline styles, configured in Linear:

**Continuous**: Every deployment creates a completed release. Use `sync` after each deploy — the release is created already completed.

**Scheduled**: An ongoing release collects changes over time, then moves through stages (e.g. "code freeze", "qa") before completion. Useful for release trains. Use `sync` to add issues, `update` to move between stages, and `complete` to finalize.

## Installation

Expand Down Expand Up @@ -77,6 +85,10 @@ chmod +x linear-release
LINEAR_ACCESS_KEY=<your-key> ./linear-release sync
```

### AI-assisted setup

Use the [Linear Release setup skill](./skills/linear-release-setup/SKILL.md) to generate CI configuration tailored to your project. It supports GitHub Actions, GitLab CI, CircleCI, and other platforms, and walks you through continuous vs. scheduled pipelines, monorepo path filtering, and more.

Choose a reason for hiding this comment

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

This format probably works with skills.sh / npx skills add linear/linear-release which seems to have a bit of ecosystem momentum if you wanted to advertise an easy command to wire it up

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, I'll look into adding it.


## Commands

### `sync`
Expand Down
31 changes: 31 additions & 0 deletions examples/circleci-continuous.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Linear Release — CircleCI (Continuous Pipeline)
#
# Use when: Every deployment creates a completed release automatically.
# Trigger: On every push to main.
# Customize: Branch name, path filters (--include-paths).
# Note: Set LINEAR_ACCESS_KEY in CircleCI project settings.

version: 2.1

jobs:
linear-release-sync:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Download Linear Release
command: |
curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
chmod +x linear-release
- run:
name: Sync release
command: ./linear-release sync

workflows:
release:
jobs:
- linear-release-sync:
filters:
branches:
only: main
129 changes: 129 additions & 0 deletions examples/circleci-scheduled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Linear Release — CircleCI (Scheduled)
#
# Use when: Releases follow a branch cut model. Main syncs without a version,
# release branches derive version from CIRCLE_BRANCH.
#
# Trigger: Auto on push, API trigger for later stage transitions and completion.
# Customize: Branch patterns, stage names, auto-promotion stage.
# Note: Set LINEAR_ACCESS_KEY and CIRCLE_TOKEN in CircleCI project settings.
#
# Branch creation detection: CircleCI has no built-in signal, so auto-promotion
# checks the API for previous pipelines on the branch.
#
# Monorepo: CircleCI doesn't support path filtering natively. Use the `path-filtering`
# orb or split into separate workflows and use the API to conditionally trigger.

version: 2.1

# Pipeline parameters — passed via CircleCI API for manual stage transitions.
# On normal pushes, defaults apply and only the sync workflows run.
parameters:
run-release-action:
type: boolean
default: false
action:
type: enum
enum: ["update", "complete"]
default: "update"
stage:
type: string
default: ""
release_version:
type: string
default: ""

jobs:
linear-release-sync-main:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Download Linear Release
command: |
curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
chmod +x linear-release
- run:
name: Sync release
command: ./linear-release sync

linear-release-sync-release:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Download Linear Release
command: |
curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
chmod +x linear-release
- run:
name: Derive version and sync
command: |
RELEASE_VERSION="${CIRCLE_BRANCH#release/}"
./linear-release sync --release-version="$RELEASE_VERSION"
- run:
name: Auto-promote on branch creation
command: |
RELEASE_VERSION="${CIRCLE_BRANCH#release/}"
# Check if this is the first pipeline on this branch (branch creation)
PIPELINE_COUNT=$(curl -s -H "Circle-Token: $CIRCLE_TOKEN" \
"https://circleci.com/api/v2/project/gh/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/pipeline?branch=$CIRCLE_BRANCH" \
| jq '.items | length')
if [ "$PIPELINE_COUNT" -le 1 ]; then
./linear-release update --release-version="$RELEASE_VERSION" --stage="code freeze"
fi

linear-release-action:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Download Linear Release
command: |
curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
chmod +x linear-release
- run:
name: Run release action
command: |
case "<< pipeline.parameters.action >>" in
update)
./linear-release update \
--release-version="<< pipeline.parameters.release_version >>" \
--stage="<< pipeline.parameters.stage >>"
;;
complete)
./linear-release complete \
--release-version="<< pipeline.parameters.release_version >>"
;;
esac

workflows:
# On normal pushes — sync issues to the release
release-sync-main:
when:
not: << pipeline.parameters.run-release-action >>
jobs:
- linear-release-sync-main:
filters:
branches:
only: main

release-sync-release:
when:
not: << pipeline.parameters.run-release-action >>
jobs:
- linear-release-sync-release:
filters:
branches:
only: /^release\/.*/

# Trigger via CircleCI API:
# curl -X POST https://circleci.com/api/v2/project/gh/ORG/REPO/pipeline \
# -H "Circle-Token: $CIRCLE_TOKEN" -H "Content-Type: application/json" \
# -d '{"parameters": {"run-release-action": true, "action": "update", "stage": "qa", "release_version": "1.2.0"}}'
release-action:
when: << pipeline.parameters.run-release-action >>
jobs:
- linear-release-action
22 changes: 22 additions & 0 deletions examples/github-actions-continuous.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# Linear Release — GitHub Actions (Continuous Pipeline)
#
# Use when: Every deployment creates a completed release automatically.
# Trigger: On every push to main.
# Customize: Branch name, path filters (include_paths input).

name: Linear Release
on:
push:
branches: [main]

jobs:
linear-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: linear/linear-release-action@v0
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
83 changes: 83 additions & 0 deletions examples/github-actions-scheduled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
# Linear Release — GitHub Actions (Scheduled)
#
# Use when: Releases follow a branch cut model. Main collects changes into the
# current release, a release branch is cut for stabilization, and branch creation
# auto-promotes to "code freeze".
#
# Trigger: push to main (sync), push to release/* (sync + auto code freeze on
# creation), or manual workflow_dispatch for later stages and completion.
# Customize: Branch patterns, stage names, version derivation.
#
# Monorepo: GitHub Actions `paths` applies to all branches in a push trigger.
# To path-filter main without filtering release branches, split into two files:
# File 1 (main): add `paths: [...]` to the push trigger, keep only the main sync step.
# File 2 (release): keep the release branch + workflow_dispatch logic as-is.
# Add `include_paths` to the action in both files.

name: Linear Release
on:
push:
branches:
- main
- "release/**"
workflow_dispatch:
inputs:
action:
description: "Release action"
required: true
type: choice
options:
- update
- complete
stage:
description: "Release stage (for update, e.g. qa)"
required: false
type: string
release_version:
description: "Release version"
required: true
type: string

jobs:
linear-release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

# Main branch: sync without --release-version (targets current started release)
- uses: linear/linear-release-action@v0
if: github.event_name == 'push' && !startsWith(github.ref_name, 'release/')
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}

# Release branch: derive version from branch name
- name: Set release version
if: github.event_name == 'push' && startsWith(github.ref_name, 'release/')
run: echo "RELEASE_VERSION=${GITHUB_REF_NAME#release/}" >> "$GITHUB_ENV"

# Release branch: sync with explicit version
- uses: linear/linear-release-action@v0
if: github.event_name == 'push' && startsWith(github.ref_name, 'release/')
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
release_version: ${{ env.RELEASE_VERSION }}

# Branch creation: auto-promote to code freeze
- uses: linear/linear-release-action@v0
if: github.event_name == 'push' && startsWith(github.ref_name, 'release/') && github.event.created
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
action: update
stage: code freeze
release_version: ${{ env.RELEASE_VERSION }}

# Manual: run the specified action (later stages, completion)
- uses: linear/linear-release-action@v0
if: github.event_name == 'workflow_dispatch'
with:
access_key: ${{ secrets.LINEAR_ACCESS_KEY }}
action: ${{ inputs.action }}
stage: ${{ inputs.stage }}
release_version: ${{ inputs.release_version }}
21 changes: 21 additions & 0 deletions examples/gitlab-ci-continuous.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Linear Release — GitLab CI (Continuous Pipeline)
#
# Use when: Every deployment creates a completed release automatically.
# Trigger: On every push to the default branch.
# Customize: Branch rules, path filters (--include-paths).

.linear-release-setup: &linear-release-setup
before_script:
- curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
- chmod +x linear-release

linear-release-sync:
<<: *linear-release-setup
stage: deploy
script:
- ./linear-release sync
variables:
LINEAR_ACCESS_KEY: $LINEAR_ACCESS_KEY
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
79 changes: 79 additions & 0 deletions examples/gitlab-ci-scheduled.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
# Linear Release — GitLab CI (Scheduled)
#
# Use when: Releases follow a branch cut model. Main syncs without a version,
# release branches derive version from branch name. Branch creation auto-promotes
# to "code freeze" (detected via CI_COMMIT_BEFORE_SHA being all zeros).
#
# Trigger: Auto on push, manual for later stage transitions and completion.
# Customize: Branch patterns, stage names, version derivation.
#
# Monorepo: Add `changes` filter to the main job's rules only (not release branch jobs):
# rules:
# - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
# changes:
# - "apps/mobile/**"
# - "packages/shared/**"
# Also add --include-paths to all sync commands.

.linear-release-setup: &linear-release-setup
before_script:
- curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release
- chmod +x linear-release

# Main branch: sync without --release-version (targets current started release)
linear-release-sync-main:
<<: *linear-release-setup
stage: deploy
script:
- ./linear-release sync
variables:
LINEAR_ACCESS_KEY: $LINEAR_ACCESS_KEY
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

# Release branch: derive version from branch name and sync
linear-release-sync-release:
<<: *linear-release-setup
stage: deploy
script:
- export RELEASE_VERSION="${CI_COMMIT_BRANCH#release/}"
- ./linear-release sync --release-version="$RELEASE_VERSION"
# Auto-promote on branch creation (first push to a new branch)
- |
if [ "$CI_COMMIT_BEFORE_SHA" = "0000000000000000000000000000000000000000" ]; then
./linear-release update --release-version="$RELEASE_VERSION" --stage="code freeze"
fi
variables:
LINEAR_ACCESS_KEY: $LINEAR_ACCESS_KEY
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_BRANCH =~ /^release\//

# Trigger manually for later stage transitions and completion
linear-release-update:
<<: *linear-release-setup
stage: deploy
script:
- export RELEASE_VERSION="${CI_COMMIT_BRANCH#release/}"
- ./linear-release update --release-version="$RELEASE_VERSION" --stage="$STAGE"
variables:
LINEAR_ACCESS_KEY: $LINEAR_ACCESS_KEY
STAGE: ""
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_BRANCH =~ /^release\//
when: manual

linear-release-complete:
<<: *linear-release-setup
stage: deploy
script:
- export RELEASE_VERSION="${CI_COMMIT_BRANCH#release/}"
- ./linear-release complete --release-version="$RELEASE_VERSION"
variables:
LINEAR_ACCESS_KEY: $LINEAR_ACCESS_KEY
GIT_DEPTH: 0
rules:
- if: $CI_COMMIT_BRANCH =~ /^release\//
when: manual
Loading