Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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