Skip to content
Draft
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
173 changes: 173 additions & 0 deletions actions/fe-staging-notification/LINEAR.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# Linear Integration

This document describes the Linear ticket integration for the staging notification action.

## Overview

When a staging deployment occurs, the action:

1. Extracts Linear ticket IDs from commit messages and PR titles
2. Looks up each ticket via Linear's GraphQL API
3. Posts a staging deployment comment on each valid ticket
4. Posts a Slack thread reply listing the linked tickets with URLs

## Ticket ID Format

Linear tickets are detected using the pattern: `[A-Z]{1,5}-[0-9]{1,5}`

Examples of supported formats:
- `INJ-142` - Injective team
- `SEC-146` - Security team
- `I-42` - Single letter prefix
- `ILO-796` - Three letter prefix
- `ID-1364`, `IC-930`, `IA-920` - Other team prefixes

Multiple tickets can appear in a single commit message or PR title. Tickets are deduplicated across all sources.

## How Ticket Sources Work

The action reads tickets from multiple sources (in order):

1. **GitHub event payload** (automatic)
- **Push events**: All commit messages from `commits[].message`
- **PR events**: Pull request title and body

2. **`commit-messages` input** (manual, for `workflow_dispatch`)
- Newline-separated commit messages passed by the calling workflow
- Use when the event payload has no commits (manual triggers)

3. **`pr-title` input** (optional)
- Pass `${{ github.event.pull_request.title }}` from the calling workflow

All sources are combined and deduplicated before processing.

## Linear API

### Authentication

Requires a Linear API key with the following permissions:
- **Read issues** - to look up tickets by identifier
- **Create comments** - to post staging deployment comments

Generate an API key at: Settings > API > Personal API keys

Store it as a GitHub Actions secret (e.g., `LINEAR_API_KEY`).

### GraphQL Queries Used

**Issue lookup:**
```graphql
query GetIssue($id: String!) {
issue(id: $id) {
id
identifier
title
url
}
}
```

**Comment creation:**
```graphql
mutation CreateComment($input: CommentCreateInput!) {
commentCreate(input: $input) {
success
comment { id }
}
}
```

### Rate Limiting

- Linear allows 1500 requests/hour for most plans
- The action caps at 20 tickets per run to stay well within limits
- If more than 20 tickets are found, a warning is logged and only the first 20 are processed

### Retry Logic

- 3 retries with 2-second delays for transient network errors
- GraphQL errors (issue not found, auth errors) are not retried
- 30-second timeout per request

## Comment Format

The comment posted on each Linear ticket looks like:

```
**Staging Deployment**
- **Repo:** injective-fe
- **Branch:** `feat/new-feature`
- **Staging URL:** https://staging.example.com
- **Author:** github-username
```

## Slack Thread Notification

After posting Linear comments, a Slack thread reply is added listing all linked tickets:

```
New staging link deployed (mainnet)
Description: Linear tickets linked:
- INJ-142 - Add login page
- SEC-146 - Fix auth vulnerability
Staging URL: https://staging.example.com
Author: github-username
```

Each ticket ID is a clickable link to the Linear issue.

## Configuration

### Required Secrets

| Secret | Description |
|--------|-------------|
| `LINEAR_API_KEY` | Linear API key with read issues + create comments permissions |

### Workflow Example

```yaml
- uses: ./actions/fe-staging-notification
with:
repo: ${{ inputs.repo }}
network: ${{ inputs.network }}
staging_url: ${{ steps.deploy.outputs.url }}
slack-user-token: ${{ secrets.SLACK_USER_TOKEN }}
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
linear-api-key: ${{ secrets.LINEAR_API_KEY }}
pr-title: ${{ github.event.pull_request.title }}
```

### For `workflow_dispatch` Triggers

Since `workflow_dispatch` events don't include commit data in the payload, pass commit messages explicitly:

```yaml
- name: Get recent commits
id: commits
run: echo "messages=$(git log --format='%s' -10)" >> $GITHUB_OUTPUT

- uses: ./actions/fe-staging-notification
with:
repo: ${{ inputs.repo }}
network: ${{ inputs.network }}
staging_url: ${{ steps.deploy.outputs.url }}
slack-user-token: ${{ secrets.SLACK_USER_TOKEN }}
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
linear-api-key: ${{ secrets.LINEAR_API_KEY }}
commit-messages: ${{ steps.commits.outputs.messages }}
```

## Backwards Compatibility

The Linear integration is fully optional. If `linear-api-key` is not provided:
- No Linear API calls are made
- `linear_tickets` and `linear_links` outputs are empty strings
- All existing Slack notification behavior is unchanged

## Error Handling

Linear integration failures are non-fatal:
- If a ticket ID doesn't exist in Linear, it's skipped with a log message
- If the Linear API is unreachable, a warning is logged and the action continues
- The Slack notification always completes regardless of Linear status
56 changes: 31 additions & 25 deletions actions/fe-staging-notification/README.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
# fe-staging-notification

A GitHub Action that extracts Jira tickets from commits and sends Slack notifications for frontend staging deployments.
A GitHub Action that sends Slack notifications for frontend staging deployments and integrates with Linear for ticket tracking.

## Features

