Skip to content
Merged
61 changes: 61 additions & 0 deletions .github/workflows/check_base_os.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2025 Google LLC
#
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################

name: 'Check Base OS Consistency'

on:
pull_request:
paths:
- 'projects/**'

jobs:
check-consistency:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch all history to compare with main

- name: Get changed project directories
id: changed-projects
run: |
# Get the list of changed files compared to the target branch
# and filter for unique directories under 'projects/'.
CHANGED_DIRS=$(git diff --name-only ${{ github.base_ref }} ${{ github.head_ref }} | \
grep '^projects/' | \
xargs -n 1 dirname | \
sort -u)
echo "changed_dirs=${CHANGED_DIRS}" >> $GITHUB_OUTPUT

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'

- name: Install dependencies
run: pip install PyYAML

- name: Check each modified project
if: steps.changed-projects.outputs.changed_dirs != ''
run: |
EXIT_CODE=0
for project_dir in ${{ steps.changed-projects.outputs.changed_dirs }};
do
echo "--- Checking ${project_dir} ---"
python3 infra/ci/check_base_os.py "${project_dir}" || EXIT_CODE=$?
done
exit $EXIT_CODE
18 changes: 15 additions & 3 deletions infra/build/functions/build_lib.py
Original file line number Diff line number Diff line change
Expand Up @@ -685,12 +685,24 @@ def get_gcb_url(build_id, cloud_project='oss-fuzz'):
f'{build_id}?project={cloud_project}')


def get_runner_image_name(test_image_suffix):
"""Returns the runner image that should be used. Returns the testing image if
|test_image_suffix|."""
def get_runner_image_name(test_image_suffix, base_image_tag=None):
"""Returns the runner image that should be used.

Returns the testing image if |test_image_suffix|.
"""
image = f'gcr.io/{BASE_IMAGES_PROJECT}/base-runner'

# For trial builds, the version is embedded in the suffix.
if test_image_suffix:
image += '-' + test_image_suffix
return image

# For local/manual runs, the version is passed as a tag.
# Only add a tag if it's specified and not 'legacy', as 'legacy' implies
# 'latest', which is the default behavior.
if base_image_tag and base_image_tag != 'legacy':
image += ':' + base_image_tag

return image


Expand Down
5 changes: 4 additions & 1 deletion infra/build/functions/build_project.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@
class Config:
testing: bool = False
test_image_suffix: Optional[str] = None
base_image_tag: Optional[str] = None
repo: Optional[str] = DEFAULT_OSS_FUZZ_REPO
branch: Optional[str] = None
parallel: bool = False
Expand Down Expand Up @@ -453,8 +454,10 @@ def get_build_steps_for_project(project,
f'--architecture {build.architecture} {project.name}\\n' +
'*' * 80)
# Test fuzz targets.
runner_image_name = build_lib.get_runner_image_name(
config.test_image_suffix, config.base_image_tag)
test_step = {
'name': build_lib.get_runner_image_name(config.test_image_suffix),
'name': runner_image_name,
'env': env,
'args': [
'bash', '-c',
Expand Down
3 changes: 2 additions & 1 deletion infra/build/functions/trial_build.py
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,7 @@ def _do_test_builds(args, test_image_suffix, end_time, version_tag):

config = build_project.Config(testing=True,
test_image_suffix=test_image_suffix,
base_image_tag=version_tag,
repo=args.repo,
branch=args.branch,
parallel=False,
Expand Down Expand Up @@ -432,7 +433,7 @@ def _do_build_type_builds(args, config, credentials, build_type, projects):
credentials,
build_type.type_name,
extra_tags=tags,
timeout=PROJECT_BUILD_TIMEOUT))
timeout=PROJECT_BUILD_TIMEOUT))['id']
time.sleep(1) # Avoid going over 75 requests per second limit.
except Exception as error: # pylint: disable=broad-except
# Handle flake.
Expand Down
2 changes: 1 addition & 1 deletion infra/build/functions/trial_build_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ def test_build_steps_correct(self, mock_gcb_build_and_push_images,
del mock_wait_on_builds
self.maxDiff = None # pylint: disable=invalid-name
build_id = 1
mock_run_build.return_value = build_id
mock_run_build.return_value = {'id': build_id}
branch_name = 'mybranch'
project = 'skcms'
args = [
Expand Down
111 changes: 111 additions & 0 deletions infra/ci/check_base_os.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
# Copyright 2025 Google LLC
#
# 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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
################################################################################
"""
A CI script to ensure that the base OS version specified in a project's
project.yaml file matches the FROM line in its Dockerfile.
"""

import os
import sys
import yaml

# Defines the base OS versions that are currently supported for use in project.yaml.
# For now, only 'legacy' is permitted. This list will be expanded as new
# base images are rolled out.
SUPPORTED_VERSIONS = [
'legacy',
# 'ubuntu-20-04',
# 'ubuntu-24-04',
]

# A map from the base_os_version in project.yaml to the expected Dockerfile
# FROM tag.
BASE_OS_TO_DOCKER_TAG = {
'legacy': 'latest',
'ubuntu-20-04': 'ubuntu-20-04',
'ubuntu-24-04': 'ubuntu-24-04',
}


def main():
"""Checks the Dockerfile FROM tag against the project's base_os_version."""
if len(sys.argv) < 2:
print(f'Usage: {sys.argv[0]} <project_path>', file=sys.stderr)
return 1

project_path = sys.argv[1]
project_yaml_path = os.path.join(project_path, 'project.yaml')
dockerfile_path = os.path.join(project_path, 'Dockerfile')

# 1. Get the base_os_version from project.yaml, defaulting to 'legacy'.
base_os_version = 'legacy'
if os.path.exists(project_yaml_path):
with open(project_yaml_path) as f:
config = yaml.safe_load(f)
if config and 'base_os_version' in config:
base_os_version = config['base_os_version']

# 2. Validate that the version is currently supported.
if base_os_version not in SUPPORTED_VERSIONS:
print(
f'Error: base_os_version "{base_os_version}" is not yet supported. '
f'The currently supported versions are: "{", ".join(SUPPORTED_VERSIONS)}"',
file=sys.stderr)
return 1

# 3. Get the expected Dockerfile tag from our mapping.
expected_tag = BASE_OS_TO_DOCKER_TAG[base_os_version]

# 4. Read the Dockerfile and find the tag in the FROM line.
if not os.path.exists(dockerfile_path):
print(f'Error: Dockerfile not found at {dockerfile_path}', file=sys.stderr)
return 1

dockerfile_tag = ''
with open(dockerfile_path) as f:
for line in f:
if line.strip().startswith('FROM'):
try:
if ':' not in line:
print(
f'Error: Malformed FROM line in Dockerfile (missing tag): {line.strip()}',
file=sys.stderr)
return 1
dockerfile_tag = line.split(':')[1].strip()
except IndexError:
print(f'Error: Could not parse tag from Dockerfile FROM line: {line}',
file=sys.stderr)
return 1
break

# 5. Compare and report.
if dockerfile_tag != expected_tag:
print(
f'Error: Mismatch found in {project_path}.\n'
f' - project.yaml (base_os_version): "{base_os_version}" (expects Dockerfile tag "{expected_tag}")\n'
f' - Dockerfile FROM tag: "{dockerfile_tag}"\n'
f'Please align the Dockerfile\'s FROM line to use the tag "{expected_tag}".',
file=sys.stderr)
return 1

print(
f'Success: {project_path} is consistent (base_os_version: "{base_os_version}", Dockerfile tag: "{dockerfile_tag}").'
)
return 0


if __name__ == '__main__':
sys.exit(main())
Loading
Loading