Agent Skills: Linear Multi-Environment Setup

|

UncategorizedID: jeremylongshore/claude-code-plugins-plus-skills/linear-multi-env-setup

Install this agent skill to your local

pnpm dlx add-skill https://github.com/jeremylongshore/claude-code-plugins-plus-skills/tree/HEAD/plugins/saas-packs/linear-pack/skills/linear-multi-env-setup

Skill Files

Browse the full folder contents for linear-multi-env-setup.

Download Skill

Loading file tree…

plugins/saas-packs/linear-pack/skills/linear-multi-env-setup/SKILL.md

Skill Metadata

Name
linear-multi-env-setup
Description
|

Linear Multi-Environment Setup

Overview

Configure Linear integrations across dev, staging, and production with isolated API keys, secret management, environment guards, and per-environment webhook routing. Use separate Linear workspaces or at minimum separate API keys per environment.

Prerequisites

  • Separate Linear API keys per environment (dev, staging, prod)
  • Secret management (Vault, AWS Secrets Manager, GCP Secret Manager)
  • CI/CD pipeline with environment support
  • Node.js 18+

Instructions

Step 1: Environment Configuration

// src/config/linear.ts
import { LinearClient } from "@linear/sdk";

interface LinearEnvConfig {
  apiKey: string;
  webhookSecret: string;
  defaultTeamKey: string;
  enableWebhooks: boolean;
  enableDebugLogging: boolean;
  cacheEnabled: boolean;
}

type Environment = "development" | "staging" | "production" | "test";

function getEnvironment(): Environment {
  const env = process.env.NODE_ENV ?? "development";
  if (!["development", "staging", "production", "test"].includes(env)) {
    throw new Error(`Unknown NODE_ENV: ${env}`);
  }
  return env as Environment;
}

async function loadConfig(): Promise<LinearEnvConfig> {
  const env = getEnvironment();

  // In production, use secret manager instead of env vars
  if (env === "production" || env === "staging") {
    return {
      apiKey: await getSecret(`linear-api-key-${env}`),
      webhookSecret: await getSecret(`linear-webhook-secret-${env}`),
      defaultTeamKey: process.env.LINEAR_DEFAULT_TEAM_KEY ?? "ENG",
      enableWebhooks: true,
      enableDebugLogging: env === "staging",
      cacheEnabled: true,
    };
  }

  // Dev/test: use environment variables
  return {
    apiKey: process.env.LINEAR_API_KEY ?? "",
    webhookSecret: process.env.LINEAR_WEBHOOK_SECRET ?? "",
    defaultTeamKey: process.env.LINEAR_DEV_TEAM_KEY ?? "DEV",
    enableWebhooks: false, // No webhook server in local dev
    enableDebugLogging: true,
    cacheEnabled: false,
  };
}

Step 2: Secret Manager Integration

// GCP Secret Manager
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";

async function getSecret(name: string): Promise<string> {
  const client = new SecretManagerServiceClient();
  const projectId = process.env.GCP_PROJECT_ID!;
  const [version] = await client.accessSecretVersion({
    name: `projects/${projectId}/secrets/${name}/versions/latest`,
  });
  return version.payload?.data?.toString() ?? "";
}

// AWS Secrets Manager
import { SecretsManagerClient, GetSecretValueCommand } from "@aws-sdk/client-secrets-manager";

async function getSecretAWS(name: string): Promise<string> {
  const client = new SecretsManagerClient({});
  const result = await client.send(new GetSecretValueCommand({ SecretId: name }));
  return result.SecretString ?? "";
}

// HashiCorp Vault
async function getSecretVault(path: string): Promise<string> {
  const response = await fetch(`${process.env.VAULT_ADDR}/v1/${path}`, {
    headers: { "X-Vault-Token": process.env.VAULT_TOKEN! },
  });
  const data = await response.json();
  return data.data.data.value;
}

Step 3: Environment-Aware Client Factory

let _client: LinearClient | null = null;
let _config: LinearEnvConfig | null = null;

export async function getLinearClient(): Promise<LinearClient> {
  if (!_client) {
    _config = await loadConfig();
    if (!_config.apiKey) {
      throw new Error(`LINEAR_API_KEY not configured for ${getEnvironment()}`);
    }
    _client = new LinearClient({ apiKey: _config.apiKey });
  }
  return _client;
}

export async function getConfig(): Promise<LinearEnvConfig> {
  if (!_config) await getLinearClient(); // Triggers config load
  return _config!;
}

// For tests: inject a mock or test client
export function setTestClient(client: LinearClient) {
  _client = client;
}

