diff --git a/src/queries/all-open-prs-query.ts b/src/queries/all-open-prs-query.ts index 4d639bd7d..91a16b354 100644 --- a/src/queries/all-open-prs-query.ts +++ b/src/queries/all-open-prs-query.ts @@ -1,16 +1,15 @@ import { gql, TypedDocumentNode } from "@apollo/client/core"; import { client } from "../graphql-client"; -import { GetAllOpenPRsAndCardIDs, GetAllOpenPRsAndCardIDsVariables } from "./schema/GetAllOpenPRsAndCardIDs"; +import { GetAllOpenPRs, GetAllOpenPRsVariables } from "./schema/GetAllOpenPRs"; import { noNullish } from "../util/util"; -export const getAllOpenPRsAndCardIDsQuery: TypedDocumentNode = gql` -query GetAllOpenPRsAndCardIDs($endCursor: String) { +export const getAllOpenPRsQuery: TypedDocumentNode = gql` +query GetAllOpenPRs($endCursor: String) { repository(owner: "DefinitelyTyped", name: "DefinitelyTyped") { id pullRequests(states: OPEN, orderBy: { field: UPDATED_AT, direction: DESC }, first: 100, after: $endCursor) { nodes { number - projectCards(first: 100) { nodes { id } } } pageInfo { hasNextPage @@ -20,22 +19,18 @@ query GetAllOpenPRsAndCardIDs($endCursor: String) { } }`; -export async function getAllOpenPRsAndCardIDs() { +export async function getAllOpenPRs() { const prNumbers: number[] = []; - const cardIDs: string[] = []; let endCursor: string | undefined | null; while (true) { const result = await client.query({ - query: getAllOpenPRsAndCardIDsQuery, + query: getAllOpenPRsQuery, fetchPolicy: "no-cache", variables: { endCursor }, }); prNumbers.push(...noNullish(result.data.repository?.pullRequests.nodes).map(pr => pr.number)); - for (const pr of noNullish(result.data.repository?.pullRequests.nodes)) { - cardIDs.push(...noNullish(pr.projectCards.nodes).map(card => card.id)); - } if (!result.data.repository?.pullRequests.pageInfo.hasNextPage) { - return { prNumbers, cardIDs }; + return prNumbers; } endCursor = result.data.repository.pullRequests.pageInfo.endCursor; } diff --git a/src/queries/card-id-to-pr-query.ts b/src/queries/card-id-to-pr-query.ts deleted file mode 100644 index da7122c80..000000000 --- a/src/queries/card-id-to-pr-query.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { gql, TypedDocumentNode } from "@apollo/client/core"; -import { client } from "../graphql-client"; -import { PullRequestState } from "./schema/graphql-global-types"; -import { CardIdToPr, CardIdToPrVariables } from "./schema/CardIdToPr"; - -interface CardPRInfo { - number: number; - state: PullRequestState; -} - -export const runQueryToGetPRForCardId = async (id: string): Promise => { - const info = await client.query({ - query: gql` - query CardIdToPr($id: ID!) { - node(id: $id) { - ... on ProjectCard { content { ... on PullRequest { state number } } } - } - }` as TypedDocumentNode, - variables: { id }, - fetchPolicy: "no-cache", - }); - const node = info.data.node; - return (node?.__typename === "ProjectCard" && node.content?.__typename === "PullRequest") - ? { number: node.content.number, state: node.content.state } - : undefined; -}; diff --git a/src/queries/projectboard-cards.ts b/src/queries/projectboard-cards.ts index eea31dd93..8a467908a 100644 --- a/src/queries/projectboard-cards.ts +++ b/src/queries/projectboard-cards.ts @@ -1,6 +1,7 @@ import { gql, TypedDocumentNode } from "@apollo/client/core"; import { client } from "../graphql-client"; import { GetProjectBoardCards } from "./schema/GetProjectBoardCards"; +import { noNullish } from "../util/util"; const GetProjectBoardCardsQuery: TypedDocumentNode = gql` query GetProjectBoardCards { @@ -17,6 +18,11 @@ const GetProjectBoardCardsQuery: TypedDocumentNode nodes { id updatedAt + content { + ... on PullRequest { + number + } + } } } } @@ -25,16 +31,6 @@ const GetProjectBoardCardsQuery: TypedDocumentNode } }`; -interface CardInfo { - id: string; - updatedAt: string; -} -interface ColumnInfo { - name: string; - totalCount: number; - cards: CardInfo[]; -} - export async function getProjectBoardCards() { const results = await client.query({ query: GetProjectBoardCardsQuery, @@ -47,17 +43,13 @@ export async function getProjectBoardCards() { throw new Error("No project found"); } - const columns: ColumnInfo[] = []; - project.columns.nodes?.forEach(col => { - if (!col) return; - const cards: CardInfo[] = []; - col.cards.nodes?.forEach(card => card && cards.push({ id: card.id, updatedAt: card.updatedAt })); - columns.push({ - name: col.name, - totalCount: col.cards.totalCount, - cards, - }); - }); - - return columns; + return noNullish(project.columns.nodes).map(column => ({ + name: column.name, + totalCount: column.cards.totalCount, + cards: noNullish(column.cards.nodes).map(card => ({ + id: card.id, + updatedAt: card.updatedAt, + number: card.content && "number" in card.content ? card.content.number : undefined, + })), + })); } diff --git a/src/queries/schema/CardIdToPr.ts b/src/queries/schema/CardIdToPr.ts deleted file mode 100644 index 96512712e..000000000 --- a/src/queries/schema/CardIdToPr.ts +++ /dev/null @@ -1,53 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -import { PullRequestState } from "./graphql-global-types"; - -// ==================================================== -// GraphQL query operation: CardIdToPr -// ==================================================== - -export interface CardIdToPr_node_CodeOfConduct { - __typename: "CodeOfConduct" | "Enterprise" | "EnterpriseUserAccount" | "Organization" | "Package" | "PackageVersion" | "PackageFile" | "Release" | "User" | "Project" | "ProjectColumn" | "Issue" | "UserContentEdit" | "Label" | "PullRequest" | "Reaction" | "Repository" | "License" | "BranchProtectionRule" | "Ref" | "PushAllowance" | "App" | "Team" | "UserStatus" | "TeamDiscussion" | "TeamDiscussionComment" | "OrganizationInvitation" | "ReviewDismissalAllowance" | "CommitComment" | "Commit" | "CheckSuite" | "CheckRun" | "Push" | "Deployment" | "DeploymentStatus" | "Status" | "StatusContext" | "StatusCheckRollup" | "Tree" | "DeployKey" | "Language" | "Milestone" | "RepositoryTopic" | "Topic" | "RepositoryVulnerabilityAlert" | "SecurityAdvisory" | "IssueComment" | "PullRequestCommit" | "PullRequestReview" | "PullRequestReviewComment" | "ReviewRequest" | "Mannequin" | "PullRequestReviewThread" | "AssignedEvent" | "Bot" | "BaseRefDeletedEvent" | "BaseRefForcePushedEvent" | "ClosedEvent" | "CommitCommentThread" | "CrossReferencedEvent" | "DemilestonedEvent" | "DeployedEvent" | "DeploymentEnvironmentChangedEvent" | "HeadRefDeletedEvent" | "HeadRefForcePushedEvent" | "HeadRefRestoredEvent" | "LabeledEvent" | "LockedEvent" | "MergedEvent" | "MilestonedEvent" | "ReferencedEvent" | "RenamedTitleEvent" | "ReopenedEvent" | "ReviewDismissedEvent" | "ReviewRequestRemovedEvent" | "ReviewRequestedEvent" | "SubscribedEvent" | "UnassignedEvent" | "UnlabeledEvent" | "UnlockedEvent" | "UnsubscribedEvent" | "UserBlockedEvent" | "AddedToProjectEvent" | "AutoMergeDisabledEvent" | "AutoMergeEnabledEvent" | "AutoRebaseEnabledEvent" | "AutoSquashEnabledEvent" | "AutomaticBaseChangeFailedEvent" | "AutomaticBaseChangeSucceededEvent" | "BaseRefChangedEvent" | "CommentDeletedEvent" | "ConnectedEvent" | "ConvertToDraftEvent" | "ConvertedNoteToIssueEvent" | "DisconnectedEvent" | "MarkedAsDuplicateEvent" | "MentionedEvent" | "MovedColumnsInProjectEvent" | "PinnedEvent" | "PullRequestCommitCommentThread" | "ReadyForReviewEvent" | "RemovedFromProjectEvent" | "TransferredEvent" | "UnmarkedAsDuplicateEvent" | "UnpinnedEvent" | "Gist" | "GistComment" | "SponsorsListing" | "SponsorsTier" | "Sponsorship" | "PublicKey" | "SavedReply" | "ReleaseAsset" | "MembersCanDeleteReposClearAuditEntry" | "MembersCanDeleteReposDisableAuditEntry" | "MembersCanDeleteReposEnableAuditEntry" | "OauthApplicationCreateAuditEntry" | "OrgAddBillingManagerAuditEntry" | "OrgAddMemberAuditEntry" | "OrgBlockUserAuditEntry" | "OrgConfigDisableCollaboratorsOnlyAuditEntry" | "OrgConfigEnableCollaboratorsOnlyAuditEntry" | "OrgCreateAuditEntry" | "OrgDisableOauthAppRestrictionsAuditEntry" | "OrgDisableSamlAuditEntry" | "OrgDisableTwoFactorRequirementAuditEntry" | "OrgEnableOauthAppRestrictionsAuditEntry" | "OrgEnableSamlAuditEntry" | "OrgEnableTwoFactorRequirementAuditEntry" | "OrgInviteMemberAuditEntry" | "OrgInviteToBusinessAuditEntry" | "OrgOauthAppAccessApprovedAuditEntry" | "OrgOauthAppAccessDeniedAuditEntry" | "OrgOauthAppAccessRequestedAuditEntry" | "OrgRemoveBillingManagerAuditEntry" | "OrgRemoveMemberAuditEntry" | "OrgRemoveOutsideCollaboratorAuditEntry" | "OrgRestoreMemberAuditEntry" | "OrgUnblockUserAuditEntry" | "OrgUpdateDefaultRepositoryPermissionAuditEntry" | "OrgUpdateMemberAuditEntry" | "OrgUpdateMemberRepositoryCreationPermissionAuditEntry" | "OrgUpdateMemberRepositoryInvitationPermissionAuditEntry" | "PrivateRepositoryForkingDisableAuditEntry" | "PrivateRepositoryForkingEnableAuditEntry" | "RepoAccessAuditEntry" | "RepoAddMemberAuditEntry" | "RepoAddTopicAuditEntry" | "RepoArchivedAuditEntry" | "RepoChangeMergeSettingAuditEntry" | "RepoConfigDisableAnonymousGitAccessAuditEntry" | "RepoConfigDisableCollaboratorsOnlyAuditEntry" | "RepoConfigDisableContributorsOnlyAuditEntry" | "RepoConfigDisableSockpuppetDisallowedAuditEntry" | "RepoConfigEnableAnonymousGitAccessAuditEntry" | "RepoConfigEnableCollaboratorsOnlyAuditEntry" | "RepoConfigEnableContributorsOnlyAuditEntry" | "RepoConfigEnableSockpuppetDisallowedAuditEntry" | "RepoConfigLockAnonymousGitAccessAuditEntry" | "RepoConfigUnlockAnonymousGitAccessAuditEntry" | "RepoCreateAuditEntry" | "RepoDestroyAuditEntry" | "RepoRemoveMemberAuditEntry" | "RepoRemoveTopicAuditEntry" | "RepositoryVisibilityChangeDisableAuditEntry" | "RepositoryVisibilityChangeEnableAuditEntry" | "TeamAddMemberAuditEntry" | "TeamAddRepositoryAuditEntry" | "TeamChangeParentTeamAuditEntry" | "TeamRemoveMemberAuditEntry" | "TeamRemoveRepositoryAuditEntry" | "VerifiableDomain" | "IpAllowListEntry" | "OrganizationIdentityProvider" | "ExternalIdentity" | "EnterpriseServerInstallation" | "EnterpriseServerUserAccount" | "EnterpriseServerUserAccountEmail" | "EnterpriseServerUserAccountsUpload" | "EnterpriseRepositoryInfo" | "EnterpriseAdministratorInvitation" | "RepositoryInvitation" | "EnterpriseIdentityProvider" | "MarketplaceCategory" | "MarketplaceListing" | "Blob" | "PackageTag" | "Tag"; -} - -export interface CardIdToPr_node_ProjectCard_content_Issue { - __typename: "Issue"; -} - -export interface CardIdToPr_node_ProjectCard_content_PullRequest { - __typename: "PullRequest"; - /** - * Identifies the state of the pull request. - */ - state: PullRequestState; - /** - * Identifies the pull request number. - */ - number: number; -} - -export type CardIdToPr_node_ProjectCard_content = CardIdToPr_node_ProjectCard_content_Issue | CardIdToPr_node_ProjectCard_content_PullRequest; - -export interface CardIdToPr_node_ProjectCard { - __typename: "ProjectCard"; - /** - * The card content item - */ - content: CardIdToPr_node_ProjectCard_content | null; -} - -export type CardIdToPr_node = CardIdToPr_node_CodeOfConduct | CardIdToPr_node_ProjectCard; - -export interface CardIdToPr { - /** - * Fetches an object given its ID. - */ - node: CardIdToPr_node | null; -} - -export interface CardIdToPrVariables { - id: string; -} diff --git a/src/queries/schema/GetAllOpenPRs.ts b/src/queries/schema/GetAllOpenPRs.ts new file mode 100644 index 000000000..884ffab71 --- /dev/null +++ b/src/queries/schema/GetAllOpenPRs.ts @@ -0,0 +1,60 @@ +/* tslint:disable */ +/* eslint-disable */ +// @generated +// This file was automatically generated and should not be edited. + +// ==================================================== +// GraphQL query operation: GetAllOpenPRs +// ==================================================== + +export interface GetAllOpenPRs_repository_pullRequests_nodes { + __typename: "PullRequest"; + /** + * Identifies the pull request number. + */ + number: number; +} + +export interface GetAllOpenPRs_repository_pullRequests_pageInfo { + __typename: "PageInfo"; + /** + * When paginating forwards, are there more items? + */ + hasNextPage: boolean; + /** + * When paginating forwards, the cursor to continue. + */ + endCursor: string | null; +} + +export interface GetAllOpenPRs_repository_pullRequests { + __typename: "PullRequestConnection"; + /** + * A list of nodes. + */ + nodes: (GetAllOpenPRs_repository_pullRequests_nodes | null)[] | null; + /** + * Information to aid in pagination. + */ + pageInfo: GetAllOpenPRs_repository_pullRequests_pageInfo; +} + +export interface GetAllOpenPRs_repository { + __typename: "Repository"; + id: string; + /** + * A list of pull requests that have been opened in the repository. + */ + pullRequests: GetAllOpenPRs_repository_pullRequests; +} + +export interface GetAllOpenPRs { + /** + * Lookup a given repository by the owner and repository name. + */ + repository: GetAllOpenPRs_repository | null; +} + +export interface GetAllOpenPRsVariables { + endCursor?: string | null; +} diff --git a/src/queries/schema/GetAllOpenPRsAndCardIDs.ts b/src/queries/schema/GetAllOpenPRsAndCardIDs.ts deleted file mode 100644 index 12306dcc5..000000000 --- a/src/queries/schema/GetAllOpenPRsAndCardIDs.ts +++ /dev/null @@ -1,77 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -// @generated -// This file was automatically generated and should not be edited. - -// ==================================================== -// GraphQL query operation: GetAllOpenPRsAndCardIDs -// ==================================================== - -export interface GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes_projectCards_nodes { - __typename: "ProjectCard"; - id: string; -} - -export interface GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes_projectCards { - __typename: "ProjectCardConnection"; - /** - * A list of nodes. - */ - nodes: (GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes_projectCards_nodes | null)[] | null; -} - -export interface GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes { - __typename: "PullRequest"; - /** - * Identifies the pull request number. - */ - number: number; - /** - * List of project cards associated with this pull request. - */ - projectCards: GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes_projectCards; -} - -export interface GetAllOpenPRsAndCardIDs_repository_pullRequests_pageInfo { - __typename: "PageInfo"; - /** - * When paginating forwards, are there more items? - */ - hasNextPage: boolean; - /** - * When paginating forwards, the cursor to continue. - */ - endCursor: string | null; -} - -export interface GetAllOpenPRsAndCardIDs_repository_pullRequests { - __typename: "PullRequestConnection"; - /** - * A list of nodes. - */ - nodes: (GetAllOpenPRsAndCardIDs_repository_pullRequests_nodes | null)[] | null; - /** - * Information to aid in pagination. - */ - pageInfo: GetAllOpenPRsAndCardIDs_repository_pullRequests_pageInfo; -} - -export interface GetAllOpenPRsAndCardIDs_repository { - __typename: "Repository"; - id: string; - /** - * A list of pull requests that have been opened in the repository. - */ - pullRequests: GetAllOpenPRsAndCardIDs_repository_pullRequests; -} - -export interface GetAllOpenPRsAndCardIDs { - /** - * Lookup a given repository by the owner and repository name. - */ - repository: GetAllOpenPRsAndCardIDs_repository | null; -} - -export interface GetAllOpenPRsAndCardIDsVariables { - endCursor?: string | null; -} diff --git a/src/queries/schema/GetProjectBoardCards.ts b/src/queries/schema/GetProjectBoardCards.ts index a4ca22109..1c07d0038 100644 --- a/src/queries/schema/GetProjectBoardCards.ts +++ b/src/queries/schema/GetProjectBoardCards.ts @@ -7,6 +7,20 @@ // GraphQL query operation: GetProjectBoardCards // ==================================================== +export interface GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content_Issue { + __typename: "Issue"; +} + +export interface GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content_PullRequest { + __typename: "PullRequest"; + /** + * Identifies the pull request number. + */ + number: number; +} + +export type GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content = GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content_Issue | GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content_PullRequest; + export interface GetProjectBoardCards_repository_project_columns_nodes_cards_nodes { __typename: "ProjectCard"; id: string; @@ -14,6 +28,10 @@ export interface GetProjectBoardCards_repository_project_columns_nodes_cards_nod * Identifies the date and time when the object was last updated. */ updatedAt: any; + /** + * The card content item + */ + content: GetProjectBoardCards_repository_project_columns_nodes_cards_nodes_content | null; } export interface GetProjectBoardCards_repository_project_columns_nodes_cards { diff --git a/src/run.ts b/src/run.ts index 1d7ce4506..ad19827bc 100755 --- a/src/run.ts +++ b/src/run.ts @@ -3,11 +3,10 @@ import * as schema from "@octokit/graphql-schema/schema"; import * as yargs from "yargs"; import { process as computeActions } from "./compute-pr-actions"; -import { getAllOpenPRsAndCardIDs } from "./queries/all-open-prs-query"; +import { getAllOpenPRs } from "./queries/all-open-prs-query"; import { queryPRInfo, deriveStateForPR } from "./pr-info"; import { executePrActions } from "./execute-pr-actions"; import { getProjectBoardCards } from "./queries/projectboard-cards"; -import { runQueryToGetPRForCardId } from "./queries/card-id-to-pr-query"; import { createMutation, client } from "./graphql-client"; import { render } from "prettyjson"; import { inspect } from "util"; @@ -60,35 +59,39 @@ const show = (name: string, value: unknown) => { console.log(str); }; +async function processSingle(pr: number) { + // Generate the info for the PR from scratch + const info = await queryPRInfo(pr); + if (args["show-raw"]) show("Raw Query Result", info); + const prInfo = info.data.repository?.pullRequest; + // If it didn't work, bail early + if (!prInfo) { + console.error(` No PR with this number exists, (${JSON.stringify(info)})`); + return; + } + const state = await deriveStateForPR(prInfo); + if (args["show-basic"]) show("Basic PR Info", state); + // Show errors in log but keep processing to show in a comment too + if (state.type === "error") console.error(` Error: ${state.message}`); + // Show other messages too + if ("message" in state) console.log(` ... ${state.message}`); + // Convert the info to a set of actions for the bot + const actions = computeActions(state, + args["show-extended"] ? i => show("Extended Info", i) : undefined); + if (args["show-actions"]) show("Actions", actions); + // Act on the actions + const mutations = await executePrActions(actions, prInfo, args.dry); + if (args["show-mutations"] ?? args.dry) show("Mutations", mutations); +} + const start = async function () { console.log(`Getting open PRs.`); - const { prNumbers: prs, cardIDs } = await getAllOpenPRsAndCardIDs(); + const prs = await getAllOpenPRs(); // for (const pr of prs) { if (!shouldRunOn(pr)) continue; console.log(`Processing #${pr} (${prs.indexOf(pr) + 1} of ${prs.length})...`); - // Generate the info for the PR from scratch - const info = await queryPRInfo(pr); - if (args["show-raw"]) show("Raw Query Result", info); - const prInfo = info.data.repository?.pullRequest; - // If it didn't work, bail early - if (!prInfo) { - console.error(` No PR with this number exists, (${JSON.stringify(info)})`); - continue; - } - const state = await deriveStateForPR(prInfo); - if (args["show-basic"]) show("Basic PR Info", state); - // Show errors in log but keep processing to show in a comment too - if (state.type === "error") console.error(` Error: ${state.message}`); - // Show other messages too - if ("message" in state) console.log(` ... ${state.message}`); - // Convert the info to a set of actions for the bot - const actions = computeActions(state, - args["show-extended"] ? i => show("Extended Info", i) : undefined); - if (args["show-actions"]) show("Actions", actions); - // Act on the actions - const mutations = await executePrActions(actions, prInfo, args.dry); - if (args["show-mutations"] ?? args.dry) show("Mutations", mutations); + await processSingle(pr); } if (args.dry || !args.cleanup) return; // @@ -110,7 +113,7 @@ const start = async function () { throw new Error(`Could not find the 'Recently Merged' column in ${columns.map(n => n.name)}`); } const { cards, totalCount } = recentlyMerged; - const afterFirst50 = cards.sort((l, r) => l.updatedAt.localeCompare(r.updatedAt)).slice(50); + const afterFirst50 = cards.sort((l, r) => Date.parse(l.updatedAt) - Date.parse(r.updatedAt)).slice(50); if (afterFirst50.length > 0) { console.log(`Cutting "Recently Merged" projects to the last 50`); if (cards.length < totalCount) { @@ -122,15 +125,12 @@ const start = async function () { // Handle other columns for (const column of columns) { if (column.name === "Recently Merged") continue; - const ids = column.cards.map(c => c.id).filter(c => !cardIDs.includes(c)); - if (ids.length === 0) continue; + const cleanup = column.cards.map(card => card.number).filter((number): number is NonNullable => + !!number && !prs.includes(number)); + if (cleanup.length === 0) continue; console.log(`Cleaning up closed PRs in "${column.name}"`); - // don't actually do the deletions, until I follow this and make sure that it's working fine - for (const id of ids) { - const info = await runQueryToGetPRForCardId(id); - await deleteObject(id, info === undefined ? "???" - : info.state === "CLOSED" ? undefined - : "#" + info.number); + for (const number of cleanup) { + await processSingle(number); } } console.log("Done");