diff --git a/.gitignore b/.gitignore
index aae64a7..0c9be46 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# Application specific
+CHANGELOG_generated.md
+
 # Byte-compiled / optimized / DLL files
 __pycache__/
 *.py[cod]
@@ -24,6 +27,8 @@ wheels/
 .installed.cfg
 *.egg
 MANIFEST
+bin/
+include/
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
diff --git a/.travis.yml b/.travis.yml
index d6aa6ab..881e436 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,13 +1,14 @@
 language: python
-python: 
+python:
   - "3.6"
   - "3.7-dev"
 
 install:
-- pip install requests
-- pip install pyre-check
-- pip install pytest-cov
+  - pip install python-dateutil
+  - pip install requests
+  - pip install pyre-check
+  - pip install pytest-cov
+  - pip install pytz
 
 script:
-- python3 -m pytest --cov-report term-missing --cov=changelog_generator
-- pyre --source-directory . check
\ No newline at end of file
+  - python3 -m pytest --cov-report term-missing --cov=changelog_generator
diff --git a/changelog_generator/calls.py b/changelog_generator/calls.py
index 18cef91..c7d57cd 100644
--- a/changelog_generator/calls.py
+++ b/changelog_generator/calls.py
@@ -44,6 +44,64 @@ def get_last_commit_date(cli_args: dict) -> str:
     )
 
 
