blob: 152db957d492dcdfbfc9698bd06399dbef86836c [file] [log] [blame]
#!/usr/bin/env python3
# Copyright (c) 2023 Project CHIP Authors
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# See the License for the specific language governing permissions and
# limitations under the License.
# This automates ZAP version changes as the zap version is repeated
# in many places
import logging
import os
import re
import sys
from enum import Flag, auto
import click
import coloredlogs
# Supported log levels, mapping string values required for argument
# parsing into logging constants
__LOG_LEVELS__ = {
'debug': logging.DEBUG,
'info': logging.INFO,
'warn': logging.WARN,
'fatal': logging.FATAL,
# A version is of the form: v2023.01.09-nightly
# At this time we hard-code nightly however we may need to figure out a more
# generic version string once we stop using nightly builds
ZAP_VERSION_RE = re.compile(r'v(\d\d\d\d)\.(\d\d)\.(\d\d)-nightly')
# A list of files where ZAP is maintained. You can get a similar list using:
# rg v2023.01.09-nightly --hidden --no-ignore --files-with-matches
# Excluding THIS file and excluding anything in .environment (logs)
# Set as a separate list to not pay the price of a full grep as the list of
# files is not likely to change often
# Note that chip-cert-bits is assumed USAGE on purpose (it compiles code)
# The chip-build image change will affect all other images as they extend
# chip-build
class UpdateChoice(Flag):
# Usage updates the CI, chip-cert and execution logic. Generally everything
# that would make use of the updated zap version
USAGE = auto()
# Docker updates just the chip-build (and as a side-effect underlying)
# image(s). This is a pre-requisite to be able to start using the new
# version.
DOCKER = auto()
'docker': UpdateChoice.DOCKER,
'usage': UpdateChoice.USAGE,
'all': UpdateChoice.DOCKER | UpdateChoice.USAGE,
# NOTE: you likely need to also update
# integrations/docker/images/chip-build/version
# in PRs that update chip-build Dockerfiles. This update is not automated in
# this script.
# Apart from the above files which contain an exact ZAP version, the zap
# execution script contains the mimimal zap execution version, which generally
# we also enforce to be the current version.
# That line is of the form "MIN_ZAP_VERSION = '2021.1.9'"
ZAP_EXECUTION_SCRIPT = 'scripts/tools/zap/'
ZAP_EXECUTION_MIN_RE = re.compile(r'(MIN_ZAP_VERSION = .)(\d\d\d\d\.\d\d?\.\d\d?)(.)')
CHIP_ROOT_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..', '..'))
type=click.Choice(__LOG_LEVELS__.keys(), case_sensitive=False),
help='Determines the verbosity of script output.')
type=click.Choice(__UPDATE_CHOICES__.keys(), case_sensitive=False),
help='What to update: docker, usage, all. Default is "docker".')
help='What version of ZAP to update to (like "v2023.01.09-nightly". If not set, versions will just be printed.')
def version_update(log_level, update, new_version):
coloredlogs.install(level=__LOG_LEVELS__[log_level], fmt='%(asctime)s %(levelname)-7s %(message)s')
update = __UPDATE_CHOICES__[update]
if new_version:
parsed = ZAP_VERSION_RE.match(new_version)
if not parsed:
logging.error(f"Version '{new_version}' does not seem to parse as a ZAP VERSION")
# get the numeric version for zap_execution
# This makes every group element (date section) to a base 10 integer,
# so for 'v2023.01.11-nightly' this gets (2023, 1, 11)
zap_min_version = tuple(map(lambda x: int(x, 10), parsed.groups()))
files_to_update = []
if UpdateChoice.USAGE in update:
if UpdateChoice.DOCKER in update:
for name in files_to_update:
with open(os.path.join(CHIP_ROOT_DIR, name), 'rt') as f:
file_data =
# Write out any matches. Note that we only write distinct matches as
# zap versions may occur several times in the same file
found_versions = set()
for m in ZAP_VERSION_RE.finditer(file_data):
version = file_data[m.start():m.end()]
if not version in found_versions:'%s currently used in %s', version, name)
# If we update, perform the update
if new_version:
search_pos = 0
need_replace = False
m =, search_pos)
while m:
version = file_data[m.start():m.end()]
if version == new_version:
logging.warning("Nothing to replace. Version already %s", version)
file_data = file_data[:m.start()] + new_version + file_data[m.end():]
need_replace = True
search_pos = m.end() # generally ok since our versions are fixed length
m =, search_pos)
if need_replace:'Replacing with version %s in %s', new_version, name)
with open(os.path.join(CHIP_ROOT_DIR, name), 'wt') as f:
# Finally, check zap_execution for any version update
if UpdateChoice.USAGE in update:
with open(os.path.join(CHIP_ROOT_DIR, ZAP_EXECUTION_SCRIPT), 'rt') as f:
file_data =
m ="Min version %s in %s",, ZAP_EXECUTION_SCRIPT)
if new_version:
new_min_version = ("%d.%d.%d" % zap_min_version)
file_data = file_data[:m.start()] + + new_min_version + + file_data[m.end():]'Updating min version to %s in %s', new_min_version, ZAP_EXECUTION_SCRIPT)
with open(os.path.join(CHIP_ROOT_DIR, ZAP_EXECUTION_SCRIPT), 'wt') as f:
if __name__ == '__main__':