diff --git a/action.js b/action.js index b34c63f00..b76d4e611 100644 --- a/action.js +++ b/action.js @@ -7,6 +7,7 @@ const { moveTaskToProjectSection, getProjectSections, } = require("./lib/actions/asana"); +const { getMobsuccessYMLFromRepo } = require("./lib/mobsuccessyml"); const customFieldLive = require("./lib/asana/custom-fields/live"); const customFieldStorybook = require("./lib/asana/custom-fields/storybook"); @@ -118,12 +119,14 @@ exports.findAsanaTaskId = function findAsanaTaskId({ }; exports.getActionParameters = function getActionParameters() { + const repository = github.context.payload.repository; const pullRequest = github.context.payload.pull_request; const action = core.getInput("action", { required: true }); const triggerPhrase = core.getInput("trigger-phrase") || ""; const amplifyUri = core.getInput("amplify-uri") || ""; const storybookAmplifyUri = core.getInput("storybook-amplify-uri") || ""; return { + repository, pullRequest, action, triggerPhrase, @@ -311,8 +314,31 @@ async function moveTaskToSprintAndEpicSection({ taskId, sectionId }) { } } +async function checkIfCanMergeWithoutAsanaTask({ repository, pullRequest }) { + const { assignees } = pullRequest; + const assigneeLogins = assignees.map(({ login }) => login); + if (!assigneeLogins.some((login) => login === "ms-testers")) { + return false; + } + + // if mobsuccess.yml has the `accept_ms_testers_without_closed_task` flag set to true, we can merge + const mobsuccessyml = await getMobsuccessYMLFromRepo({ + owner: repository.owner.login, + repo: repository.name, + }); + const asanaSettings = mobsuccessyml.asana || {}; + if (asanaSettings.accept_ms_testers_without_closed_task) { + console.log( + "accept_ms_testers_without_closed_task is set to true, ok to merge" + ); + return true; + } + return false; +} + exports.action = async function action() { const { + repository, pullRequest, action, triggerPhrase, @@ -391,7 +417,15 @@ exports.action = async function action() { }); console.log("Task is completed?", completed); if (!completed) { - throw new Error("Asana task is not yet completed, blocking merge"); + // check if can merge without a completed asana task + const canMergeWithoutAsanaTask = await checkIfCanMergeWithoutAsanaTask( + { repository, pullRequest } + ); + if (!canMergeWithoutAsanaTask) { + throw new Error( + "Asana task is not yet completed, blocking merge" + ); + } } } } diff --git a/action.test.js b/action.test.js index 2ef659b51..7f8c6b327 100644 --- a/action.test.js +++ b/action.test.js @@ -355,14 +355,61 @@ describe("Asana GitHub actions", () => { const spyGetAsanaPRStatus = jest.spyOn(action, "getAsanaPRStatus"); action.getAsanaPRStatus.mockImplementation(() => "test-value"); + let errorHasBeenThrown = false; try { await action.action(); } catch (error) { + errorHasBeenThrown = true; expect(error).toBeInstanceOf(Error); expect(error.message).toBe( "Asana task is not yet completed, blocking merge" ); } + expect(errorHasBeenThrown).toBe(true); + expect(spyGetAsanaPRStatus).toHaveBeenCalledTimes(1); + expect(spyGetAsanaPRStatus).toHaveBeenLastCalledWith({ pullRequest }); + }); + + test("synchronize should not fail for not completed task with asana: accept_ms_testers_without_closed_task", async () => { + jest.resetAllMocks(); + jest.resetModules(); + jest.mock("./lib/actions/asana"); + require("./lib/actions/asana").getTask.mockImplementation(() => ({ + completed: false, + memberships: [], + })); + jest.mock("./lib/mobsuccessyml"); + require("./lib/mobsuccessyml").getMobsuccessYMLFromRepo.mockImplementation( + () => ({ + asana: { accept_ms_testers_without_closed_task: true }, + }) + ); + + const action = require("./action"); + const pullRequest = { + number: 1234, + body: + "test-trigger-phrase https://app.asana.com/0/1200114135468212/1200114477821446/f", + requested_reviewers: [], + assignees: [{ login: "ms-testers" }], + }; + jest.spyOn(action, "getActionParameters"); + action.getActionParameters.mockImplementation(() => ({ + repository: { + owner: { + login: "test-owner", + }, + name: "test-repo", + }, + pullRequest, + action: "synchronize", + triggerPhrase: "test-trigger-phrase", + })); + + const spyGetAsanaPRStatus = jest.spyOn(action, "getAsanaPRStatus"); + action.getAsanaPRStatus.mockImplementation(() => "test-value"); + + await action.action(); expect(spyGetAsanaPRStatus).toHaveBeenCalledTimes(1); expect(spyGetAsanaPRStatus).toHaveBeenLastCalledWith({ pullRequest }); diff --git a/lib/mobsuccessyml.js b/lib/mobsuccessyml.js new file mode 100644 index 000000000..ccffe64e4 --- /dev/null +++ b/lib/mobsuccessyml.js @@ -0,0 +1,19 @@ +const yaml = require("js-yaml"); +const { octokit } = require("./actions/octokit"); + +exports.getMobsuccessYMLFromRepo = async function getMobsuccessYMLFromRepo({ + owner, + repo, +}) { + try { + const { data } = await octokit.rest.repos.getContent({ + owner, + repo, + path: ".mobsuccess.yml", + }); + const content = Buffer.from(data.content, "base64").toString("utf8"); + return yaml.load(content); + } catch (e) { + return {}; + } +};