Step 4: Environment Guards

Prevent dangerous operations from running in the wrong environment.

function requireProduction(operation: string) {
  if (getEnvironment() !== "production") {
    throw new Error(`${operation} is production-only (current: ${getEnvironment()})`);
  }
}

function preventProduction(operation: string) {
  if (getEnvironment() === "production") {
    throw new Error(`${operation} is forbidden in production`);
  }
}

// Usage
async function deleteAllTestIssues(teamKey: string) {
  preventProduction("deleteAllTestIssues"); // Safety guard

  const client = await getLinearClient();
  const issues = await client.issues({
    filter: {
      team: { key: { eq: teamKey } },
      title: { startsWith: "[TEST]" },
    },
  });

  for (const issue of issues.nodes) {
    await issue.delete();
  }
}

// Safe delete: archives in prod, deletes in dev
async function safeRemoveIssue(issueId: string) {
  const client = await getLinearClient();
  if (getEnvironment() === "production") {
    await client.archiveIssue(issueId);
  } else {
    await client.deleteIssue(issueId);
  }
}

Step 5: Per-Environment Webhooks

// Different webhook configs per environment
const webhookConfigs: Record<Environment, {
  resourceTypes: string[];
  enabled: boolean;
}> = {
  development: {
    resourceTypes: [], // No webhooks in dev — use polling/ngrok manually
    enabled: false,
  },
  staging: {
    resourceTypes: ["Issue", "Comment", "Project", "Cycle"],
    enabled: true,
  },
  production: {
    resourceTypes: ["Issue", "Comment", "Project", "Cycle", "IssueLabel", "ProjectUpdate"],
    enabled: true,
  },
  test: {
    resourceTypes: [],
    enabled: false,
  },
};

Step 6: CI/CD with Environment Secrets

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main, release/*]

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    environment: staging
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npm run deploy:staging
        env:
          LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
          LINEAR_WEBHOOK_SECRET: ${{ secrets.LINEAR_WEBHOOK_SECRET }}

  deploy-production:
    if: startsWith(github.ref, 'refs/heads/release/')
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v4
      - run: npm ci && npm run build
      - run: npm run deploy:production
        env:
          LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
          LINEAR_WEBHOOK_SECRET: ${{ secrets.LINEAR_WEBHOOK_SECRET }}

Step 7: Environment Validation Script

// scripts/validate-environment.ts
async function validateEnvironment() {
  const env = getEnvironment();
  console.log(`Validating Linear config for: ${env}\n`);

  const config = await loadConfig();

  const checks = [
    { name: "API Key", ok: config.apiKey.startsWith("lin_api_") },
    { name: "Webhook Secret", ok: !config.enableWebhooks || config.webhookSecret.length > 10 },
    { name: "Default Team", ok: config.defaultTeamKey.length > 0 },
  ];

  // Test API connectivity
  try {
    const client = new LinearClient({ apiKey: config.apiKey });
    const viewer = await client.viewer;
    const teams = await client.teams();
    const team = teams.nodes.find(t => t.key === config.defaultTeamKey);

    checks.push({ name: "API Auth", ok: true });
    checks.push({ name: "Default Team Exists", ok: !!team });

    console.log(`  User: ${viewer.name} (${viewer.email})`);
    console.log(`  Teams: ${teams.nodes.map(t => t.key).join(", ")}`);
  } catch (e: any) {
    checks.push({ name: "API Auth", ok: false });
    console.error(`  Auth failed: ${e.message}`);
  }

  for (const { name, ok } of checks) {
    console.log(`  ${ok ? "PASS" : "FAIL"}: ${name}`);
  }

  const failed = checks.filter(c => !c.ok).length;
  if (failed > 0) process.exit(1);
}

validateEnvironment();

Error Handling

| Error | Cause | Solution | |-------|-------|----------| | Wrong environment data | API key for wrong workspace | Verify secrets per environment | | Secret not found | Missing in secret manager | Add secret for the target environment | | Team not found | Wrong defaultTeamKey | Check team key matches the environment's workspace | | Permission denied | Insufficient API key scope | Regenerate with correct scopes |

Examples

Quick Validation

NODE_ENV=staging npx tsx scripts/validate-environment.ts
# Output:
# Validating Linear config for: staging
#   User: CI Bot (ci@company.com)
#   Teams: ENG, PRODUCT, DESIGN
#   PASS: API Key
#   PASS: Webhook Secret
#   PASS: Default Team
#   PASS: API Auth
#   PASS: Default Team Exists

Resources