Skip to content

Commit 804e085

Browse files
committed
feat(ISV-7356): certProject check fallback to base branch on operator removal
AI-assisted-by: Cursor
1 parent 89927ef commit 804e085

11 files changed

Lines changed: 454 additions & 59 deletions

File tree

ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-hosted-pipeline.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -536,6 +536,9 @@ spec:
536536
- name: source
537537
workspace: repository
538538
subPath: src
539+
- name: base
540+
workspace: repository
541+
subPath: base
539542

540543
- name: get-organization
541544
runAfter:

ansible/roles/operator-pipeline/templates/openshift/pipelines/operator-release-pipeline.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,9 @@ spec:
293293
- name: source
294294
workspace: repository
295295
subPath: src
296+
- name: base
297+
workspace: repository
298+
subPath: base
296299

297300
- name: get-organization
298301
runAfter:

ansible/roles/operator-pipeline/templates/openshift/tasks/cert-project-check.yml

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -20,63 +20,27 @@ spec:
2020
description: Identifier of certification project from Red Hat Connect
2121
workspaces:
2222
- name: source
23+
- name: base
24+
description: Clone of the repo at head of the base branch
25+
optional: true
2326
steps:
2427
- name: certification-project-check
2528
image: "$(params.pipeline_image)"
2629
workingDir: $(workspaces.source.path)
2730
script: |
2831
#! /usr/bin/env bash
2932
set -xe
30-
echo "Checking availability of cert project identifier"
3133
32-
if [ "$(params.cert_project_required)" != "true" ]; then
33-
echo "Cert project ID is not required."
34-
echo -n "" | tee $(results.certification_project_id.path)
35-
exit 0
34+
EXTRA_ARGS=""
35+
if [[ "$(workspaces.base.bound)" == "true" ]]; then
36+
EXTRA_ARGS+=" --repo-base-path $(workspaces.base.path)"
3637
fi
3738
38-
if [[ -z "$(params.affected_operators)" && -z "$(params.affected_catalog_operators)" ]]; then
39-
echo "No operator is affected."
40-
exit 1
41-
fi
42-
43-
# Create dictionary of all affected operators to serve as a set
44-
declare -A ALL_AFFECTED_OPERATORS
45-
46-
# Add operators from affected_operators
47-
if [ -n "$(params.affected_operators)" ]; then
48-
IFS=',' read -ra affected_ops_array <<< "$(params.affected_operators)"
49-
for op in "${affected_ops_array[@]}"; do
50-
ALL_AFFECTED_OPERATORS["$op"]=1
51-
done
52-
fi
53-
54-
# Add operators from affected_catalog_operators
55-
if [ -n "$(params.affected_catalog_operators)" ]; then
56-
IFS=',' read -ra affected_catalog_ops_array <<< "$(params.affected_catalog_operators)"
57-
for op in "${affected_catalog_ops_array[@]}"; do
58-
operator_name=$(echo "$op" | cut -d'/' -f2) # Parse operator name from its catalog path
59-
if [ -n "$operator_name" ]; then
60-
ALL_AFFECTED_OPERATORS["$operator_name"]=1
61-
fi
62-
done
63-
fi
64-
65-
66-
for operator_name in "${!ALL_AFFECTED_OPERATORS[@]}"; do
67-
file_path="operators/$operator_name/ci.yaml"
68-
69-
if [ ! -f "$file_path" ]; then
70-
echo "File '$file_path' not found."
71-
exit 1
72-
fi
73-
74-
CERT_PROJECT_ID=$(cat "$file_path" | yq -r '.cert_project_id | select (.!=null)')
75-
76-
if [ -z "$CERT_PROJECT_ID" ]; then
77-
echo "Certification project ID is missing in '$file_path' file (cert_project_id)"
78-
exit 1
79-
fi
80-
done
81-
82-
echo -n "$CERT_PROJECT_ID" | tee "$(results.certification_project_id.path)"
39+
certification-project-check \
40+
--repo-head-path "$(workspaces.source.path)" \
41+
--affected-operators "$(params.affected_operators)" \
42+
--affected-catalog-operators "$(params.affected_catalog_operators)" \
43+
--cert-project-required "$(params.cert_project_required)" \
44+
--output-file "$(results.certification_project_id.path)" \
45+
--verbose \
46+
$EXTRA_ARGS

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,12 @@ Depending on your use case, you may:
3939