- Extracts Jira tickets (IL-XXXXX) from commit messages
- Extracts Linear tickets (e.g., INJ-142, SEC-146) from commit messages and PR titles
- Posts staging URL as a comment on each Linear ticket
- Creates or updates Slack messages per branch
- Threads subsequent deployments to existing messages
- Replaces staging URL with latest (no accumulation)
- Deduplicates Jira tickets across the entire thread
- Non-fatal error handling (won't fail your CI)
- Built-in retry logic for Slack API calls
- Built-in retry logic for Slack and Linear API calls

## Usage

Expand All @@ -22,6 +22,8 @@ A GitHub Action that extracts Jira tickets from commits and sends Slack notifica
staging_url: "https://staging.example.com"
slack-user-token: ${{ secrets.SLACK_USER_TOKEN }}
slack-bot-token: ${{ secrets.SLACK_BOT_TOKEN }}
linear-api-key: ${{ secrets.LINEAR_API_KEY }}
pr-title: ${{ github.event.pull_request.title }}
```

## Inputs
Expand All @@ -36,20 +38,22 @@ A GitHub Action that extracts Jira tickets from commits and sends Slack notifica
| `slack-bot-token` | Yes | - | Slack bot token for sending messages |
| `staging_url` | Yes | - | URL of the staging deployment |
| `slack-channel` | No | "frontend-staging" | Slack channel name |
| `linear-api-key` | No | - | Linear API key for posting comments on tickets |
| `commit-messages` | No | - | Newline-separated commit messages (for workflow_dispatch) |
| `pr-title` | No | - | Pull request title (for extracting Linear tickets) |

## Outputs

| Output | Description |
|--------|-------------|
| `branch_name` | The branch name that was deployed |
| `jira_tickets` | Comma-separated list of Jira tickets found |
| `jira_links` | Formatted Jira links for Slack |
| `message_found` | Whether an existing Slack message was found |
| `existing_message_ts` | Timestamp of existing Slack message if found |
| `existing_channel_id` | Channel ID of existing Slack message if found |
| `existing_jira_tickets` | Jira tickets from existing Slack message |
| `channel_name` | Slack channel name used |
| `message_ts` | Timestamp of the Slack message |
| `linear_tickets` | Comma-separated list of Linear ticket IDs found |
| `linear_links` | Comma-separated list of Linear ticket URLs |

## Slack Token Requirements

Expand All @@ -76,12 +80,14 @@ fe-staging-notification/
├── src/ # Source code
│ ├── index.js # Main entry point - orchestrates the action
│ ├── git.js # Branch name detection
│ ├── jira.js # Jira ticket extraction from commits
│ ├── commits.js # Commit message extraction from event payload
│ ├── linear.js # Linear ticket extraction and API integration
│ └── slack.js # Slack API helpers with retry logic
├── __tests__/ # Unit tests (Vitest)
│ ├── git.test.js
│ ├── jira.test.js
│ ├── commits.test.js
│ ├── linear.test.js
│ └── slack.test.js
├── dist/ # Bundled output (auto-generated)
Expand All @@ -107,33 +113,33 @@ fe-staging-notification/
│ │
│ 1. Get inputs from action.yml │
│ 2. Detect branch name │
│ 3. Extract Jira tickets from commits
│ 3. Extract Linear tickets from commits │
│ 4. Search for existing Slack message │
│ 5. Update existing OR create new message │
│ 6. Set outputs │
└─────────────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ git.js │ │ jira.js │ slack.js │
└──────────┘ └──────────┘ └──────────┘
┌──────────┐ ┌────────────┌──────────┐ ┌──────────┐
│ git.js │ │ commits.js │ │linear.js │ │ slack.js │
└──────────┘ └────────────└──────────┘ └──────────┘

getBranchName() extractJiraTickets() slackRequest()
- INPUT_BRANCH - git fetch origin/dev - Retry logic (3x)
- Event file - git log - Rate limiting
- GITHUB_HEAD_REF - Pattern matching
- GITHUB_REF_NAME generateJiraLinks() searchExistingMessage()
- Slack formatting updateMessage()
postMessage()
postThreadReply()
addMessageId()
getBranchName() getCommitMessages() extractLinearTickets() slackRequest()
- INPUT_BRANCH - Push commits - Regex matching - Retry logic (3x)
- Event file - PR title/body lookupIssue() - Rate limiting
- GITHUB_HEAD_REF postIssueComment()
- GITHUB_REF_NAME formatLinearComment() searchExistingMessage()
updateMessage()
postMessage()
postThreadReply()
addMessageId()
```

### Key Design Decisions

1. **Dual Slack Tokens**: User token for search (API limitation), bot token for posting
2. **30-Day Search Limit**: Prevents finding stale messages from old deployments
3. **Thread-Based Deduplication**: Scans entire thread for Jira tickets, not just main message
3. **Linear Integration**: Posts staging URL as comments on extracted Linear tickets
4. **URL Replacement**: Shows only the latest staging URL (previous fix for URL accumulation)
5. **Non-Fatal Errors**: Logs warnings but never fails the CI pipeline
6. **Retry Logic**: 3 retries with 2-second delays for transient Slack errors
Expand Down Expand Up @@ -232,7 +238,7 @@ The repository is now configured to use the JS version:
After deployment, monitor for:

1. Slack messages appearing correctly
2. Jira tickets being extracted
2. Linear tickets being extracted and commented on
3. Thread replies working
4. No CI failures due to the action

Expand Down Expand Up @@ -293,7 +299,7 @@ If rollback was needed, investigate:
| "Could not determine branch name" | Missing workflow_dispatch input | Ensure workflow has `branch` input or pass it explicitly |
| "Slack API error: channel_not_found" | Wrong channel name or bot not in channel | Verify channel name, invite bot to channel |
| "Slack API error: invalid_auth" | Bad token | Regenerate Slack token |
| No Jira tickets found | Commits don't have IL-XXXXX pattern | Check commit message format |
| No Linear tickets found | Commits don't have TEAM-NUMBER pattern | Check commit message format (e.g., INJ-142) |
| Message not threading | Search didn't find existing message | Message may be >30 days old |

### Debug Mode
Expand Down
Loading