// Name: github project tasks
// Description: Fetch all tasks in a GitHub ProjectV2 using the GitHub GraphQL API
// and flattens the response into a flat object so that it can be easily inspected.
// Author: Vivek Kodira
import "@johnlindquist/kit";
const GITHUB_API_URL = "https://api.github.com/graphql";
const GITHUB_ACCESS_TOKEN = await env("GITHUB_ACCESS_TOKEN");
const GITHUB_PROJECT_ID = await env("GITHUB_PROJECT_ID");
// TypeScript types
interface FieldValue {
name?: string; // Status field (e.g., "Done", "In Progress")
}
interface ProjectItem {
id: string;
title: string;
fieldValues: {
nodes: FieldValue[];
};
}
interface PageInfo {
hasNextPage: boolean;
endCursor: string | null;
}
interface ProjectItemsResponse {
data?: {
node: {
items: {
nodes: ProjectItem[];
pageInfo: PageInfo;
};
};
};
errors?: { message: string }[];
}
// Fetch tasks in a GitHub ProjectV2 with pagination
async function fetchAllProjectTasks(projectId: string): Promise<ProjectItem[]> {
let tasks: ProjectItem[] = [];
let hasNextPage = true;
let endCursor: string | null = null;
while (hasNextPage) {
const query = `
query ($projectId: ID!, $after: String) {
node(id: $projectId) {
... on ProjectV2 {
items(first: 50, after: $after) {
nodes {
id
content {
... on Issue {
title
number
repository {
name
owner { login }
}
assignees(first: 5) {
nodes { login }
}
closed
}
}
fieldValues(first: 100) {
nodes {
... on ProjectV2ItemFieldSingleSelectValue {
name
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldTextValue {
text
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldNumberValue {
number
field { ... on ProjectV2FieldCommon { name } }
}
... on ProjectV2ItemFieldIterationValue {
title
field {
... on ProjectV2FieldCommon { name }
}
}
}
}
}
pageInfo {
hasNextPage
endCursor
}
}
}
}
}
`;
const variables = { projectId, after: endCursor };
const response = await fetch(GITHUB_API_URL, {
method: "POST",
headers: {
Authorization: `Bearer ${GITHUB_ACCESS_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ query, variables }),
});
const data: ProjectItemsResponse = await response.json();
if (data.errors) {
console.error("GraphQL Errors:", data.errors);
return tasks;
}
const items = data.data?.node.items;
if (!items) break;
tasks = tasks.concat(items.nodes);
// Update pagination info
hasNextPage = items.pageInfo.hasNextPage;
endCursor = items.pageInfo.endCursor;
}
return tasks;
}
// Flatten a GraphQL response into a flat object
const flattenGraphQLResponse = function (response) {
let flat = {};
// Extract top-level fields
flat["id"] = response.id;
if (response.content) {
flat["title"] = response.content.title;
flat["issue_number"] = response.content.number;
flat["repository"] = response.content.repository?.name;
flat["repo_owner"] = response.content.repository?.owner?.login;
flat["closed"] = response.content.closed;
// Extract assignees (comma-separated list)
flat["assignees"] = response.content?.assignees?.nodes
.map((a) => a.login)
.join(", ");
}
// Extract fieldValues
if (response.fieldValues) {
response.fieldValues.nodes.forEach((field) => {
if (field.field && field.field.name) {
if (field.text !== undefined) {
flat[field.field.name] = field.text;
} else if (field.name !== undefined) {
flat[field.field.name] = field.name;
} else if (field.number !== undefined) {
flat[field.field.name] = field.number;
} else if (field.title !== undefined) {
flat[field.field.name] = field.title;
}
}
});
}
return flat;
};
try {
let tasks = await fetchAllProjectTasks(GITHUB_PROJECT_ID);
let flattenedTasks = tasks.map((task) => flattenGraphQLResponse(task));
inspect(flattenedTasks, "falttened tasks");
} catch (error) {
console.log("Fetch Error:", error);
}