4040
To remove the operator completely from the catalog:
4141

42-
- Delete the your operator directory from the `operators/` folder.
42+
- Delete your operator directory from the `operators/` folder.
4343
- Remove all catalog files related to your operator from the `catalogs/` directory.
4444
- Open a **single** pull request that includes these changes. Follow our contribution guidelines on how to [open a PR](users/contributing-via-pr.md).
4545

46+
Community and certified (ISV) operators follow the same removal steps above. You do not need to keep a `ci.yaml` stub in the pull request for certified operators. When the operator directory is removed in the PR, the ISV pipeline reads `cert_project_id` from the `ci.yaml` on the target branch to authorize and process the removal.
47+
4648
For reference, here’s an [example PR](https://github.com/redhat-openshift-ecosystem/community-operators-prod/pull/5955/files) demonstrating these steps.
4749

4850
### Remove the Operator from Specific Catalog Version(s)

docs/developer-guide.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,8 @@ python3 -m pip install ansible-lint
346346
To run unit tests and code style checkers:
347347

348348
```bash
349-
tox
349+
poetry run tox -p # all tests in paralel
350+
poetry run tox -e bandit # single check
350351
```
351352

352353
### Local development
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Check certification project identifiers for affected operators."""
2+
3+
import argparse
4+
import logging
5+
import sys
6+
from pathlib import Path
7+
8+
from operatorcert.logger import setup_logger
9+
from operatorcert.operator_repo.utils import load_yaml
10+
from operatorcert.utils import SplitArgs
11+
12+
LOGGER = logging.getLogger("operator-cert")
13+
14+
15+
def setup_argparser() -> argparse.ArgumentParser:
16+
"""
17+
Setup argument parser
18+
19+
Returns:
20+
argparse.ArgumentParser: Argument parser
21+
"""
22+
parser = argparse.ArgumentParser(
23+
description="Check certification project identifiers for affected operators."
24+
)
25+
parser.add_argument(
26+
"--repo-head-path",
27+
required=True,
28+
type=Path,
29+
help="Path to the PR head repository clone",
30+
)
31+
parser.add_argument(
32+
"--repo-base-path",
33+
type=Path,
34+
help="Path to the base branch repository clone",
35+
)
36+
parser.add_argument(
37+
"--affected-operators",
38+
default=[],
39+
action=SplitArgs,
40+
help="Comma separated list of affected operators",
41+
)
42+
parser.add_argument(
43+
"--affected-catalog-operators",
44+
default=[],
45+
action=SplitArgs,
46+
help="Comma separated list of affected catalog operators",
47+
)
48+
parser.add_argument(
49+
"--cert-project-required",
50+
default="true",
51+
help="Whether a cert project ID must be present",
52+
)
53+
parser.add_argument(
54+
"--output-file",
55+
help="Path to a file where the certification project ID will be stored",
56+
)
57+
parser.add_argument("--verbose", action="store_true", help="Verbose output")
58+
59+
return parser
60+
61+
62+
def collect_affected_operators(
63+
affected_operators: list[str], affected_catalog_operators: list[str]
64+
) -> set[str]:
65+
"""
66+
Build a set of operator names affected by the pull request.
67+
68+
Args:
69+
affected_operators (list[str]): Operator names from detect-changes
70+
affected_catalog_operators (list[str]): Catalog operators in catalog/version format
71+
72+
Returns:
73+
set[str]: Unique affected operator names
74+
"""
75+
operators = set(affected_operators)
76+
for catalog_operator in affected_catalog_operators:
77+
if "/" not in catalog_operator:
78+
continue
79+
_, operator_name = catalog_operator.split("/", 1)
80+
if operator_name:
81+
operators.add(operator_name)
82+
return operators
83+
84+
85+
def resolve_operator_ci_yaml_path(
86+
source_root: Path,
87+
base_root: Path | None,
88+
operator_name: str,
89+
) -> Path:
90+
"""
91+
Resolve the ci.yaml path for an operator, falling back to the base branch.
92+
93+
Args:
94+
source_root (Path): PR head repository root
95+
base_root (Path | None): Base branch repository root
96+
operator_name (str): Operator name
97+
98+
Returns:
99+
Path: Resolved ci.yaml path
100+
101+
Raises:
102+
FileNotFoundError: When ci.yaml cannot be found in head or base
103+
"""
104+
head_path = source_root / "operators" / operator_name / "ci.yaml"
105+
if head_path.is_file():
106+
return head_path
107+
108+
if base_root is not None:
109+
base_path = base_root / "operators" / operator_name / "ci.yaml"
110+
if base_path.is_file():
111+
LOGGER.info(
112+
"Using ci.yaml from base branch for operator '%s' (removed in PR).",
113+
operator_name,
114+
)
115+
return base_path
116+
117+
raise FileNotFoundError(
118+
f"ci.yaml not found for operator '{operator_name}' in PR head or base branch"
119+
)
120+
121+
122+
def check_certification_projects(
123+
source_root: Path,
124+
base_root: Path | None,
125+
operator_names: set[str],
126+
) -> str:
127+
"""
128+
Validate cert_project_id for all affected operators.
129+
130+
Args:
131+
source_root (Path): PR head repository root
132+
base_root (Path | None): Base branch repository root
133+
operator_names (set[str]): Operator names to check
134+
135+
Returns:
136+
str: Certification project ID from the last validated operator
137+
138+
Raises:
139+
ValueError: When no operators are affected or cert_project_id is missing
140+
FileNotFoundError: When ci.yaml cannot be resolved
141+
"""
142+
if not operator_names:
143+
raise ValueError("No operator is affected.")
144+
145+
cert_project_id = ""
146+
for operator_name in sorted(operator_names):
147+
ci_path = resolve_operator_ci_yaml_path(source_root, base_root, operator_name)
148+
config = load_yaml(ci_path)
149+
project_id = (config or {}).get("cert_project_id")
150+
if not project_id:
151+
raise ValueError(
152+
f"Certification project ID is missing in '{ci_path}' (cert_project_id)"
153+
)
154+
cert_project_id = project_id
155+
156+
return cert_project_id
157+
158+
159+
def main() -> None:
160+
"""
161+
Main function of the script
162+
"""
163+
parser = setup_argparser()
164+
args = parser.parse_args()
165+
166+
log_level = "DEBUG" if args.verbose else "INFO"
167+
setup_logger(level=log_level)
168+
169+
if args.cert_project_required != "true":
170+
LOGGER.info("Cert project ID is not required.")
171+
if args.output_file:
172+
Path(args.output_file).write_text("", encoding="utf-8")
173+
return
174+
175+
operator_names = collect_affected_operators(
176+
args.affected_operators, args.affected_catalog_operators
177+
)
178+
179+
try:
180+
cert_project_id = check_certification_projects(
181+
args.repo_head_path,
182+
args.repo_base_path,
183+
operator_names,
184+
)
185+
except (FileNotFoundError, ValueError) as exc:
186+
LOGGER.error("%s", exc)
187+
sys.exit(1)
188+
print(cert_project_id)
189+
if args.output_file:
190+
Path(args.output_file).write_text(cert_project_id, encoding="utf-8")
191+
192+
193+
if __name__ == "__main__": # pragma: no cover
194+
main()

operatorcert/entrypoints/check_permissions.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -562,9 +562,9 @@ def check_permissions(
562562
operators = operators.union(
563563
extract_operators_from_catalog(head_repo, affected_catalog_operators)
564564
)
565-
removed_catalog_operators = changes.get("removed_catalog_operators", [])
565+
deleted_catalog_operators = changes.get("deleted_catalog_operators", [])
566566
operators = operators.union(
567-
extract_operators_from_catalog(base_repo, removed_catalog_operators)
567+
extract_operators_from_catalog(base_repo, deleted_catalog_operators)
568568
)
569569

570570
is_approved = []

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ build-scratch-catalog = "operatorcert.entrypoints.build_scratch_catalog:main"
4646
bulk-retrigger = "operatorcert.entrypoints.bulk_retrigger:main"
4747
bundle-dockerfile = "operatorcert.entrypoints.bundle_dockerfile:main"
4848
catalog-browser = "operatorcert.catalog.catalog_cli:main"
49+
certification-project-check = "operatorcert.entrypoints.certification_project_check:main"
4950
check-permissions = "operatorcert.entrypoints.check_permissions:main"
5051
create-container-image = "operatorcert.entrypoints.create_container_image:main"
5152
create-github-gist = "operatorcert.entrypoints.create_github_gist:main"

0 commit comments

Comments
 (0)