Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,25 +40,12 @@ function fixture(
writeFileSync(
gh,
`#!${process.execPath}
import { readFileSync, writeFileSync, appendFileSync } from 'node:fs';
import { readFileSync, writeFileSync } from 'node:fs';
import { fakeGitHub } from ${JSON.stringify(join(import.meta.dir, "shipping.test-helper.ts"))};
const state = JSON.parse(readFileSync(process.env.SHIPPING_STATE, 'utf8'));
const args = process.argv.slice(2);
const query = args.find(arg => arg.startsWith('query='));
let result;
if (query.includes('disablePullRequestAutoMerge')) {
state.autoMergeRequest = null;
result = { disablePullRequestAutoMerge: { clientMutationId: null } };
} else if (query.includes('dequeuePullRequest')) {
if (!query.includes('dequeuePullRequest(input:{id:$id})')) {
console.error("DequeuePullRequestInput requires id");
process.exit(1);
}
state.mergeQueueEntry = null;
result = { dequeuePullRequest: { clientMutationId: null } };
} else result = { repository: { pullRequest: state } };
appendFileSync(process.env.SHIPPING_STATE + '.calls', query + '\\n');
const response = fakeGitHub(state, process.argv.slice(2));
writeFileSync(process.env.SHIPPING_STATE, JSON.stringify(state));
console.log(JSON.stringify({ data: result }));
console.log(JSON.stringify(response));
`
);
chmodSync(gh, 0o755);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// The slice of GitHub's GraphQL schema the shipping service depends on: the
// one required input field of each mutation, and the pull request field the
// mutation clears. Regenerate the input names with
// gh api graphql -f query='{ __type(name:"DequeuePullRequestInput") { inputFields { name } } }'
const MUTATIONS: Record<string, { input: string; clears: string }> = {
disablePullRequestAutoMerge: {
input: "pullRequestId",
clears: "autoMergeRequest",
},
dequeuePullRequest: { input: "id", clears: "mergeQueueEntry" },
};

const MUTATION =
/mutation\s*\w*\s*\(\s*\$id\s*:\s*ID!\s*\)\s*\{\s*(\w+)\(\s*input\s*:\s*\{\s*(\w+)\s*:\s*\$id\s*\}\s*\)/;

/**
* Answer one `gh api graphql` call the way GitHub would.
*
* A query returns only the pull request fields it selects. A mutation is
* refused unless it declares `$id` as `ID!`, names a known mutation and its
* required input field, and carries this pull request's node id.
*/
export function fakeGitHub(
pullRequest: Record<string, unknown>,
argv: readonly string[]
): { data: Record<string, unknown> } {
const query = argv.find((arg) => arg.startsWith("query=")) ?? "";
if (!query.includes("mutation")) {
// Drop nested selections, so `mergeQueueEntry { id }` does not count as
// selecting the pull request's own `id`.
const selection = query.replace(/\{[^{}]*\}/g, "");
const selected = Object.entries(pullRequest).filter(([field]) =>
new RegExp(`\\b${field}\\b`).test(selection)
);
return {
data: { repository: { pullRequest: Object.fromEntries(selected) } },
};
}
const [, name = "", input = ""] = MUTATION.exec(query) ?? [];
const contract = MUTATIONS[name];
if (!contract) throw new Error(`not a mutation GitHub accepts: ${query}`);
if (input !== contract.input)
throw new Error(`${name} input requires ${contract.input}, got ${input}`);
if (!argv.includes(`id=${pullRequest.id}`))
throw new Error("Could not resolve to a node with the given id");
pullRequest[contract.clears] = null;
return { data: { [name]: { clientMutationId: null } } };
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
type LandingRecord,
type ShippingService,
} from "./shipping.ts";
import { fakeGitHub } from "./shipping.test-helper.ts";

const context = { owner: "owner", repo: "repo", number: parsePrNumber(1) };
const empty: LandingRecord = {
Expand Down Expand Up @@ -227,25 +228,14 @@ describe("shipping GitHub boundary", () => {
});

it("uses separate mutations for both pending mechanisms", async () => {
let autoMergeRequest: { enabledAt: string } | null = { enabledAt: "now" };
let mergeQueueEntry: { id: string } | null = { id: "queue" };
const service = new GhShippingService(async (args) => {
const query = args.find((arg) => arg.startsWith("query=")) ?? "";
if (query.includes("disablePullRequestAutoMerge")) {
expect(args).toContain("id=pr-id");
autoMergeRequest = null;
return {
data: { disablePullRequestAutoMerge: { clientMutationId: null } },
};
}
if (query.includes("dequeuePullRequest")) {
expect(query).toContain("dequeuePullRequest(input:{id:$id})");
expect(args).toContain("id=pr-id");
mergeQueueEntry = null;
return { data: { dequeuePullRequest: { clientMutationId: null } } };
}
return response({ ...raw, autoMergeRequest, mergeQueueEntry });
});
const pullRequest = {
...raw,
autoMergeRequest: { enabledAt: "now" },
mergeQueueEntry: { id: "queue" },
};
const service = new GhShippingService(async (args) =>
fakeGitHub(pullRequest, args)
);
const expected = await service.inspect(context);
expect(await cancelPending(service, expected)).toEqual({
kind: "cancelled",
Expand Down
Loading