Claude-code-plugins-plus linear-multi-env-setup
install
source · Clone the upstream repo
git clone https://github.com/jeremylongshore/claude-code-plugins-plus-skills
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/jeremylongshore/claude-code-plugins-plus-skills "$T" && mkdir -p ~/.claude/skills && cp -r "$T/plugins/saas-packs/linear-pack/skills/linear-multi-env-setup" ~/.claude/skills/jeremylongshore-claude-code-plugins-plus-linear-multi-env-setup && rm -rf "$T"
manifest:
plugins/saas-packs/linear-pack/skills/linear-multi-env-setup/SKILL.mdsource content
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 | 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