From 117316c0b135a40f03f2ad6cc2c74640c43d0620 Mon Sep 17 00:00:00 2001
From: Rhys Arkins <rhys@keylocation.sg>
Date: Tue, 8 Aug 2017 23:03:52 +0200
Subject: [PATCH] fix: check current status check before setting (#655)

Fixes #649
---
 lib/api/github.js                             | 14 ++++-
 lib/api/gitlab.js                             | 18 ++++++
 lib/workers/branch/index.js                   | 22 ++++---
 test/api/github.spec.js                       | 60 +++++++++++++++++++
 test/api/gitlab.spec.js                       | 45 ++++++++++++++
 .../branch/__snapshots__/index.spec.js.snap   |  2 +
 test/workers/branch/index.spec.js             |  6 +-
 7 files changed, 157 insertions(+), 10 deletions(-)

diff --git a/lib/api/github.js b/lib/api/github.js
index 2a116ca4a8..6d30329ba3 100644
--- a/lib/api/github.js
+++ b/lib/api/github.js
@@ -59,6 +59,7 @@ module.exports = {
   isBranchStale,
   getBranchPr,
   getBranchStatus,
+  getBranchStatusCheck,
   setBranchStatus,
   deleteBranch,
   mergeBranch,
@@ -359,11 +360,22 @@ async function getBranchStatus(branchName, requiredStatusChecks) {
     return 'failed';
   }
   const gotString = `repos/${config.repoName}/commits/${branchName}/status`;
-  logger.debug(gotString);
   const res = await ghGotRetry(gotString);
   return res.body.state;
 }
 
+async function getBranchStatusCheck(branchName, context) {
+  const branchCommit = await getBranchCommit(branchName);
+  const url = `repos/${config.repoName}/commits/${branchCommit}/statuses`;
+  const res = await ghGotRetry(url);
+  for (const check of res.body) {
+    if (check.context === context) {
+      return check.state;
+    }
+  }
+  return null;
+}
+
 async function setBranchStatus(
   branchName,
   context,
diff --git a/lib/api/gitlab.js b/lib/api/gitlab.js
index a2976d3e59..c5391f4e6b 100644
--- a/lib/api/gitlab.js
+++ b/lib/api/gitlab.js
@@ -15,6 +15,7 @@ module.exports = {
   getBranch,
   getBranchPr,
   getBranchStatus,
+  getBranchStatusCheck,
   setBranchStatus,
   deleteBranch,
   // issue
@@ -222,6 +223,23 @@ async function getBranchStatus(branchName, requiredStatusChecks) {
   return status;
 }
 
+async function getBranchStatusCheck(branchName, context) {
+  // First, get the branch to find the commit SHA
+  let url = `projects/${config.repoName}/repository/branches/${branchName}`;
+  let res = await glGot(url);
+  const branchSha = res.body.commit.id;
+  // Now, check the statuses for that commit
+  url = `projects/${config.repoName}/repository/commits/${branchSha}/statuses`;
+  res = await glGot(url);
+  logger.debug(`Got res with ${res.body.length} results`);
+  for (const check of res.body) {
+    if (check.name === context) {
+      return check.state;
+    }
+  }
+  return null;
+}
+
 async function setBranchStatus(
   branchName,
   context,
diff --git a/lib/workers/branch/index.js b/lib/workers/branch/index.js
index d2f3bbbbe4..ac181b4b1e 100644
--- a/lib/workers/branch/index.js
+++ b/lib/workers/branch/index.js
@@ -199,17 +199,25 @@ async function ensureBranch(config) {
   }
   // Set unpublishable status check
   if (config.unpublishSafe && typeof unpublishable !== 'undefined') {
+    const context = 'renovate/unpublish-safe';
     const state = unpublishable ? 'success' : 'pending';
     const description = unpublishable
       ? 'Packages are at least 24 hours old'
       : 'Packages < 24 hours old can be unpublished';
-    await api.setBranchStatus(
-      branchName,
-      'renovate/unpublish-safe',
-      description,
-      state,
-      'https://github.com/singapore/renovate/blob/master/docs/status-checks.md#unpublish-safe'
-    );
+    // Check if state needs setting
+    const existingState = await api.getBranchStatusCheck(branchName, context);
+    if (existingState === state) {
+      logger.debug('Status check is already up-to-date');
+    } else {
+      logger.debug(`Updating status check state to ${state}`);
+      await api.setBranchStatus(
+        branchName,
+        context,
+        description,
+        state,
+        'https://github.com/singapore/renovate/blob/master/docs/status-checks.md#unpublish-safe'
+      );
+    }
   }
   if (config.automergeEnabled === false || config.automergeType === 'pr') {
     // No branch automerge
diff --git a/test/api/github.spec.js b/test/api/github.spec.js
index 6662ed1034..8bf3de3256 100644
--- a/test/api/github.spec.js
+++ b/test/api/github.spec.js
@@ -823,6 +823,66 @@ describe('api/github', () => {
       expect(res).toEqual('failed');
     });
   });
+  describe('getBranchStatusCheck', () => {
+    it('returns state if found', async () => {
+      await initRepo('some/repo', 'token');
+      // getBranchCommit
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          object: {
+            sha: '1235',
+          },
+        },
+      }));
+      ghGot.mockImplementationOnce(() => ({
+        body: [
+          {
+            context: 'context-1',
+            state: 'state-1',
+          },
+          {
+            context: 'context-2',
+            state: 'state-2',
+          },
+          {
+            context: 'context-3',
+            state: 'state-3',
+          },
+        ],
+      }));
+      const res = await github.getBranchStatusCheck('somebranch', 'context-2');
+      expect(res).toEqual('state-2');
+    });
+    it('returns null', async () => {
+      await initRepo('some/repo', 'token');
+      // getBranchCommit
+      ghGot.mockImplementationOnce(() => ({
+        body: {
+          object: {
+            sha: '1235',
+          },
+        },
+      }));
+      ghGot.mockImplementationOnce(() => ({
+        body: [
+          {
+            context: 'context-1',
+            state: 'state-1',
+          },
+          {
+            context: 'context-2',
+            state: 'state-2',
+          },
+          {
+            context: 'context-3',
+            state: 'state-3',
+          },
+        ],
+      }));
+      const res = await github.getBranchStatusCheck('somebranch', 'context-4');
+      expect(res).toEqual(null);
+    });
+  });
   describe('setBranchStatus', () => {
     it('sets branch status', async () => {
       await initRepo('some/repo', 'token');
diff --git a/test/api/gitlab.spec.js b/test/api/gitlab.spec.js
index f339228423..10ac368182 100644
--- a/test/api/gitlab.spec.js
+++ b/test/api/gitlab.spec.js
@@ -326,6 +326,51 @@ describe('api/gitlab', () => {
       expect(res).toEqual('foo');
     });
   });
+  describe('getBranchStatusCheck', () => {
+    beforeEach(() => {
+      glGot.mockReturnValueOnce({
+        body: {
+          commit: {
+            id: 1,
+          },
+        },
+      });
+    });
+    it('returns null if no results', async () => {
+      glGot.mockReturnValueOnce({
+        body: [],
+      });
+      const res = await gitlab.getBranchStatusCheck(
+        'somebranch',
+        'some-context'
+      );
+      expect(res).toEqual(null);
+    });
+    it('returns null if no matching results', async () => {
+      glGot.mockReturnValueOnce({
+        body: [{ name: 'context-1', status: 'pending' }],
+      });
+      const res = await gitlab.getBranchStatusCheck(
+        'somebranch',
+        'some-context'
+      );
+      expect(res).toEqual(null);
+    });
+    it('returns status if name found', async () => {
+      glGot.mockReturnValueOnce({
+        body: [
+          { name: 'context-1', state: 'pending' },
+          { name: 'some-context', state: 'success' },
+          { name: 'context-3', state: 'failed' },
+        ],
+      });
+      const res = await gitlab.getBranchStatusCheck(
+        'somebranch',
+        'some-context'
+      );
+      expect(res).toEqual('success');
+    });
+  });
   describe('setBranchStatus', () => {
     it('sets branch status', async () => {
       await initRepo('some/repo', 'token');
diff --git a/test/workers/branch/__snapshots__/index.spec.js.snap b/test/workers/branch/__snapshots__/index.spec.js.snap
index 2ad706187a..61afb6cc4f 100644
--- a/test/workers/branch/__snapshots__/index.spec.js.snap
+++ b/test/workers/branch/__snapshots__/index.spec.js.snap
@@ -13,6 +13,7 @@ Object {
       "branchExists": [Function],
       "commitFilesToBranch": [Function],
       "getBranchStatus": [Function],
+      "getBranchStatusCheck": [Function],
       "getFileContent": [Function],
       "mergeBranch": [Function],
       "setBranchStatus": [Function],
@@ -38,6 +39,7 @@ Object {
       "branchExists": [Function],
       "commitFilesToBranch": [Function],
       "getBranchStatus": [Function],
+      "getBranchStatusCheck": [Function],
       "getFileContent": [Function],
       "mergeBranch": [Function],
       "setBranchStatus": [Function],
diff --git a/test/workers/branch/index.spec.js b/test/workers/branch/index.spec.js
index c0b57d4d53..b97729532e 100644
--- a/test/workers/branch/index.spec.js
+++ b/test/workers/branch/index.spec.js
@@ -119,6 +119,7 @@ describe('workers/branch', () => {
       config.api.commitFilesToBranch = jest.fn();
       config.api.getFileContent.mockReturnValueOnce('old content');
       config.api.getBranchStatus = jest.fn();
+      config.api.getBranchStatusCheck = jest.fn();
       config.api.setBranchStatus = jest.fn();
       config.depName = 'dummy';
       config.currentVersion = '1.0.0';
@@ -173,13 +174,14 @@ describe('workers/branch', () => {
       expect(await branchWorker.ensureBranch(config)).toBe(true);
       expect(config.api.setBranchStatus.mock.calls).toHaveLength(1);
     });
-    it('sets branch status success', async () => {
+    it('skips branch status success', async () => {
       branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
       packageJsonHelper.setNewValue.mockReturnValueOnce('new content');
       config.api.branchExists.mockReturnValueOnce(true);
       config.upgrades[0].unpublishable = true;
+      config.api.getBranchStatusCheck.mockReturnValueOnce('success');
       expect(await branchWorker.ensureBranch(config)).toBe(true);
-      expect(config.api.setBranchStatus.mock.calls).toHaveLength(1);
+      expect(config.api.setBranchStatus.mock.calls).toHaveLength(0);
     });
     it('automerges successful branches', async () => {
       branchWorker.getParentBranch.mockReturnValueOnce('dummy branch');
-- 
GitLab