diff --git a/lib/platform/bitbucket/comments.ts b/lib/platform/bitbucket/comments.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dcff83a27182d29dcf486dd80127456c912bfdf6
--- /dev/null
+++ b/lib/platform/bitbucket/comments.ts
@@ -0,0 +1,120 @@
+import { logger } from '../../logger';
+import { Config, accumulateValues } from './utils';
+import { api } from './bb-got-wrapper';
+
+interface Comment {
+  content: { raw: string };
+  id: number;
+}
+
+export type CommentsConfig = Pick<Config, 'repository'>;
+
+async function getComments(config: CommentsConfig, prNo: number) {
+  const comments = await accumulateValues<Comment>(
+    `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments`
+  );
+
+  logger.debug(`Found ${comments.length} comments`);
+  return comments;
+}
+
+async function addComment(config: CommentsConfig, prNo: number, raw: string) {
+  await api.post(
+    `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments`,
+    {
+      body: { content: { raw } },
+    }
+  );
+}
+
+async function editComment(
+  config: CommentsConfig,
+  prNo: number,
+  commentId: number,
+  raw: string
+) {
+  await api.put(
+    `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}`,
+    {
+      body: { content: { raw } },
+    }
+  );
+}
+
+async function deleteComment(
+  config: CommentsConfig,
+  prNo: number,
+  commentId: number
+) {
+  await api.delete(
+    `/2.0/repositories/${config.repository}/pullrequests/${prNo}/comments/${commentId}`
+  );
+}
+
+export async function ensureComment(
+  config: CommentsConfig,
+  prNo: number,
+  topic: string | null,
+  content: string
+) {
+  try {
+    const comments = await getComments(config, prNo);
+    let body: string;
+    let commentId: number | undefined;
+    let commentNeedsUpdating: boolean | undefined;
+    if (topic) {
+      logger.debug(`Ensuring comment "${topic}" in #${prNo}`);
+      body = `### ${topic}\n\n${content}`;
+      comments.forEach(comment => {
+        if (comment.content.raw.startsWith(`### ${topic}\n\n`)) {
+          commentId = comment.id;
+          commentNeedsUpdating = comment.content.raw !== body;
+        }
+      });
+    } else {
+      logger.debug(`Ensuring content-only comment in #${prNo}`);
+      body = `${content}`;
+      comments.forEach(comment => {
+        if (comment.content.raw === body) {
+          commentId = comment.id;
+          commentNeedsUpdating = false;
+        }
+      });
+    }
+    if (!commentId) {
+      await addComment(config, prNo, body);
+      logger.info({ repository: config.repository, prNo }, 'Comment added');
+    } else if (commentNeedsUpdating) {
+      await editComment(config, prNo, commentId, body);
+      logger.info({ repository: config.repository, prNo }, 'Comment updated');
+    } else {
+      logger.debug('Comment is already update-to-date');
+    }
+    return true;
+  } catch (err) /* istanbul ignore next */ {
+    logger.warn({ err }, 'Error ensuring comment');
+    return false;
+  }
+}
+
+export async function ensureCommentRemoval(
+  config: CommentsConfig,
+  prNo: number,
+  topic: string
+) {
+  try {
+    logger.debug(`Ensuring comment "${topic}" in #${prNo} is removed`);
+    const comments = await getComments(config, prNo);
+    let commentId;
+    comments.forEach(comment => {
+      if (comment.content.raw.startsWith(`### ${topic}\n\n`)) {
+        commentId = comment.id;
+      }
+    });
+    if (commentId) {
+      await deleteComment(config, prNo, commentId);
+    }
+  } catch (err) /* istanbul ignore next */ {
+    logger.warn({ err }, 'Error ensuring comment removal');
+  }
+}
diff --git a/lib/platform/bitbucket/index.ts b/lib/platform/bitbucket/index.ts
index 4f3a8f818929b6fb7c75442116fbe4fa6a68c7a0..776b7aa81ea8f32e644bbf92c14e946d0a199dc4 100644
--- a/lib/platform/bitbucket/index.ts
+++ b/lib/platform/bitbucket/index.ts
@@ -6,20 +6,9 @@ import { logger } from '../../logger';
 import GitStorage from '../git/storage';
 import { readOnlyIssueBody } from '../utils/read-only-issue-body';
 import { appSlug } from '../../config/app-strings';
