diff --git a/README.md b/README.md index e5ea53f..9084064 100644 --- a/README.md +++ b/README.md @@ -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 @@ -77,6 +85,10 @@ chmod +x linear-release LINEAR_ACCESS_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. + ## Commands ### `sync` diff --git a/examples/circleci-continuous.yml b/examples/circleci-continuous.yml new file mode 100644 index 0000000..36179e3 --- /dev/null +++ b/examples/circleci-continuous.yml @@ -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 diff --git a/examples/circleci-scheduled.yml b/examples/circleci-scheduled.yml new file mode 100644 index 0000000..cfd6c8f --- /dev/null +++ b/examples/circleci-scheduled.yml @@ -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 diff --git a/examples/github-actions-continuous.yml b/examples/github-actions-continuous.yml new file mode 100644 index 0000000..2c5416e --- /dev/null +++ b/examples/github-actions-continuous.yml @@ -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 }} diff --git a/examples/github-actions-scheduled.yml b/examples/github-actions-scheduled.yml new file mode 100644 index 0000000..946d63a --- /dev/null +++ b/examples/github-actions-scheduled.yml @@ -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 }} diff --git a/examples/gitlab-ci-continuous.yml b/examples/gitlab-ci-continuous.yml new file mode 100644 index 0000000..fac0062 --- /dev/null +++ b/examples/gitlab-ci-continuous.yml @@ -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 diff --git a/examples/gitlab-ci-scheduled.yml b/examples/gitlab-ci-scheduled.yml new file mode 100644 index 0000000..440740a --- /dev/null +++ b/examples/gitlab-ci-scheduled.yml @@ -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 diff --git a/skills/linear-release-setup/SKILL.md b/skills/linear-release-setup/SKILL.md new file mode 100644 index 0000000..904e14b --- /dev/null +++ b/skills/linear-release-setup/SKILL.md @@ -0,0 +1,116 @@ +--- +name: linear-release-setup +description: Generate CI/CD configuration for Linear Release. Use when setting up + release tracking, configuring CI pipelines for Linear, or integrating deployments + with Linear releases. Supports GitHub Actions, GitLab CI, CircleCI, and other platforms. +--- + +# Linear Release Setup + +## Interactive Workflow + +### Step 1: Preflight + +Before generating config, confirm: + +1. **Pipeline exists in Linear** — the user must have created a release pipeline in Linear first (Settings → Releases). Each pipeline has its own access key. +2. **Detect CI platform** — look for `.github/workflows/*.yml` (GitHub Actions), `.gitlab-ci.yml` (GitLab CI), `.circleci/config.yml` (CircleCI), or other CI config. +3. **Detect default branch** — check `git symbolic-ref refs/remotes/origin/HEAD` or the CI config. Don't assume `main`. + +### Step 2: Ask the user + +Gather the following information (skip questions you can infer from the codebase): + +1. **CI platform** — if not auto-detected +2. **Pipeline type** — continuous (every deploy = a completed release) or scheduled (releases collect changes over time, then move through stages) +3. **Monorepo?** — if the repo has multiple apps/services, ask which paths to track (e.g. `apps/web/**`) + +For scheduled pipelines, always ask these explicitly (don't infer — they significantly affect the generated config): + +4. **Branch model** — just `main`, or `main` + release branches (e.g. `release/*`)? +5. **Release versioning** — calendar-based (e.g. `2026.05`), semver (e.g. `1.2.0`), or default (commit SHA)? Where does the version come from — branch name, CI variable, file, git tag? +6. **Release stages** — what stages before completion (e.g. "code freeze", "qa")? +7. **Automation level** — all manual (via workflow_dispatch), or some automated (e.g. branch creation → code freeze)? + +### Step 3: Generate the CI configuration + +Select the right example template, read it, adapt it (branch patterns, stage names, paths, version format), and add it to an existing workflow or create a new one. + +| Platform | Pipeline Type | Example | +| -------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------------- | +| GitHub Actions | Continuous | [`github-actions-continuous.yml`](https://github.com/linear/linear-release/blob/main/examples/github-actions-continuous.yml) | +| GitHub Actions | Scheduled | [`github-actions-scheduled.yml`](https://github.com/linear/linear-release/blob/main/examples/github-actions-scheduled.yml) | +| GitLab CI | Continuous | [`gitlab-ci-continuous.yml`](https://github.com/linear/linear-release/blob/main/examples/gitlab-ci-continuous.yml) | +| GitLab CI | Scheduled | [`gitlab-ci-scheduled.yml`](https://github.com/linear/linear-release/blob/main/examples/gitlab-ci-scheduled.yml) | +| CircleCI | Continuous | [`circleci-continuous.yml`](https://github.com/linear/linear-release/blob/main/examples/circleci-continuous.yml) | +| CircleCI | Scheduled | [`circleci-scheduled.yml`](https://github.com/linear/linear-release/blob/main/examples/circleci-scheduled.yml) | + +For GitHub Actions, prefer the official action (`linear/linear-release-action@v0`). For other platforms, download the CLI binary and refer to the [README](https://github.com/linear/linear-release#commands) for the full command reference: + +```bash +curl -sL https://github.com/linear/linear-release/releases/latest/download/linear-release-linux-x64 -o linear-release +chmod +x linear-release +``` + +Each scheduled example includes a **monorepo** note in the header explaining how to split workflows for path filtering per platform. + +### Step 4: Remind about secrets + +Tell the user to add the `LINEAR_ACCESS_KEY` secret to their CI environment: + +- **GitHub Actions**: Repository Settings → Secrets and variables → Actions → New repository secret +- **GitLab CI**: Settings → CI/CD → Variables +- **CircleCI**: Project Settings → Environment Variables + +The access key is created in Linear from the pipeline's settings page. Each pipeline has its own access key. + +## Key Concepts + +### Pipelines + +A **release pipeline** in Linear represents a deployment lane — e.g. "iOS", "Android", "Web". Each product or environment you ship independently should be its own pipeline. Don't confuse this with CI pipelines — a Linear pipeline is the release tracking unit, and your CI config calls the CLI to update it. + +### Pipeline Types + +**Continuous**: Every deploy creates a completed release. One `sync` call on push. + +**Scheduled**: An ongoing release collects changes, then moves through stages before completion. Three commands: + +- `sync` — adds issues from new commits to the current release +- `update --stage=` — moves the release to a stage (e.g. "code freeze") +- `complete` — marks the release as shipped + +The typical scheduled flow uses **release branches**: `main` collects changes, a `release/*` branch is cut for stabilization, and branch creation auto-promotes to a stage. Version is derived from the branch name (e.g. `release/1.2.0` → `1.2.0`). On `main`, `sync` runs without `--release-version` so issues land on the current started release. On release branches, `sync` runs with `--release-version` to target the specific release. + +### Stages + +Stages are phases a scheduled release moves through — e.g. "code freeze", "in review", "qa". They represent process steps, not environments. Different environments (staging, production) should be separate pipelines. + +Stages can be **frozen** in Linear. A frozen stage makes `sync` (without `--release-version`) skip that release and target the next one — a safety net for code freezes. + +### Commands + +| Command | Purpose | Key flags | +| ---------- | ---------------------------------- | ------------------------------------------------ | +| `sync` | Create/update release from commits | `--name`, `--release-version`, `--include-paths` | +| `update` | Move release to a stage | `--stage` (required), `--release-version` | +| `complete` | Mark release as complete | `--release-version` | + +### Path Filtering (Monorepos) + +Path filters can be configured in Linear's pipeline settings or via the CLI's `--include-paths` flag (CLI takes precedence if both are set). If the user has already configured paths in Linear, the CLI flag is optional. + +For **monorepos with release branches**, CI platforms often can't path-filter differently per branch. The solution is two workflow/job definitions: `main` with path filtering, release branches without. Each scheduled example includes platform-specific instructions. + +### Requirements + +- **Full git history**: `fetch-depth: 0` or equivalent — shallow clones miss commits between releases. +- **`LINEAR_ACCESS_KEY`**: Per-pipeline access key from Linear, stored as a CI secret. + +### Checklist + +- [ ] Full clone / `fetch-depth: 0` +- [ ] `LINEAR_ACCESS_KEY` set as a secret (one per pipeline) +- [ ] Correct binary platform (`linux-x64`, `darwin-arm64`, or `darwin-x64`) +- [ ] Triggers on the correct branches (`main` for continuous; `main` + `release/*` for scheduled) +- [ ] Monorepo: path filters set (in Linear config or via `--include-paths`), and separate workflows if using release branches