diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index b0d6430c3c2f4b979b9f82d1bd4be496a8f2f6c1..c38e2be9038804675ece168e10ed2a495312793d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,3 +1,28 @@
+code-analysis-update:
+  stage: build
+  image: iopsys/sdk-builder:latest
+  variables:
+    GIT_STRATEGY: none
+  script:
+    - git clone --depth=1 -b "$CI_COMMIT_BRANCH" "https://token:${CODE_ANALYSIS_TOKEN}@${CI_SERVER_HOST}/${CI_PROJECT_PATH}.git/" gitlab-ci-pipeline
+    - cd gitlab-ci-pipeline/docker/code-analysis
+    - ./bump_code_analysis_lib_versions.py Dockerfile
+    - |
+        if ! git diff-index --name-status --exit-code HEAD; then
+          git config user.name 'IOWRT code-analysis updater'
+          git config user.email 'noreply@iopsys.eu'
+          git add .
+          git commit -m 'Bump IOWRT library versions'
+          git push
+        fi
+  rules:
+    - if: '$CI_PIPELINE_SOURCE == "pipeline"'
+  interruptible: true
+  # Job retry is useful if somebody happened to push to the code-analysis repo while this job was running
+  retry:
+    max: 2
+    when: script_failure
+
 pages:
   stage: deploy
   image: alpine:latest
diff --git a/docker/code-analysis/bump_code_analysis_lib_versions.json b/docker/code-analysis/bump_code_analysis_lib_versions.json
new file mode 100644
index 0000000000000000000000000000000000000000..35c8cf10f9a76327afc7a971db5f5ec1b7da0f7a
--- /dev/null
+++ b/docker/code-analysis/bump_code_analysis_lib_versions.json
@@ -0,0 +1,6 @@
+{
+  "ubus": "https://dev.iopsys.eu/iopsys/iopsyswrt/-/raw/devel/package/system/ubus/Makefile",
+  "uci": "https://dev.iopsys.eu/iopsys/iopsyswrt/-/raw/devel/package/system/uci/Makefile",
+  "rpcd": "https://dev.iopsys.eu/iopsys/iopsyswrt/-/raw/devel/package/system/rpcd/Makefile",
+  "libubox": "https://dev.iopsys.eu/iopsys/iopsyswrt/-/raw/devel/package/libs/libubox/Makefile"
+}
diff --git a/docker/code-analysis/bump_code_analysis_lib_versions.py b/docker/code-analysis/bump_code_analysis_lib_versions.py
new file mode 100755
index 0000000000000000000000000000000000000000..f4f6a82246695156fa7fd8451c687e25ada64e31
--- /dev/null
+++ b/docker/code-analysis/bump_code_analysis_lib_versions.py
@@ -0,0 +1,54 @@
+#!/usr/bin/python3
+
+import argparse
+import json
+import logging
+import pathlib
+import re
+
+import requests
+
+PKG_SOURCE_VERSION = re.compile(r'PKG_SOURCE_VERSION:=(\S+)')
+
+
+def get_package_version(makefile_url: str) -> str:
+    resp = requests.get(makefile_url)
+    version = PKG_SOURCE_VERSION.search(resp.text).group(1)
+    return version
+
+
+if __name__ == '__main__':
+    logging.basicConfig(level=logging.INFO)
+
+    config_path = pathlib.Path(__file__).absolute().parent / 'bump_code_analysis_lib_versions.json'
+
+    with config_path.open('r') as f:
+        config = json.load(f)
+        logging.info(f'Configuration: {config}')
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument(
+        'dockerfile',
+        help='Dockerfile to update version arguments in',
+        metavar='DOCKERFILE',
+        type=argparse.FileType('r'),
+    )
+    args = parser.parse_args()
+
+    with args.dockerfile as f:
+        dockerfile = f.read()
+
+    dockerfile_orig = dockerfile
+
+    for package_name, makefile_url in config.items():
+        logging.info(f'Fetching Makefile for package {package_name}')
+        package_version = get_package_version(makefile_url)
+        logging.info(f'Package {package_name} has version {package_version} in Makefile')
+
+        docker_arg = f'{package_name.upper()}_VERSION'
+        dockerfile = re.sub(fr'(?<=ARG {docker_arg}=)\S+', package_version, dockerfile)
+
+    if dockerfile != dockerfile_orig:
+        logging.info('Dockerfile changed, writing to disk')
+        with open(args.dockerfile.name, 'w') as f:
+            f.write(dockerfile)