+import * as comments from './comments';
 
-interface Config {
-  baseBranch: string;
-  baseCommitSHA: string;
-  defaultBranch: string;
-  fileList: any[];
-  mergeMethod: string;
-  owner: string;
-  prList: any[];
-  repository: string;
-  storage: GitStorage;
-}
-
-let config: Config = {} as any;
+let config: utils.Config = {} as any;
 
 export function initPlatform({
   endpoint,
@@ -75,9 +64,12 @@ export async function initRepo({
     hostType: 'bitbucket',
     url: 'https://api.bitbucket.org/',
   });
-  config = {} as any;
+  config = {
+    repository,
+    username: opts!.username,
+  } as any;
+
   // TODO: get in touch with @rarkins about lifting up the caching into the app layer
-  config.repository = repository;
   const platformConfig: any = {};
 
   const url = GitStorage.getUrl({
@@ -101,11 +93,16 @@ export async function initRepo({
     platformConfig.privateRepo = info.privateRepo;
     platformConfig.isFork = info.isFork;
     platformConfig.repoFullName = info.repoFullName;
-    config.owner = info.owner;
+
+    Object.assign(config, {
+      owner: info.owner,
+      defaultBranch: info.mainbranch,
+      baseBranch: info.mainbranch,
+      mergeMethod: info.mergeMethod,
+      has_issues: info.has_issues,
+    });
+
     logger.debug(`${repository} owner = ${config.owner}`);
-    config.defaultBranch = info.mainbranch;
-    config.baseBranch = config.defaultBranch;
-    config.mergeMethod = info.mergeMethod;
   } catch (err) /* istanbul ignore next */ {
     if (err.statusCode === 404) {
       throw new Error('not-found');
@@ -296,12 +293,11 @@ export async function setBranchStatus(
 
 async function findOpenIssues(title: string) {
   try {
-    const currentUser = (await api.get('/2.0/user')).body.username;
     const filter = encodeURIComponent(
       [
         `title=${JSON.stringify(title)}`,
         '(state = "new" OR state = "open")',
-        `reporter.username="${currentUser}"`,
+        `reporter.username="${config.username}"`,
       ].join(' AND ')
     );
     return (
@@ -310,13 +306,19 @@ async function findOpenIssues(title: string) {
       )).body.values || /* istanbul ignore next */ []
     );
   } catch (err) /* istanbul ignore next */ {
-    logger.warn('Error finding issues');
+    logger.warn({ err }, 'Error finding issues');
     return [];
   }
 }
 
 export async function findIssue(title: string) {
   logger.debug(`findIssue(${title})`);
+
+  /* istanbul ignore if */
+  if (!config.has_issues) {
+    logger.warn('Issues are disabled');
+    return null;
+  }
   const issues = await findOpenIssues(title);
   if (!issues.length) {
     return null;
@@ -339,6 +341,12 @@ async function closeIssue(issueNumber: number) {
 
 export async function ensureIssue(title: string, body: string) {
   logger.debug(`ensureIssue()`);
+
+  /* istanbul ignore if */
+  if (!config.has_issues) {
+    logger.warn('Issues are disabled');
+    return null;
+  }
   try {
     const issues = await findOpenIssues(title);
     if (issues.length) {
@@ -381,13 +389,38 @@ export async function ensureIssue(title: string, body: string) {
   return null;
 }
 
-export /* istanbul ignore next */ function getIssueList() {
+export /* istanbul ignore next */ async function getIssueList() {
   logger.debug(`getIssueList()`);
-  // TODO: Needs implementation
-  return [];
+
+  /* istanbul ignore if */
+  if (!config.has_issues) {
+    logger.warn('Issues are disabled');
+    return [];
+  }
+  try {
+    const filter = encodeURIComponent(
+      [
+        '(state = "new" OR state = "open")',
+        `reporter.username="${config.username}"`,
+      ].join(' AND ')
+    );
+    return (
+      (await api.get(
+        `/2.0/repositories/${config.repository}/issues?q=${filter}`
+      )).body.values || /* istanbul ignore next */ []
+    );
+  } catch (err) /* istanbul ignore next */ {
+    logger.warn({ err }, 'Error finding issues');
+    return [];
+  }
 }
 
 export async function ensureIssueClosing(title: string) {
+  /* istanbul ignore if */
+  if (!config.has_issues) {
+    logger.warn('Issues are disabled');
+    return;
+  }
   const issues = await findOpenIssues(title);
   for (const issue of issues) {
     await closeIssue(issue.id);
@@ -420,22 +453,18 @@ export /* istanbul ignore next */ function deleteLabel() {
   throw new Error('deleteLabel not implemented');
 }
 
-/* eslint-disable @typescript-eslint/no-unused-vars */
 export function ensureComment(
-  _prNo: number,
-  _topic: string | null,
-  _content: string
+  prNo: number,
+  topic: string | null,
+  content: string
 ) {
   // https://developer.atlassian.com/bitbucket/api/2/reference/search?q=pullrequest+comment
-  logger.warn('Comment functionality not implemented yet');
-  return Promise.resolve();
+  return comments.ensureComment(config, prNo, topic, content);
 }
 
-export function ensureCommentRemoval(_prNo: number, _topic: string) {
-  // The api does not support removing comments
-  return Promise.resolve();
+export function ensureCommentRemoval(prNo: number, topic: string) {
+  return comments.ensureCommentRemoval(config, prNo, topic);
 }
-/* eslint-enable @typescript-eslint/no-unused-vars */
 
 // istanbul ignore next
 function matchesState(state: string, desiredState: string) {
diff --git a/lib/platform/bitbucket/utils.ts b/lib/platform/bitbucket/utils.ts
index d44bf2b1dd508ece22d218749c53dbc1b18cd85b..b753a2f45ec3a98f3090cc1121740713669c242e 100644
--- a/lib/platform/bitbucket/utils.ts
+++ b/lib/platform/bitbucket/utils.ts
@@ -1,5 +1,21 @@
 import url from 'url';
 import { api } from './bb-got-wrapper';
+import { Storage } from '../git/storage';
+
+export interface Config {
+  baseBranch: string;
+  baseCommitSHA: string;
+  defaultBranch: string;
+  fileList: any[];
+  has_issues: boolean;
+  mergeMethod: string;
+  owner: string;
+  prList: any[];
+  repository: string;
+  storage: Storage;
+
+  username: string;
+}
 
 export function repoInfoTransformer(repoInfoBody: any) {
   return {
@@ -9,6 +25,7 @@ export function repoInfoTransformer(repoInfoBody: any) {
     owner: repoInfoBody.owner.username,
     mainbranch: repoInfoBody.mainbranch.name,
     mergeMethod: 'merge',
+    has_issues: repoInfoBody.has_issues,
   };
 }
 
@@ -40,13 +57,13 @@ const addMaxLength = (inputUrl: string, pagelen = 100) => {
   return maxedUrl;
 };
 
-export async function accumulateValues(
+export async function accumulateValues<T = any>(
   reqUrl: string,
   method = 'get',
   options?: any,
   pagelen?: number
 ) {
-  let accumulator: any[] = [];
+  let accumulator: T[] = [];
   let nextUrl = addMaxLength(reqUrl, pagelen);
   const lowerCaseMethod = method.toLocaleLowerCase();
 
diff --git a/test/platform/bitbucket/__snapshots__/comments.spec.ts.snap b/test/platform/bitbucket/__snapshots__/comments.spec.ts.snap
new file mode 100644
index 0000000000000000000000000000000000000000..0e7a5aeef0302416b4175f4a5c9e131d5fe21a00
--- /dev/null
+++ b/test/platform/bitbucket/__snapshots__/comments.spec.ts.snap
@@ -0,0 +1,91 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`platform/comments ensureComment() add comment if not found 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() add comment if not found 2`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() add updates comment if necessary 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() add updates comment if necessary 2`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() does not throw 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/3/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() skips comment 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureComment() skips comment 2`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureCommentRemoval() deletes comment if found 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureCommentRemoval() deletes nothing 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
+
+exports[`platform/comments ensureCommentRemoval() does not throw 1`] = `
+Array [
+  Array [
+    "/2.0/repositories/some/repo/pullrequests/5/comments?pagelen=100",
+    undefined,
+  ],
+]
+`;
diff --git a/test/platform/bitbucket/__snapshots__/index.spec.ts.snap b/test/platform/bitbucket/__snapshots__/index.spec.ts.snap
index ced5c899ec9d1c995ff552111cd3c62c8415cf63..75470c880fc44f8340d3fe9b0c3133f19c5f7fab 100644
--- a/test/platform/bitbucket/__snapshots__/index.spec.ts.snap
+++ b/test/platform/bitbucket/__snapshots__/index.spec.ts.snap
@@ -66,10 +66,7 @@ Array [
     undefined,
   ],
   Array [
-    "/2.0/user",
-  ],
-  Array [
-    "/2.0/repositories/some/empty/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22nobody%22",
+    "/2.0/repositories/some/empty/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22abc%22",
   ],
 ]
 `;
@@ -94,10 +91,7 @@ Array [
 exports[`platform/bitbucket ensureIssue() noop for existing issue 1`] = `
 Array [
   Array [
-    "/2.0/user",
-  ],
-  Array [
-    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22nobody%22",
+    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22abc%22",
   ],
 ]
 `;
@@ -105,10 +99,7 @@ Array [
 exports[`platform/bitbucket ensureIssue() updates existing issues 1`] = `
 Array [
   Array [
-    "/2.0/user",
-  ],
-  Array [
-    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22nobody%22",
+    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22abc%22",
   ],
 ]
 `;
@@ -118,10 +109,7 @@ exports[`platform/bitbucket ensureIssue() updates existing issues 2`] = `Array [
 exports[`platform/bitbucket ensureIssueClosing() does not throw 1`] = `
 Array [
   Array [
-    "/2.0/user",
-  ],
-  Array [
-    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22nobody%22",
+    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22abc%22",
   ],
 ]
 `;
@@ -138,10 +126,7 @@ Object {
 exports[`platform/bitbucket findIssue() does not throw 2`] = `
 Array [
   Array [
-    "/2.0/user",
-  ],
-  Array [
-    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22nobody%22",
+    "/2.0/repositories/some/repo/issues?q=title%3D%22title%22%20AND%20(state%20%3D%20%22new%22%20OR%20state%20%3D%20%22open%22)%20AND%20reporter.username%3D%22abc%22",
   ],
 ]
 `;
diff --git a/test/platform/bitbucket/_fixtures/responses.js b/test/platform/bitbucket/_fixtures/responses.js
index 28ab68654db0f0252a29c262fad0eb010578f55d..88142ba1f4715764848122acace1c09c8ee0b51f 100644
--- a/test/platform/bitbucket/_fixtures/responses.js
+++ b/test/platform/bitbucket/_fixtures/responses.js
@@ -21,6 +21,7 @@ const issue = {
 const repo = {
   is_private: false,
   full_name: 'some/repo',
+  has_issues: true,
   owner: { username: 'some' },
   mainbranch: { name: 'master' },
 };
@@ -66,6 +67,14 @@ module.exports = {
   '/2.0/repositories/some/repo/pullrequests/5/commits': {
     values: [{}],
   },
+  '/2.0/repositories/some/repo/pullrequests/5/comments': {
+    values: [
+      { id: 21, content: { raw: '### some-subject\n\nblablabla' } },
+      { id: 22, content: { raw: '!merge' } }
+    ],
+  },
+  '/2.0/repositories/some/repo/pullrequests/5/comments/21': {},
+  '/2.0/repositories/some/repo/pullrequests/5/comments/22': {},
   '/2.0/repositories/some/repo/refs/branches': {
     values: [
       { name: 'master' },
diff --git a/test/platform/bitbucket/comments.spec.ts b/test/platform/bitbucket/comments.spec.ts
new file mode 100644
index 0000000000000000000000000000000000000000..05c47a81940e29e233ea7081489789e6393dcce7
--- /dev/null
+++ b/test/platform/bitbucket/comments.spec.ts
@@ -0,0 +1,133 @@
+import URL from 'url';
+import { api as _api } from '../../../lib/platform/bitbucket/bb-got-wrapper';
+import * as comments from '../../../lib/platform/bitbucket/comments';
+import responses from './_fixtures/responses';
+
+jest.mock('../../../lib/platform/bitbucket/bb-got-wrapper');
+
+const api: jest.Mocked<typeof _api> = _api as any;
+
+describe('platform/comments', () => {
+  const config: comments.CommentsConfig = { repository: 'some/repo' };
+
+  async function mockedGet(path: string) {
+    const uri = URL.parse(path).pathname!;
+    let body = (responses as any)[uri];
+    if (!body) {
+      throw new Error('Missing request');
+    }
+    if (typeof body === 'function') {
+      body = await body();
+    }
+    return { body } as any;
+  }
+
+  beforeAll(() => {
+    api.get.mockImplementation(mockedGet);
+    api.post.mockImplementation(mockedGet);
+    api.put.mockImplementation(mockedGet);
+    api.delete.mockImplementation(mockedGet);
+  });
+
+  beforeEach(() => {
+    jest.clearAllMocks();
+  });
+
+  describe('ensureComment()', () => {
+    it('does not throw', async () => {
+      expect.assertions(2);
+      expect(await comments.ensureComment(config, 3, 'topic', 'content')).toBe(
+        false
+      );
+      expect(api.get.mock.calls).toMatchSnapshot();
+    });
+
+    it('add comment if not found', async () => {
+      expect.assertions(6);
+      api.get.mockClear();
+
+      expect(await comments.ensureComment(config, 5, 'topic', 'content')).toBe(
+        true
+      );
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.post).toHaveBeenCalledTimes(1);
+
+      api.get.mockClear();
+      api.post.mockClear();
+
+      expect(await comments.ensureComment(config, 5, null, 'content')).toBe(
+        true
+      );
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.post).toHaveBeenCalledTimes(1);
+    });
+
+    it('add updates comment if necessary', async () => {
+      expect.assertions(8);
+      api.get.mockClear();
+
+      expect(
+        await comments.ensureComment(config, 5, 'some-subject', 'some\ncontent')
+      ).toBe(true);
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.post).toHaveBeenCalledTimes(0);
+      expect(api.put).toHaveBeenCalledTimes(1);
+
+      api.get.mockClear();
+      api.put.mockClear();
+
+      expect(
+        await comments.ensureComment(config, 5, null, 'some\ncontent')
+      ).toBe(true);
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.post).toHaveBeenCalledTimes(1);
+      expect(api.put).toHaveBeenCalledTimes(0);
+    });
+
+    it('skips comment', async () => {
+      expect.assertions(6);
+      api.get.mockClear();
+
+      expect(
+        await comments.ensureComment(config, 5, 'some-subject', 'blablabla')
+      ).toBe(true);
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.put).toHaveBeenCalledTimes(0);
+
+      api.get.mockClear();
+      api.put.mockClear();
+
+      expect(await comments.ensureComment(config, 5, null, '!merge')).toBe(
+        true
+      );
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.put).toHaveBeenCalledTimes(0);
+    });
+  });
+
+  describe('ensureCommentRemoval()', () => {
+    it('does not throw', async () => {
+      expect.assertions(1);
+      await comments.ensureCommentRemoval(config, 5, 'topic');
+      expect(api.get.mock.calls).toMatchSnapshot();
+    });
+
+    it('deletes comment if found', async () => {
+      expect.assertions(2);
+      api.get.mockClear();
+
+      await comments.ensureCommentRemoval(config, 5, 'some-subject');
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.delete).toHaveBeenCalledTimes(1);
+    });
+
+    it('deletes nothing', async () => {
+      expect.assertions(2);
+      api.get.mockClear();
+
+      await comments.ensureCommentRemoval(config, 5, 'topic');
+      expect(api.get.mock.calls).toMatchSnapshot();
+      expect(api.delete).toHaveBeenCalledTimes(0);
+    });
+  });
+});