+def get_closed_issues_for_project(cli_args: dict) -> dict:
+    """
+    Queries a specified GitLab API and returns a list containing
+    the titles and URLs of closed issues since a given date.
+    """
+    request_url = f"http://{cli_args['ip_address']}/api/v{cli_args['api_version']}/projects/{cli_args['project_group']}%2F{cli_args['project']}/issues?state=closed"
+    logger.info(
+        f"Requesting tags for project {cli_args['project']} with URL: {request_url}"
+    )
+    try:
+        response = requests.get(request_url)
+        response.raise_for_status()
+    except requests.exceptions.HTTPError as ex:
+        logger.error(
+            f"{get_commits_since_date.__name__} call to GitLab API failed with HTTPError: {ex}"
+        )
+        sys.exit(1)
+    except requests.exceptions.ConnectionError as ex:
+        logger.error(
+            f"{get_commits_since_date.__name__} call to GitLab API failed with ConnectionError: {ex}"
+        )
+        sys.exit(1)
+
+    logger.debug(response.status_code)
+    logger.debug(response.json())
+
+    return response.json()
+
+
+def get_last_tagged_release_date(cli_args: dict) -> str:
+    """
+    Queries a specified GitLab API and returns a string containing
+    the created_at date of the last tagged release.
+    """
+    request_url = f"http://{cli_args['ip_address']}/api/v{cli_args['api_version']}/projects/{cli_args['project_group']}%2F{cli_args['project']}/repository/tags"
+    logger.info(
+        f"Requesting tags for project {cli_args['project']} with URL: {request_url}"
+    )
+    try:
+        response = requests.get(request_url)
+        response.raise_for_status()
+    except requests.exceptions.HTTPError as ex:
+        logger.error(
+            f"{get_commits_since_date.__name__} call to GitLab API failed with HTTPError: {ex}"
+        )
+        sys.exit(1)
+    except requests.exceptions.ConnectionError as ex:
+        logger.error(
+            f"{get_commits_since_date.__name__} call to GitLab API failed with ConnectionError: {ex}"
+        )
+        sys.exit(1)
+
+    logger.debug(response.status_code)
+    logger.debug(response.json())
+
+    return response.json()[0]["commit"]["created_at"]
+
+
 def get_commits_since_date(date: str, cli_args: dict) -> list:
     """
     Queries a specified GitLab API and returns a JSON response containing
diff --git a/changelog_generator/generator.py b/changelog_generator/generator.py
index 24b94c0..cea4db2 100644
--- a/changelog_generator/generator.py
+++ b/changelog_generator/generator.py
@@ -1,10 +1,13 @@
 import datetime
+import dateutil.parser
 import os.path
 
 from changelog_generator.log_handlers import logger
 from changelog_generator.calls import (
-    get_last_commit_date,
+    get_closed_issues_for_project,
     get_commits_since_date,
+    get_last_commit_date,
+    get_last_tagged_release_date,
 )
 
 
@@ -12,6 +15,8 @@ def generate_changelog(cli_args: dict) -> str:
     # Get the date of the last commit
     last_commit = get_last_commit_date(cli_args)
 
+    closed_issues_since_last_tag = get_closed_issues_since_last_tag(cli_args)
+
     # Get any commits since that date
     new_commits = get_commits_since_date(last_commit, cli_args)
 
@@ -35,6 +40,15 @@ def generate_changelog(cli_args: dict) -> str:
                         for commit in new_commits
                         for y in commit["message"].split("\n")
                     ]
+
+                    if closed_issues_since_last_tag:
+                        modified_changelog.write(f"\n### Closed Issues\n")
+                        [
+                            modified_changelog.write(
+                                f"* {closed_issue['title']}"
+                            )
+                            for closed_issue in closed_issues_since_last_tag
+                        ]
                     modified_changelog.write(f"\n")
                     modified_changelog.write(original_changelog_data)
                     return "CHANGELOG.md updated successfully"
@@ -50,6 +64,13 @@ def generate_changelog(cli_args: dict) -> str:
                     for commit in new_commits
                     for y in commit["message"].split("\n")
                 ]
+
+                if closed_issues_since_last_tag:
+                    changelog.write(f"\n### Closed Issues\n")
+                    [
+                        changelog.write(f"* {closed_issue['title']}")
+                        for closed_issue in closed_issues_since_last_tag
+                    ]
             return "CHANGELOG_generated.md written successfully"
     else:
         logger.info("No CHANGELOG.md found and no CHANGELOG.md specified...")
@@ -61,5 +82,28 @@ def generate_changelog(cli_args: dict) -> str:
                 for commit in new_commits
                 for y in commit["message"].split("\n")
             ]
-
+            if closed_issues_since_last_tag:
+                changelog.write(f"\n### Closed Issues\n")
+                [
+                    changelog.write(f"* {closed_issue['title']}")
+                    for closed_issue in closed_issues_since_last_tag
+                ]
         return "New CHANGELOG.md file written successfully"
+
+
+def get_closed_issues_since_last_tag(cli_args: dict) -> list:
+    last_tagged_release_date = get_last_tagged_release_date(cli_args)
+
+    closed_issues = get_closed_issues_for_project(cli_args)
+
+    closed_issues_since_tag = []
+    for issue in closed_issues:
+        logger.info(issue)
+        if dateutil.parser.parse(issue["closed_at"]) > dateutil.parser.parse(
+            last_tagged_release_date
+        ):
+            closed_issues_since_tag.append(
+                {"closed_at": issue["closed_at"], "title": issue["title"]}
+            )
+
+    return closed_issues_since_tag

Implement 'fixed bugs' section in CHANGELOG.md
The [GitLab API](https://docs.gitlab.com/ee/api/) can be queried to find closed issues since the last tagged version of a repository. We can query this at a more granular level to determine closed issues which had a `bug` label applied to them. This feature can be used to add a new section to the `CHANGELOG.md` file which is generated by this utility, something along the lines of 'Fixed Bugs' which details those issues labelled as bugs which have been closed.
diff --git a/changelog_generator/tests/test_calls.py b/changelog_generator/tests/test_calls.py
index caf19f4..0024e2f 100644
--- a/changelog_generator/tests/test_calls.py
+++ b/changelog_generator/tests/test_calls.py
@@ -4,6 +4,8 @@ import unittest
 
 from changelog_generator.calls import (
     get_last_commit_date,
+    get_last_tagged_release_date,
+    get_closed_issues_for_project,
     get_commits_since_date,
 )
 
@@ -116,3 +118,56 @@ class TestCalls(unittest.TestCase):
             )
             for commit in commits
         ]
+
+    @mock.patch("changelog_generator.calls.requests.get")
+    def test_get_closed_issues_for_project(self, mock_get):
+
+        mock_get.return_value.json.return_value = [
+            {
+                "closed_at": "2018-06-10T14:01:44.000+00:00",
+                "title": "A Closed Issue",
+            }
+        ]
+
+        cli_args = {
+            "ip_address": "localhost",
+            "api_version": "4",
+            "project_group": "test-group",
+            "project": "test-project",
+            "branch_one": "release",
+            "branch_two": "master",
+            "version": "1",
+            "changelog": "N",
+        }
+
+        self.assertEqual(
+            mock_get.return_value.json.return_value,
+            get_closed_issues_for_project(cli_args),
+        )
+
+    @mock.patch("changelog_generator.calls.requests.get")
+    def test_get_last_tagged_release_date(self, mock_get):
+        mock_get.return_value.json.return_value = [
+            {
+                "commit": {
+                    "created_at": "2018-06-10T14:01:44.000+00:00",
+                    "name": "A Closed Issue",
+                }
+            }
+        ]
+
+        cli_args = {
+            "ip_address": "localhost",
+            "api_version": "4",
+            "project_group": "test-group",
+            "project": "test-project",
+            "branch_one": "release",
+            "branch_two": "master",
+            "version": "1",
+            "changelog": "N",
+        }
+
+        self.assertEqual(
+            "2018-06-10T14:01:44.000+00:00",
+            get_last_tagged_release_date(cli_args),
+        )
diff --git a/changelog_generator/tests/test_generator.py b/changelog_generator/tests/test_generator.py
index 7fd8668..c52c15d 100644
--- a/changelog_generator/tests/test_generator.py
+++ b/changelog_generator/tests/test_generator.py
@@ -2,15 +2,25 @@ import datetime
 import mock
 import unittest
 
-from changelog_generator.generator import generate_changelog
+from changelog_generator.generator import (
+    generate_changelog,
+    get_closed_issues_since_last_tag,
+)
 
 
 class TestGenerator(unittest.TestCase):
+    @mock.patch(
+        "changelog_generator.generator.get_closed_issues_since_last_tag"
+    )
     @mock.patch("os.path.isfile")
     @mock.patch("changelog_generator.generator.get_commits_since_date")
     @mock.patch("changelog_generator.generator.get_last_commit_date")
     def test_generate_changelog_existing(
-        self, mock_get_commit_date, mock_get_commits, mock_is_file
+        self,
+        mock_get_commit_date,
+        mock_get_commits,
+        mock_is_file,
+        mock_closed_issues,
     ):
         mock_is_file.return_value = True
         mock_get_commit_date.return_value = "2018-06-10T14:01:45.000000+00:00"
@@ -21,6 +31,12 @@ class TestGenerator(unittest.TestCase):
                 "message": "Test commit message",
             }
         ]
+        mock_closed_issues.return_value = [
+            {
+                "title": "A Closed Issue",
+                "closed_at": "2018-06-10T14:01:44.000+00:00",
+            }
+        ]
 
         cli_args = {
             "ip_address": "localhost",
@@ -36,13 +52,22 @@ class TestGenerator(unittest.TestCase):
         with mock.patch("builtins.open", mock.mock_open()) as mock_file:
             result = generate_changelog(cli_args)
             mock_file.assert_called_once_with(mock.ANY, "w")
-            mock_file().write.assert_called_with(
-                "* 2018-06-10 - Test commit message \n"
-            )
+            mock_file_write_calls = [
+                mock.call(
+                    f"## v1 ({datetime.datetime.now().strftime('%Y-%m-%d')})\n"
+                ),
+                mock.call("* 2018-06-10 - Test commit message \n"),
+                mock.call("\n### Closed Issues\n"),
+                mock.call("* A Closed Issue"),
+            ]
+            mock_file().write.assert_has_calls(mock_file_write_calls)
             self.assertEqual(
                 result, "CHANGELOG_generated.md written successfully"
             )
 
+    @mock.patch(
+        "changelog_generator.generator.get_closed_issues_since_last_tag"
+    )
     @mock.patch("changelog_generator.generator.datetime")
     @mock.patch("os.path.isfile")
     @mock.patch("changelog_generator.generator.get_commits_since_date")
@@ -53,6 +78,7 @@ class TestGenerator(unittest.TestCase):
         mock_get_commits,
         mock_is_file,
         mock_datetime,
+        mock_closed_issues,
     ):
         mock_datetime.datetime.now.return_value = datetime.date(2018, 6, 18)
         mock_is_file.return_value = True
@@ -64,6 +90,12 @@ class TestGenerator(unittest.TestCase):
                 "message": "Test commit message",
             }
         ]
+        mock_closed_issues.return_value = [
+            {
+                "title": "A Closed Issue",
+                "closed_at": "2018-06-10T14:01:44.000+00:00",
+            }
+        ]
 
         cli_args = {
             "ip_address": "localhost",
@@ -85,17 +117,26 @@ class TestGenerator(unittest.TestCase):
                 [
                     mock.call("## v1 (2018-06-18)\n"),
                     mock.call("* 2018-06-10 - Test commit message \n"),
+                    mock.call("\n### Closed Issues\n"),
+                    mock.call("* A Closed Issue"),
                     mock.call("\n"),
                     mock.call("Existing data"),
                 ]
             )
             self.assertEqual(result, "CHANGELOG.md updated successfully")
 
+    @mock.patch(
+        "changelog_generator.generator.get_closed_issues_since_last_tag"
+    )
     @mock.patch("os.path.isfile")
     @mock.patch("changelog_generator.generator.get_commits_since_date")
     @mock.patch("changelog_generator.generator.get_last_commit_date")
     def test_generate_changelog_new(
-        self, mock_get_commit_date, mock_get_commits, mock_is_file
+        self,
+        mock_get_commit_date,
+        mock_get_commits,
+        mock_is_file,
+        mock_closed_issues,
     ):
         mock_is_file.return_value = False
         mock_get_commit_date.return_value = "2018-06-10T14:01:45.000000+00:00"
@@ -106,6 +147,12 @@ class TestGenerator(unittest.TestCase):
                 "message": "Test commit message",
             }
         ]
+        mock_closed_issues.return_value = [
+            {
+                "title": "A Closed Issue",
+                "closed_at": "2018-06-10T14:01:44.000+00:00",
+            }
+        ]
 
         cli_args = {
             "ip_address": "localhost",
@@ -121,9 +168,77 @@ class TestGenerator(unittest.TestCase):
         with mock.patch("builtins.open", mock.mock_open()) as mock_file:
             result = generate_changelog(cli_args)
             mock_file.assert_called_once_with(mock.ANY, "w")
-            mock_file().write.assert_called_with(
-                "* 2018-06-10 - Test commit message \n"
-            )
+            mock_file_write_calls = [
+                mock.call(
+                    f"## v1 ({datetime.datetime.now().strftime('%Y-%m-%d')})\n"
+                ),
+                mock.call("* 2018-06-10 - Test commit message \n"),
+                mock.call("\n### Closed Issues\n"),
+                mock.call("* A Closed Issue"),
+            ]
+            mock_file().write.assert_has_calls(mock_file_write_calls)
             self.assertEqual(
                 result, "New CHANGELOG.md file written successfully"
             )
+
+    @mock.patch("changelog_generator.generator.get_closed_issues_for_project")
+    @mock.patch("changelog_generator.generator.get_last_tagged_release_date")
+    def test_get_closed_issues_since_last_tag(
+        self, mock_release_date, mock_closed_project_issues
+    ):
+        mock_release_date.return_value = "2018-06-10T14:01:44.000+00:00"
+        mock_closed_project_issues.return_value = [
+            {
+                "closed_at": "2050-06-10T14:01:44.000+00:00",
+                "title": "A Closed Issue",
+                "assignee": 1,
+            }
+        ]
+
+        cli_args = {
+            "ip_address": "localhost",
+            "api_version": "4",
+            "project_group": "test-group",
+            "project": "test-project",
+            "branch_one": "release",
+            "branch_two": "master",
+            "version": "1",
+            "changelog": "N",
+        }
+
+        self.assertEqual(
+            get_closed_issues_since_last_tag(cli_args),
+            [
+                {
+                    "closed_at": "2050-06-10T14:01:44.000+00:00",
+                    "title": "A Closed Issue",
+                }
+            ],
+        )
+
+    @mock.patch("changelog_generator.generator.get_closed_issues_for_project")
+    @mock.patch("changelog_generator.generator.get_last_tagged_release_date")
+    def test_get_closed_issues_since_last_tag_no_issues(
+        self, mock_release_date, mock_closed_project_issues
+    ):
+        mock_release_date.return_value = "2018-06-10T14:01:44.000+00:00"
+        mock_closed_project_issues.return_value = [
+            {
+                "closed_at": "1990-06-10T14:01:44.000+00:00",
+                "title": "A Closed Issue",
+                "assignee": 1,
+            }
+        ]
+
+        cli_args = {
+            "ip_address": "localhost",
+            "api_version": "4",
+            "project_group": "test-group",
+            "project": "test-project",
+            "branch_one": "release",
+            "branch_two": "master",
+            "version": "1",
+            "changelog": "N",
+        }
+
+        self.assertEqual(get_closed_issues_since_last_tag(cli_args), [])

pip install -e .[dev]
parse_log_pytest
pytest
pytest mock python-dateutil
apt-get update apt-get install -y gcc
3.7
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
attrs @ file:///croot/attrs_1668696182826/work
certifi @ file:///croot/certifi_1671487769961/work/certifi
charset-normalizer==3.4.1
flit_core @ file:///opt/conda/conda-bld/flit-core_1644941570762/work/source/flit_core
-e git+https://github.com/stuartmccoll/gitlab-changelog-generator.git@7e6b2ef2d74c19b89f88eda28c4b1b912d2aab1c#egg=gitlab_changelog_generator
idna==3.10
importlib-metadata @ file:///tmp/build/80754af9/importlib-metadata_1648562407465/work
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
mock==5.2.0
packaging @ file:///croot/packaging_1671697413597/work
pluggy @ file:///tmp/build/80754af9/pluggy_1648042572264/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pytest==7.1.2
python-dateutil==2.9.0.post0
requests==2.31.0
six==1.17.0
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing_extensions @ file:///croot/typing_extensions_1669924550328/work
urllib3==2.0.7
zipp @ file:///croot/zipp_1672387121353/work

name: gitlab-changelog-generator
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=22.1.0=py37h06a4308_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2022.12.7=py37h06a4308_0
  - flit-core=3.6.0=pyhd3eb1b0_0
  - importlib-metadata=4.11.3=py37h06a4308_0
  - importlib_metadata=4.11.3=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=22.0=py37h06a4308_0
  - pip=22.3.1=py37h06a4308_0
  - pluggy=1.0.0=py37h06a4308_1
  - py=1.11.0=pyhd3eb1b0_0
  - pytest=7.1.2=py37h06a4308_0
  - python=3.7.16=h7a1cb2a_0
  - readline=8.2=h5eee18b_0
  - setuptools=65.6.3=py37h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tomli=2.0.1=py37h06a4308_0
  - typing_extensions=4.4.0=py37h06a4308_0
  - wheel=0.38.4=py37h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.11.0=py37h06a4308_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - charset-normalizer==3.4.1
      - idna==3.10
      - mock==5.2.0
      - python-dateutil==2.9.0.post0
      - requests==2.31.0
      - six==1.17.0
      - urllib3==2.0.7
prefix: /opt/conda/envs/gitlab-changelog-generator

changelog_generator/tests/test_calls.py::TestCalls::test_commits_since_date changelog_generator/tests/test_calls.py::TestCalls::test_get_closed_issues_for_project changelog_generator/tests/test_calls.py::TestCalls::test_get_last_commit_date changelog_generator/tests/test_calls.py::TestCalls::test_get_last_tagged_release_date changelog_generator/tests/test_calls.py::TestCalls::test_unsuccessful_commits_since_date changelog_generator/tests/test_calls.py::TestCalls::test_unsuccessful_get_last_commit_date changelog_generator/tests/test_generator.py::TestGenerator::test_generate_changelog_new changelog_generator/tests/test_generator.py::TestGenerator::test_generate_changelog_update changelog_generator/tests/test_generator.py::TestGenerator::test_get_closed_issues_since_last_tag changelog_generator/tests/test_generator.py::TestGenerator::test_get_closed_issues_since_last_tag_no_issues
changelog_generator/tests/test_generator.py::TestGenerator::test_generate_changelog_existing
.gitignore .travis.yml changelog_generator/calls.py changelog_generator/generator.py
.gitignore .travis.yml changelog_generator/calls.py changelog_generator/generator.py