feat: update Linear issue tracking with project query and workflow state handling
This commit is contained in:
+159
-27
@@ -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 };
|
||||
Reference in New Issue
Block a user