feat: update Linear issue tracking with project query and workflow state handling

This commit is contained in:
Do Siki
2025-09-05 03:44:04 +02:00
parent b6365543ef
commit 578a85ec1a
8 changed files with 501 additions and 247 deletions
+159 -27
View File
@@ -46,7 +46,8 @@ function parseArgs() {
const args = process.argv.slice(2);
const options = {
dryRun: false,
verbose: false
verbose: false,
projects: false
};
for (const arg of args) {
@@ -57,6 +58,9 @@ function parseArgs() {
case '--verbose':
options.verbose = true;
break;
case '--projects':
options.projects = true;
break;
case '--help':
console.log(`
🔄 Linear-TODO Sync Tool
@@ -66,11 +70,13 @@ Használat:
Opciók:
--dry-run Teszt üzemmód - nem hajt végre valódi változtatásokat
--verbose Részletes kimenet
--verbose Részletes kimenet
--projects Linear projektek lekérdezése
--help Ez a súgó
Példa:
node linear-sync.js --dry-run --verbose
node linear-sync.js --projects
`);
process.exit(0);
break;
@@ -89,7 +95,7 @@ function delay(ms) {
loadDotEnv();
// Configuration
const LINEAR_API_ENDPOINT = 'https://linear.app/graphql';
const LINEAR_API_ENDPOINT = 'https://api.linear.app/graphql';
const TEAM_ID = 'cf285407-a26b-434c-bb99-19676385ef67'; // Zeener team
const PROJECT_ID = '54559e3b-9005-4dfa-b7a9-1415cd4bc453'; // Website Development project
@@ -130,7 +136,7 @@ class LinearClient {
const data = JSON.stringify({ query, variables });
const options = {
hostname: 'linear.app',
hostname: 'api.linear.app',
path: '/graphql',
method: 'POST',
headers: {
@@ -190,6 +196,32 @@ class LinearClient {
});
}
async getProjects() {
const query = `
query GetProjects {
projects {
nodes {
id
name
description
state
targetDate
slugId
url
teams {
nodes {
id
name
}
}
}
}
}
`;
const result = await this.request(query);
return result.projects.nodes;
}
async getIssues() {
const query = `
query GetIssues($teamId: ID!) {
@@ -199,7 +231,10 @@ class LinearClient {
identifier
title
description
status
state {
id
name
}
project {
id
name
@@ -235,22 +270,37 @@ class LinearClient {
return result.issueCreate.issue;
}
async updateIssueStatus(issueId, status) {
async getWorkflowStates(teamId) {
const query = `
mutation UpdateIssueStatus($input: IssueUpdateInput!) {
issueUpdate(input: $input) {
issue {
query GetWorkflowStates($teamId: ID!) {
workflowStates(filter: { team: { id: { eq: $teamId } } }) {
nodes {
id
status
name
type
}
}
}
`;
const input = {
id: issueId,
status
};
const result = await this.request(query, { input });
const result = await this.request(query, { teamId });
return result.workflowStates.nodes;
}
async updateIssueStatus(issueId, stateId) {
const query = `
mutation UpdateIssueStatus($id: String!, $stateId: String!) {
issueUpdate(id: $id, input: { stateId: $stateId }) {
issue {
id
state {
id
name
}
}
}
}
`;
const result = await this.request(query, { id: issueId, stateId });
return result.issueUpdate.issue;
}
}
@@ -309,6 +359,69 @@ function updateTodoFile(content, ticketMappings) {
return updatedContent;
}
/**
* Query and display Linear projects
*/
async function queryProjects() {
console.log('🔍 Querying Linear projects...');
// Check for API key
const apiKey = process.env.LINEAR_API_KEY;
if (!apiKey || apiKey === 'your_linear_api_key_here') {
console.error('❌ ERROR: LINEAR_API_KEY is required in .env file');
console.log(' Add your Linear API key to .env file:');
console.log(' LINEAR_API_KEY=lin_api_your_actual_key_here');
console.log(' Get your API key from: https://linear.app/settings/api');
return;
}
const client = new LinearClient(apiKey);
try {
console.log('✅ API connection validated');
const projects = await client.getProjects();
console.log(`\n📋 Found ${projects.length} Linear projects:\n`);
projects.forEach((project, index) => {
console.log(`${index + 1}. ${project.name}`);
console.log(` ID: ${project.id}`);
console.log(` Slug: ${project.slugId}`);
console.log(` State: ${project.state}`);
console.log(` URL: ${project.url}`);
if (project.description) {
console.log(` Description: ${project.description}`);
}
if (project.targetDate) {
console.log(` Target Date: ${new Date(project.targetDate).toLocaleDateString()}`);
}
if (project.teams && project.teams.nodes.length > 0) {
const teamNames = project.teams.nodes.map(team => team.name).join(', ');
console.log(` Teams: ${teamNames}`);
}
console.log(''); // Empty line
});
if (projects.length === 0) {
console.log('⚠️ No projects found. This could mean:');
console.log(' - You don\'t have access to any projects');
console.log(' - Your API key doesn\'t have the right permissions');
console.log(' - There are no projects created yet');
}
} catch (error) {
console.error('❌ Failed to query projects:', error.message);
if (error.message.includes('GraphQL')) {
console.error(' Please check your LINEAR_API_KEY in .env file');
console.error(' Visit: https://linear.app/settings/api');
}
}
}
/**
* Main sync function
*/
@@ -360,8 +473,16 @@ async function syncTodoLinear() {
const existingIssues = await client.getIssues();
const existingTitles = existingIssues.map(i => i.title);
console.log(`${modePrefix}🔄 Fetching workflow states...`);
const workflowStates = await client.getWorkflowStates(TEAM_ID);
const doneState = workflowStates.find(s => s.type === 'completed') || workflowStates.find(s => s.name.toLowerCase().includes('done'));
if (OPTIONS.verbose) {
console.log(`${modePrefix}📋 Found ${existingIssues.length} Linear issues`);
console.log(`${modePrefix}🔄 Found ${workflowStates.length} workflow states`);
if (doneState) {
console.log(`${modePrefix}✅ Done state found: ${doneState.name} (${doneState.id})`);
}
}
// Find tasks missing Linear tickets
@@ -408,19 +529,26 @@ async function syncTodoLinear() {
console.log(`${modePrefix}📋 Updating Linear statuses for completed tasks...`);
let statusUpdateCount = 0;
for (const issue of existingIssues) {
if (todos.completed.some(t => t.task === issue.title) && issue.status !== 'Done') {
console.log(`${modePrefix}🔧 Updating status to Done: ${issue.identifier} - ${issue.title}`);
try {
await client.updateIssueStatus(issue.id, 'Done');
statusUpdateCount++;
if (OPTIONS.verbose) {
console.log(`${modePrefix}✅ Updated: ${issue.identifier}`);
if (doneState) {
for (const issue of existingIssues) {
const isCompleted = todos.completed.some(t => t.task === issue.title);
const isNotDone = issue.state.id !== doneState.id;
if (isCompleted && isNotDone) {
console.log(`${modePrefix}🔧 Updating status to ${doneState.name}: ${issue.identifier} - ${issue.title}`);
try {
await client.updateIssueStatus(issue.id, doneState.id);
statusUpdateCount++;
if (OPTIONS.verbose) {
console.log(`${modePrefix}✅ Updated: ${issue.identifier}`);
}
} catch (error) {
console.error(`❌ Failed to update: ${issue.identifier}`, error.message);
}
} catch (error) {
console.error(`❌ Failed to update: ${issue.identifier}`, error.message);
}
}
} else {
console.log(`${modePrefix}⚠️ Could not find 'Done' state - skipping status updates`);
}
console.log(`${modePrefix}📊 Updated ${statusUpdateCount} Linear issues`);
@@ -467,7 +595,11 @@ async function syncTodoLinear() {
// Run the sync if this script is executed directly
if (require.main === module) {
syncTodoLinear().catch(console.error);
if (OPTIONS.projects) {
queryProjects().catch(console.error);
} else {
syncTodoLinear().catch(console.error);
}
}
module.exports = { syncTodoLinear };
module.exports = { syncTodoLinear, queryProjects };