Skip to content

Commit e467066

Browse files
akladievilya-lavrenovmryzhov
authored
[GHA][CVS-112829] Public Dockerfile (openvinotoolkit#23064)
Co-authored-by: Ilya Lavrenov <ilya.lavrenov@intel.com> Co-authored-by: Mikhail Ryzhov <mikhail.ryzhov@intel.com>
1 parent d16ce02 commit e467066

File tree

13 files changed

+588
-59
lines changed

13 files changed

+588
-59
lines changed

.dockerignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*
2+
!install_build_dependencies.sh
3+
!scripts/install_dependencies/install_openvino_dependencies.sh
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: 'Handle Docker images'
2+
description: 'Builds, tags and pushes a given Docker image when needed'
3+
inputs:
4+
images:
5+
description: 'Image names (registry name + namespace + base name)'
6+
required: true
7+
registry:
8+
description: 'Docker registry'
9+
required: true
10+
dockerfiles_root_dir:
11+
description: 'Path to dockerfiles root dir relative to repository root'
12+
required: true
13+
push:
14+
description: 'Push built images to registry'
15+
required: false
16+
default: 'true'
17+
changed_components:
18+
description: 'Components changed by a pull request'
19+
required: true
20+
21+
outputs:
22+
images:
23+
description: "Images to use in workflow"
24+
value: ${{ steps.handle_images.outputs.images }}
25+
26+
runs:
27+
using: 'composite'
28+
steps:
29+
- name: Checkout head
30+
uses: actions/checkout@v4
31+
32+
- name: Checkout base
33+
uses: actions/checkout@v4
34+
with:
35+
ref: ${{ github.base_ref || github.event.merge_group.base_ref }}
36+
sparse-checkout: ${{ inputs.dockerfiles_root_dir }}/docker_tag
37+
path: base
38+
39+
- name: Install Python dependencies
40+
uses: py-actions/py-dependency-install@v4
41+
with:
42+
path: "${{ github.action_path }}/requirements.txt"
43+
update-setuptools: "false"
44+
update-wheel: "false"
45+
46+
- name: Set up Docker Buildx
47+
id: buildx
48+
uses: docker/setup-buildx-action@v3
49+
50+
- name: Handle docker images
51+
id: handle_images
52+
shell: bash
53+
run: |
54+
images=$(echo "${{ inputs.images }}" | tr '\n' ',' | sed 's/,*$//')
55+
pr="${{ github.event.pull_request.number }}"
56+
57+
python3 .github/actions/handle_docker/get_images_to_build.py \
58+
-d "${{ inputs.dockerfiles_root_dir }}" \
59+
-r "${{ inputs.registry }}" \
60+
--images "$images" \
61+
--head_tag_file "${{ inputs.dockerfiles_root_dir }}/docker_tag" \
62+
--base_tag_file "base/${{ inputs.dockerfiles_root_dir }}/docker_tag" \
63+
--docker_env_changed "${{ fromJSON(inputs.changed_components).docker_env }}" \
64+
--dockerfiles_changed "${{ fromJSON(inputs.changed_components).dockerfiles }}" \
65+
--docker_builder "${{ steps.buildx.outputs.name}}" \
66+
--repo "${{ github.repository }}" \
67+
--ref_name "${{ github.ref_name }}" \
68+
$([[ -n $pr ]] && echo "--pr $pr" || echo '-s ${{ github.sha }}') \
69+
$([[ -n "${{ inputs.push }}" ]] && echo "--push" || echo '')
70+
env:
71+
GITHUB_TOKEN: ${{ github.token }}
72+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
import argparse
2+
import json
3+
import re
4+
import sys
5+
6+
from distutils.util import strtobool
7+
from helpers import *
8+
from images_api import *
9+
10+
11+
def parse_args():
12+
parser = argparse.ArgumentParser(description='Returns list of Docker images to build for a given workflow')
13+
parser.add_argument('-i', '--images', required=True, help='Comma-separated docker images')
14+
parser.add_argument('-d', '--dockerfiles_root', required=True, help='Path to dockerfiles')
15+
parser.add_argument('-r', '--registry', required=True, help='Docker registry name')
16+
parser.add_argument('-s', '--commit', required=False, help='Commit SHA. If not set, --pr is used')
17+
parser.add_argument('-b', '--docker_builder', required=False, help='Docker buildx builder name')
18+
parser.add_argument('--pr', type=int, required=False, help='PR number, if event is pull_request')
19+
parser.add_argument('--head_tag_file', default='.github/dockerfiles/docker_tag', help='Head docker tag file path')
20+
parser.add_argument('--base_tag_file', default=None, required=False, help='Base docker tag file path')
21+
parser.add_argument('--ref_name', required=False, default='', help='GitHub ref name')
22+
parser.add_argument('--repo', default='openvinotoolkit/openvino', help='GitHub repository')
23+
parser.add_argument('--docker_env_changed', type=lambda x: bool(strtobool(x)), default=True,
24+
help='Whether PR changes docker env')
25+
parser.add_argument('--dockerfiles_changed', type=lambda x: bool(strtobool(x)), default=True,
26+
help='Whether PR changes dockerfiles')
27+
parser.add_argument('--action_path', default='.github/actions/handle_docker', help='Path to this GitHub action')
28+
parser.add_argument('--push', action='store_true', required=False, help='Whether to push images to registry')
29+
parser.add_argument('--dry_run', action='store_true', required=False, help='Dry run')
30+
args = parser.parse_args()
31+
return args
32+
33+
34+
def main():
35+
init_logger()
36+
logger = logging.getLogger(__name__)
37+
args = parse_args()
38+
for arg, value in sorted(vars(args).items()):
39+
logger.info(f"Argument {arg}: {value}")
40+
41+
head_tag = Path(args.head_tag_file).read_text().strip()
42+
43+
base_tag_exists = args.base_tag_file and Path(args.base_tag_file).exists()
44+
base_tag = Path(args.base_tag_file).read_text().strip() if base_tag_exists else None
45+
46+
all_dockerfiles = Path(args.dockerfiles_root).rglob('**/*/Dockerfile')
47+
48+
images = ImagesHandler(args.dry_run)
49+
for image in all_dockerfiles:
50+
images.add_from_dockerfile(image, args.dockerfiles_root, args.registry, head_tag, base_tag)
51+
52+
requested_images = set(args.images.split(','))
53+
skip_workflow = False
54+
missing_only = False
55+
56+
merge_queue_target_branch = next(iter(re.findall(f'^gh-readonly-queue/(.*)/', args.ref_name)), None)
57+
58+
if args.pr:
59+
environment_affected = args.docker_env_changed or args.dockerfiles_changed
60+
if environment_affected:
61+
expected_tag = f'pr-{args.pr}'
62+
63+
if head_tag != expected_tag:
64+
logger.error(f"Please update docker tag in {args.head_tag_file} to {expected_tag}")
65+
sys.exit(1)
66+
67+
elif merge_queue_target_branch:
68+
environment_affected = head_tag != base_tag
69+
if environment_affected:
70+
logger.info(f"Environment is affected by PR(s) in merge group")
71+
else:
72+
environment_affected = False
73+
74+
if environment_affected:
75+
changeset = get_changeset(args.repo, args.pr, merge_queue_target_branch, args.commit)
76+
changed_dockerfiles = [p for p in changeset if p.startswith(args.dockerfiles_root) and p.endswith('Dockerfile')]
77+
78+
if args.docker_env_changed:
79+
logger.info(f"Common docker environment is modified, will build all requested images")
80+
changed_images = requested_images
81+
else:
82+
logger.info(f"Common docker environment is not modified, will build only changed and missing images")
83+
changed_images = set([name_from_dockerfile(d, args.dockerfiles_root) for d in changed_dockerfiles])
84+
85+
unchanged_images = requested_images - changed_images
86+
unchanged_with_no_base = images.get_missing(unchanged_images, base=True)
87+
88+
if unchanged_with_no_base:
89+
logger.info("The following images were unchanged, but will be built anyway since the base for them "
90+
f"is missing in registry: {unchanged_with_no_base}")
91+
92+
images_to_tag = unchanged_images.difference(unchanged_with_no_base)
93+
images_to_build = requested_images.intersection(changed_images).union(unchanged_with_no_base)
94+
95+
only_dockerfiles_changed = len(changeset) == len(changed_dockerfiles)
96+
if only_dockerfiles_changed and not images_to_build:
97+
skip_workflow = True
98+
else:
99+
logger.info(f"Environment is not affected, will build only missing images, if any")
100+
images_to_build = requested_images
101+
images_to_tag = []
102+
missing_only = True
103+
104+
if not images_to_build:
105+
logger.info(f"No images to build, will return the list of pre-built images with a new tag")
106+
107+
built_images = images.build(images_to_build, missing_only, args.push, args.docker_builder)
108+
if not built_images:
109+
logger.info(f"No images were built, a new tag will be applied to a pre-built base image if needed")
110+
111+
# When a custom builder is used, it allows to push the image automatically once built. Otherwise, pushing manually
112+
if args.push and not args.docker_builder:
113+
images.push(images_to_build, missing_only)
114+
115+
if environment_affected and base_tag:
116+
images.tag(images_to_tag)
117+
118+
images_output = images_to_output(images.get(requested_images))
119+
set_github_output("images", json.dumps(images_output))
120+
121+
if skip_workflow:
122+
logger.info(f"Docker image changes are irrelevant for current workflow, workflow may be skipped")
123+
set_github_output("skip_workflow", str(skip_workflow))
124+
125+
126+
main()
+72
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import logging
2+
import os
3+
import subprocess
4+
from ghapi.all import GhApi
5+
from pathlib import Path
6+
7+
8+
def init_logger():
9+
logging.basicConfig(level=logging.INFO,
10+
format='%(asctime)s %(name)-15s %(levelname)-8s %(message)s',
11+
datefmt='%m-%d-%Y %H:%M:%S')
12+
13+
14+
def set_github_output(name: str, value: str, github_output_var_name: str = 'GITHUB_OUTPUT'):
15+
"""Sets output variable for a GitHub Action"""
16+
logger = logging.getLogger(__name__)
17+
# In an environment variable "GITHUB_OUTPUT" GHA stores path to a file to write outputs to
18+
with open(os.environ.get(github_output_var_name), 'a+') as file:
19+
logger.info(f"Add {name}={value} to {github_output_var_name}")
20+
print(f'{name}={value}', file=file)
21+
22+
23+
def images_to_output(images: list):
24+
images_output = {}
25+
for image in images:
26+
image_name, os_name = image.name.split('/', 1)
27+
if image_name not in images_output:
28+
images_output[image_name] = {}
29+
30+
images_output[image_name][os_name] = image.ref()
31+
32+
return images_output
33+
34+
35+
def get_changeset(repo: str, pr: str, target_branch: str, commit_sha: str):
36+
"""Returns changeset either from PR or commit"""
37+
owner, repository = repo.split('/')
38+
gh_api = GhApi(owner=owner, repo=repository, token=os.getenv("GITHUB_TOKEN"))
39+
if pr:
40+
changed_files = gh_api.pulls.list_files(pr)
41+
elif target_branch:
42+
target_branch_head_commit = gh_api.repos.get_branch(target_branch).commit.sha
43+
changed_files = gh_api.repos.compare_commits(f'{target_branch_head_commit}...{commit_sha}').get('files', [])
44+
else:
45+
raise ValueError(f'Either "pr" or "target_branch" parameter must be non-empty')
46+
return set([f.filename for f in changed_files])
47+
48+
49+
def run(cmd: str, dry_run: bool = False, fail_on_error: bool = True):
50+
logger = logging.getLogger('run')
51+
logger.info(cmd)
52+
53+
if dry_run:
54+
return 0, ''
55+
56+
with subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
57+
for line in proc.stdout:
58+
logger.info(line.strip())
59+
60+
proc.communicate()
61+
if proc.returncode != 0:
62+
msg = f"Command '{cmd}' returned non-zero exit status {proc.returncode}"
63+
if fail_on_error:
64+
raise RuntimeError(msg)
65+
66+
logger.warning(msg)
67+
return proc.returncode
68+
69+
70+
def name_from_dockerfile(dockerfile: str | Path, dockerfiles_root: str | Path) -> str:
71+
image_name = str(Path(dockerfile).relative_to(dockerfiles_root).parent.as_posix())
72+
return image_name

0 commit comments

Comments
 (0)