From f53cfc7cf250defa9edd8e69d1da3f1498802367 Mon Sep 17 00:00:00 2001 From: Gloria Ciavarrini Date: Thu, 18 Jun 2026 11:42:54 +0200 Subject: [PATCH 1/4] Update shared-workflows for control-plane monolith. Tag control-plane instead of archived managers, refresh Quay docs, and remove the unused gateway contract test workflow. Assisted-By: Claude (Anthropic) Signed-off-by: Gloria Ciavarrini --- .github/workflows/build-push-quay.yaml | 4 +- .github/workflows/gateway-contract-test.yaml | 107 ------ README.md | 69 +--- hack/gateway-contract-test.py | 372 ------------------- hack/tag-release.sh | 9 +- 5 files changed, 11 insertions(+), 550 deletions(-) delete mode 100644 .github/workflows/gateway-contract-test.yaml delete mode 100644 hack/gateway-contract-test.py diff --git a/.github/workflows/build-push-quay.yaml b/.github/workflows/build-push-quay.yaml index 1d3884b..7a7ac54 100644 --- a/.github/workflows/build-push-quay.yaml +++ b/.github/workflows/build-push-quay.yaml @@ -1,5 +1,5 @@ # Reusable workflow: build a container image and push to Quay.io. -# Call from manager repos (service-provider-manager, catalog-manager, etc.). +# Call from DCM repos that publish container images (control-plane, service providers, etc.). # Caller must pass QUAY_USERNAME and QUAY_PASSWORD (or robot token) as secrets. name: Build and Push to Quay @@ -8,7 +8,7 @@ on: workflow_call: inputs: image-name: - description: 'Image name (e.g. service-provider-manager)' + description: 'Image name (e.g. control-plane)' type: string required: true registry: diff --git a/.github/workflows/gateway-contract-test.yaml b/.github/workflows/gateway-contract-test.yaml deleted file mode 100644 index db3b146..0000000 --- a/.github/workflows/gateway-contract-test.yaml +++ /dev/null @@ -1,107 +0,0 @@ -name: Gateway Contract Test - -on: - workflow_call: - inputs: - krakend-repo: - description: 'Repo containing krakend.json (owner/repo). Empty = calling repo.' - type: string - default: '' - krakend-ref: - description: 'Git ref when fetching krakend.json from external repo' - type: string - default: 'main' - krakend-config: - description: 'Path to krakend.json within the repo' - type: string - default: 'config/krakend.json' - override-spec: - description: 'Override a service spec with local file: hostname=path/to/spec.yaml' - type: string - default: '' - service: - description: 'Only validate routes for this service hostname' - type: string - default: '' - warn-uncovered: - description: 'Warn about spec paths not covered by gateway routes' - type: boolean - default: false - watch-paths: - description: 'Paths that trigger the contract test (newline-separated globs)' - type: string - default: | - config/krakend.json - config/krakend.json.tmpl - api/**/openapi.yaml - -jobs: - contract-test: - name: Contract test - runs-on: ubuntu-latest - steps: - - name: Checkout calling repo - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Check for relevant changes - id: changes - uses: tj-actions/changed-files@v47 - with: - files: ${{ inputs.watch-paths }} - - - name: Checkout shared-workflows - if: steps.changes.outputs.any_changed == 'true' - uses: actions/checkout@v6 - with: - repository: dcm-project/shared-workflows - ref: main - path: .shared-workflows - - - name: Checkout krakend repo - if: steps.changes.outputs.any_changed == 'true' && inputs.krakend-repo != '' - uses: actions/checkout@v6 - with: - repository: ${{ inputs.krakend-repo }} - ref: ${{ inputs.krakend-ref }} - path: .krakend-repo - - - name: Install dependencies - if: steps.changes.outputs.any_changed == 'true' - run: pip install pyyaml jsonref - - - name: Run contract test - if: steps.changes.outputs.any_changed == 'true' - env: - INPUT_KRAKEND_REPO: ${{ inputs.krakend-repo }} - INPUT_KRAKEND_CONFIG: ${{ inputs.krakend-config }} - INPUT_WARN_UNCOVERED: ${{ inputs.warn-uncovered }} - INPUT_SERVICE: ${{ inputs.service }} - INPUT_OVERRIDE_SPEC: ${{ inputs.override-spec }} - run: | - if [ -n "${INPUT_KRAKEND_REPO}" ]; then - CONFIG="${GITHUB_WORKSPACE}/.krakend-repo/${INPUT_KRAKEND_CONFIG}" - else - CONFIG="${GITHUB_WORKSPACE}/${INPUT_KRAKEND_CONFIG}" - fi - - FLAGS="" - if [ "${INPUT_WARN_UNCOVERED}" = "true" ]; then - FLAGS="${FLAGS} --warn-uncovered" - fi - if [ -n "${INPUT_SERVICE}" ]; then - FLAGS="${FLAGS} --service ${INPUT_SERVICE}" - fi - if [ -n "${INPUT_OVERRIDE_SPEC}" ]; then - SPEC="${INPUT_OVERRIDE_SPEC}" - HOST="${SPEC%%=*}" - PATH_PART="${SPEC#*=}" - FLAGS="${FLAGS} --override ${HOST}=${GITHUB_WORKSPACE}/${PATH_PART}" - fi - - python3 .shared-workflows/hack/gateway-contract-test.py --config "${CONFIG}" ${FLAGS} - - - name: Skip notice - if: steps.changes.outputs.any_changed != 'true' - run: echo "No relevant file changes detected, skipping contract test." diff --git a/README.md b/README.md index 31f1c67..ffc510d 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,8 @@ Reusable GitHub Actions workflows for DCM project repositories. | `check-aep.yaml` | Validate OpenAPI specs against AEP standards | Repos with OpenAPI | | `check-generate.yaml` | Verify generated files are in sync | Repos with code generation | | `check-clean-commits.yaml` | Ensure PR commits are cleaned before merge | All repos | -| `build-push-quay.yaml` | Build container image and push to Quay.io | Manager repos (Containerfile) | +| `build-push-quay.yaml` | Build container image and push to Quay.io | DCM repos with a Containerfile | | `tag-release.yaml` | Git-tag all service repos with a release or RC version | shared-workflows (manual dispatch) | -| `gateway-contract-test.yaml` | Validate KrakenD gateway routes against backend OpenAPI specs | api-gateway and backend repos | | `gitleaks.yaml` | Scan for leaked secrets using gitleaks | All repos | ## Usage @@ -37,7 +36,7 @@ See individual workflow files for available options and inputs. ### Build and push to Quay.io -Use `build-push-quay.yaml` from manager repos that have a `Containerfile`. Create `.github/workflows/build-push-quay.yaml`: +Use `build-push-quay.yaml` from DCM repos that have a `Containerfile`. Create `.github/workflows/build-push-quay.yaml`: ```yaml name: Build and Push Image @@ -55,11 +54,11 @@ jobs: build-push: uses: dcm-project/shared-workflows/.github/workflows/build-push-quay.yaml@main with: - image-name: service-provider-manager + image-name: control-plane version: ${{ github.event.inputs.version }} secrets: quay-username: ${{ secrets.QUAY_USERNAME }} - quay-password: ${{ secrets.QUAY_PASSWORD }} + quay-password: ${{ secrets.QUAY_TOKEN }} ``` #### Tag behavior @@ -132,7 +131,7 @@ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ -f tag=v0.0.1-rc.2 \ - -f services="placement-manager catalog-manager" + -f services="control-plane kubevirt-service-provider" gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ -f tag=v0.0.1 @@ -147,69 +146,13 @@ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ ```bash ./hack/tag-release.sh v0.0.1-rc.1 # tag all services -./hack/tag-release.sh v0.0.1-rc.2 placement-manager catalog-manager # specific services only +./hack/tag-release.sh v0.0.1-rc.2 control-plane kubevirt-service-provider # specific services only ./hack/tag-release.sh v0.0.1 # final release ``` The script resolves the HEAD of the release branch (derived from the tag: `v0.0.1-rc.1` or `v0.0.1` -> branch `release/v0.0.1`) for each service repo via `gh api`, creates an annotated git tag at that commit, and pushes it. -### Gateway contract test - -Use `gateway-contract-test.yaml` from the **api-gateway** repo to validate that KrakenD routes match backend OpenAPI specs, and from **manager** repos to validate that a service's OpenAPI spec is covered by the gateway. The gateway's `krakend.json` must define `x-contract-specs`. - -**x-contract-specs** is a top-level key in the KrakenD config (e.g. `config/krakend.json`). It is an object mapping backend hostname (as used in gateway routes) to `{ "openapi_url": "https://..." }`. The script loads each spec from `openapi_url` and checks that every backend route's method and path exists in the corresponding spec. Example: - -```json -"x-contract-specs": { - "service-provider-manager": { - "openapi_url": "https://raw.githubusercontent.com/dcm-project/service-provider-manager/main/api/v1alpha1/openapi.yaml" - }, - "catalog-manager": { - "openapi_url": "https://raw.githubusercontent.com/dcm-project/catalog-manager/main/api/v1alpha1/openapi.yaml" - } -} -``` - -**Gateway repo:** In the repo that owns `krakend.json` (e.g. api-gateway), add a job to `.github/workflows/ci.yaml`: - -```yaml -gateway-contract-test: - name: Gateway Contract Test - uses: dcm-project/shared-workflows/.github/workflows/gateway-contract-test.yaml@main - with: - warn-uncovered: true - watch-paths: | - config/krakend.json - config/krakend.json.tmpl -``` - -Use `warn-uncovered: true` to warn when the spec defines paths that no gateway route uses. - -**Manager repo:** In a backend/manager repo that exposes an OpenAPI spec and is wired in the gateway's `x-contract-specs`, create `.github/workflows/gateway-contract-test.yaml` (or add a job to CI) that calls the shared workflow with `krakend-repo` set to the gateway repo and `override-spec` so this repo's OpenAPI file is used for that service. Run on PRs when `api/**/openapi.yaml` (or equivalent) changes: - -```yaml -name: Gateway Contract Test -on: - pull_request: - branches: [main] - -jobs: - contract-test: - uses: dcm-project/shared-workflows/.github/workflows/gateway-contract-test.yaml@main - with: - krakend-repo: dcm-project/api-gateway - krakend-config: config/krakend.json - override-spec: service-provider-manager=api/v1alpha1/openapi.yaml - service: service-provider-manager - watch-paths: | - api/**/openapi.yaml -``` - -`override-spec` is `hostname=path/to/openapi.yaml` (path relative to the manager repo root). `service` limits validation to that backend. - -**Key inputs:** `krakend-repo`, `krakend-config`, `override-spec`, `service`, `warn-uncovered`, `watch-paths`. See the workflow file for the full list. - ### Gitleaks secret scanning Use `gitleaks.yaml` to scan for leaked secrets. It supports three scan modes: **diff** (PR commits), **push** (pushed commits), and **full** (entire history). Results are reported in SARIF format and uploaded to GitHub Code Scanning by default, so findings appear in the repository Security tab and as inline PR annotations. diff --git a/hack/gateway-contract-test.py b/hack/gateway-contract-test.py deleted file mode 100644 index c4c7f2c..0000000 --- a/hack/gateway-contract-test.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -"""Validate KrakenD backend routes against upstream OpenAPI specs.""" - -import argparse -import json -import re -import sys -from dataclasses import dataclass -from pathlib import Path -from urllib.parse import urlparse -from urllib.request import urlopen -from urllib.error import URLError - -import yaml -import jsonref - -HTTP_METHODS = {"get", "post", "put", "patch", "delete", "head", "options", "trace"} - - -def normalize_path(path: str) -> str: - """Replace path parameters like {id} or {id:[0-9]+} with {_}.""" - return re.sub(r"\{[^}]*\}", "{_}", path) - - -def extract_hostname(url: str) -> str: - """Extract hostname from a URL like http://service:8080/path.""" - parsed = urlparse(url) - if parsed.hostname: - return parsed.hostname - return url - - -def extract_base_path(spec: dict) -> str: - """Detect Swagger 2.0 vs OpenAPI 3.x and return the base path.""" - if spec.get("swagger"): - base = spec.get("basePath", "") - return base.rstrip("/") - - servers = spec.get("servers", []) - if servers: - server_url = servers[0].get("url", "") - parsed = urlparse(server_url) - if parsed.path: - return parsed.path.rstrip("/") - return "" - - -def parse_spec(spec: dict, verbose: bool = False) -> tuple[set[str], set[str]]: - """Parse an OpenAPI/Swagger spec and return operations and paths. - - Returns: - operations: set of "METHOD /normalized/path" strings - all_paths: set of all normalized paths (for uncovered warnings) - """ - operations = set() - all_paths = set() - - paths = spec.get("paths", {}) - if not paths: - return operations, all_paths - - base_path = extract_base_path(spec) - - for api_path, path_item in paths.items(): - if not isinstance(path_item, dict): - continue - for method, operation in path_item.items(): - if method == "$ref": - continue - if method.lower() not in HTTP_METHODS: - continue - - full_path = f"{base_path}{api_path}" if base_path else api_path - normalized = normalize_path(full_path) - key = f"{method.upper()} {normalized}" - - operations.add(key) - all_paths.add(normalized) - - if verbose: - print(f" spec: {key}") - - return operations, all_paths - - -def download_spec(url: str) -> dict: - """Download and parse a spec from a URL.""" - parsed = urlparse(url) - if parsed.scheme not in ("http", "https"): - raise SystemExit(f"Error: unsupported URL scheme {parsed.scheme!r} in {url}") - - try: - with urlopen(url, timeout=30) as response: - data = response.read() - except (URLError, OSError) as exc: - raise SystemExit(f"Error: failed to download spec from {url}: {exc}") from exc - - return yaml.safe_load(data) - - -def load_spec(path: str) -> dict: - """Load and parse a spec from a local file.""" - try: - with open(path, encoding="utf-8") as f: - return yaml.safe_load(f) - except FileNotFoundError: - raise SystemExit(f"Error: file not found: {path}") - except yaml.YAMLError as exc: - raise SystemExit(f"Error: invalid YAML in {path}: {exc}") - - -def _yaml_loader(uri: str) -> dict: - """Load a document from a file or HTTP URI and parse as YAML (or JSON).""" - try: - with urlopen(uri, timeout=30) as response: - data = response.read() - except (URLError, OSError) as exc: - raise jsonref.JsonRefError(f"Failed to load {uri}: {exc}", None, uri=uri) from exc - try: - return yaml.safe_load(data) - except yaml.YAMLError as exc: - raise jsonref.JsonRefError(f"Invalid YAML/JSON in {uri}: {exc}", None, uri=uri) from exc - - -def resolve_refs(spec: dict, base_uri: str) -> dict: - """Resolve $ref in the spec so path items and operations are inlined. Returns a new dict. - - base_uri must be the spec's location (absolute file URI or URL) so relative $ref - (e.g. ./schemas/common.yaml) resolve correctly. - """ - return jsonref.replace_refs( - spec, - base_uri=base_uri, - loader=_yaml_loader, - proxies=False, - ) - - -@dataclass -class BackendRoute: - method: str - path: str - hostname: str - - -def extract_routes(config: dict, service_filter: str, verbose: bool) -> list[BackendRoute]: - """Extract backend routes from KrakenD config.""" - routes = [] - - for endpoint in config.get("endpoints", []): - endpoint_method = endpoint.get("method", "") - - for backend in endpoint.get("backend", []): - hosts = backend.get("host", []) - url_pattern = backend.get("url_pattern", "") - - if not hosts: - if verbose: - print(f" WARN backend {url_pattern} has no host defined; " - "skipping validation", file=sys.stderr) - continue - - host_url = hosts[0] - hostname = extract_hostname(host_url) - - if service_filter and hostname != service_filter: - continue - - method = backend.get("method", "") or endpoint_method or "GET" - if verbose and not backend.get("method") and not endpoint_method: - print(f" WARN no method for backend {url_pattern}; defaulting to GET", - file=sys.stderr) - - routes.append(BackendRoute( - method=method.upper(), - path=url_pattern, - hostname=hostname, - )) - - return routes - - -@dataclass -class ValidationResult: - method: str - path: str - hostname: str - passed: bool - reason: str = "" - - -def validate_routes( - routes: list[BackendRoute], - spec_ops: dict[str, set[str]], - verbose: bool, -) -> tuple[list[ValidationResult], dict[str, set[str]]]: - """Validate routes against spec operations. - - Returns: - results: list of ValidationResult - covered_paths: dict of service -> set of paths that were matched - """ - results = [] - covered_paths: dict[str, set[str]] = {} - - for route in routes: - if route.hostname not in spec_ops: - results.append(ValidationResult( - method=route.method, - path=route.path, - hostname=route.hostname, - passed=False, - reason=f"no spec configured for hostname {route.hostname!r}", - )) - continue - - if verbose and re.search(r"\{[^}]+:[^}]+\}", route.path): - print(f' WARN path "{route.path}" contains regex parameter; ' - "match may be unreliable", file=sys.stderr) - - normalized = normalize_path(route.path) - lookup_key = f"{route.method} {normalized}" - - if lookup_key in spec_ops[route.hostname]: - results.append(ValidationResult( - method=route.method, - path=route.path, - hostname=route.hostname, - passed=True, - )) - covered_paths.setdefault(route.hostname, set()).add(normalized) - else: - results.append(ValidationResult( - method=route.method, - path=route.path, - hostname=route.hostname, - passed=False, - reason=f"not found in OpenAPI spec (expected key: {lookup_key})", - )) - - return results, covered_paths - - -def main() -> int: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--config", default="config/krakend.json", - help="Path to krakend config (default: config/krakend.json)") - parser.add_argument("--warn-uncovered", action="store_true", - help="Warn about spec paths not covered by any backend route") - parser.add_argument("--verbose", action="store_true", help="Verbose output") - parser.add_argument("--service", default="", - help="Only validate routes for this service hostname") - parser.add_argument("--override", action="append", default=[], metavar="HOST=PATH", - help="Override a service spec with local file (repeatable)") - args = parser.parse_args() - - overrides = {} - for override in args.override: - if "=" not in override: - print("Error: --override must be in format hostname=/path/to/spec.yaml", file=sys.stderr) - return 2 - host, path = override.split("=", 1) - if not host or not path: - print("Error: --override must be in format hostname=/path/to/spec.yaml", file=sys.stderr) - return 2 - overrides[host] = path - - # Load KrakenD config - print("Contract Test: KrakenD vs OpenAPI Specs") - print("========================================") - - try: - with open(args.config) as f: - config = json.load(f) - except FileNotFoundError: - print(f"Error: config file not found: {args.config}", file=sys.stderr) - return 2 - except json.JSONDecodeError as exc: - print(f"Error: invalid JSON in {args.config}: {exc}", file=sys.stderr) - return 2 - - contract_specs = config.get("x-contract-specs") - if not contract_specs: - print("Error: no x-contract-specs found in config", file=sys.stderr) - return 2 - - # Download and parse specs - print("Downloading specs...") - spec_ops: dict[str, set[str]] = {} - spec_all_paths: dict[str, set[str]] = {} - - for svc_name in sorted(contract_specs.keys()): - if args.service and svc_name != args.service: - if args.verbose: - print(f" {svc_name}: skipped (filtering for {args.service})") - continue - - if svc_name in overrides: - print(f" {svc_name}: loading from local file {overrides[svc_name]}") - spec = load_spec(overrides[svc_name]) - # Resolve to absolute so relative $ref in the spec resolve against the file's location. - base_uri = Path(overrides[svc_name]).resolve().as_uri() - else: - spec_url = contract_specs[svc_name].get("openapi_url", "") - if not spec_url: - print(f"Error: service {svc_name}: openapi_url is empty", file=sys.stderr) - return 2 - spec = download_spec(spec_url) - base_uri = spec_url - - try: - spec = resolve_refs(spec, base_uri) - except jsonref.JsonRefError as exc: - print(f"Error: service {svc_name}: failed to resolve $ref: {exc}", file=sys.stderr) - return 2 - - if args.verbose: - swagger_ver = spec.get("swagger") - openapi_ver = spec.get("openapi") - if swagger_ver: - print(f" detected: Swagger {swagger_ver}") - elif openapi_ver: - print(f" detected: OpenAPI {openapi_ver}") - - operations, all_paths = parse_spec(spec, verbose=args.verbose) - - if len(operations) == 0: - print(f"Error: service {svc_name}: spec contains no operations " - "(0 paths with HTTP methods)", file=sys.stderr) - return 2 - - spec_ops[svc_name] = operations - spec_all_paths[svc_name] = all_paths - print(f" {svc_name}: OK ({len(operations)} operations)") - - # Extract and validate routes - routes = extract_routes(config, args.service, args.verbose) - - print(f"\nValidating {len(routes)} backend routes...") - - results, covered_paths = validate_routes(routes, spec_ops, args.verbose) - - passed = 0 - failed = 0 - for result in results: - if result.passed: - print(f" PASS {result.method:<6} {result.path:<45} -> {result.hostname}") - passed += 1 - else: - print(f" FAIL {result.method:<6} {result.path:<45} -> {result.hostname}") - print(f" {result.reason}") - failed += 1 - - # Warn about uncovered paths - if args.warn_uncovered: - print() - for svc_name in sorted(spec_all_paths.keys()): - covered = covered_paths.get(svc_name, set()) - for path in sorted(spec_all_paths[svc_name] - covered): - print(f" WARN spec path {path:<45} in {svc_name} " - "not covered by any gateway route") - - print() - if failed > 0: - print(f"Result: FAIL ({passed} passed, {failed} failed)") - return 1 - print(f"Result: PASS ({passed} passed, {failed} failed)") - return 0 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/hack/tag-release.sh b/hack/tag-release.sh index 49e3d26..4c094e9 100755 --- a/hack/tag-release.sh +++ b/hack/tag-release.sh @@ -11,16 +11,13 @@ # # Usage: # ./hack/tag-release.sh v0.0.1-rc.1 -# ./hack/tag-release.sh v0.0.1-rc.2 placement-manager catalog-manager +# ./hack/tag-release.sh v0.0.1-rc.2 control-plane kubevirt-service-provider # ./hack/tag-release.sh v0.0.1 set -euo pipefail ORG="dcm-project" ALL_SERVICES=( - placement-manager - service-provider-manager - catalog-manager - policy-manager + control-plane kubevirt-service-provider k8s-container-service-provider acm-cluster-service-provider @@ -34,7 +31,7 @@ usage() { echo "" echo "Examples:" echo " $0 v0.0.1-rc.1" - echo " $0 v0.0.1-rc.2 placement-manager catalog-manager" + echo " $0 v0.0.1-rc.2 control-plane kubevirt-service-provider" echo " $0 v0.0.1" exit 1 } From bffa952dbe99c5c56499aaf9c18e0a313dfd89c7 Mon Sep 17 00:00:00 2001 From: Gloria Ciavarrini Date: Thu, 18 Jun 2026 11:49:18 +0200 Subject: [PATCH 2/4] Wrap README to 80 chars Signed-off-by: Gloria Ciavarrini --- README.md | 74 +++++++++++++++++++++++++++++++++++++------------------ 1 file changed, 50 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index ffc510d..4aeedcb 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,8 @@ See individual workflow files for available options and inputs. ### Build and push to Quay.io -Use `build-push-quay.yaml` from DCM repos that have a `Containerfile`. Create `.github/workflows/build-push-quay.yaml`: +Use `build-push-quay.yaml` from DCM repos that have a `Containerfile`. Create +`.github/workflows/build-push-quay.yaml`: ```yaml name: Build and Push Image @@ -70,11 +71,13 @@ jobs: | Push of `v*` git tag | ``, tag name | `:a6882f7`, `:v0.0.1`, `:v0.0.1-rc.1` | | Manual dispatch with `version` | Only the specified tag | `:v0.0.1` | -`main` tag is only produced by pushes to the `main` branch. Version branch and git tag pushes do not update it. +`main` tag is only produced by pushes to the `main` branch. Version branch and +git tag pushes do not update it. #### Release flow -1. **Development happens on `main`** -- every merge triggers a build tagged `main` and ``. +1. **Development happens on `main`** -- every merge triggers a build tagged + `main` and ``. 2. **Create a release branch** prefixed with `release/`: ```bash @@ -82,22 +85,25 @@ jobs: git push -u origin release/v0.0.1 ``` -3. **Push fixes** to the release branch. Each push triggers CI, producing a `` image: +3. **Push fixes** to the release branch. Each push triggers CI, producing a + `` image: ```bash git checkout release/v0.0.1 # ... commit fixes ... git push origin release/v0.0.1 ``` -4. **(Optional) Tag a release candidate**. Use the [script](https://github.com/dcm-project/shared-workflows/blob/main/hack/tag-release.sh) +4. **(Optional) Tag a release candidate**. Use the + [script](https://github.com/dcm-project/shared-workflows/blob/main/hack/tag-release.sh) to git-tag all service repos at once from their release branch HEAD: ```bash ./hack/tag-release.sh v0.0.1-rc.1 ``` - Each git tag push triggers CI, which builds the image with tags `v0.0.1-rc.1` and ``. + Each git tag push triggers CI, which builds the image with tags `v0.0.1-rc.1` + and ``. -5. **QE validates** against the RC tag. If issues are found, - push fixes to the release branch (step 3) and cut the next RC (step 4). +5. **QE validates** against the RC tag. If issues are found, push fixes to the + release branch (step 3) and cut the next RC (step 4). 6. **Tag the final release** on the approved commit using the same script: ```bash @@ -105,25 +111,32 @@ jobs: ``` CI builds the image with tags `v0.0.1` and `` for each service. -7. **Cherry-pick** bug fixes from the release branch into `main` (if not already in main), - so that issues caught during stabilization are propagated into main. +7. **Cherry-pick** bug fixes from the release branch into `main` (if not already + in main), so that issues caught during stabilization are propagated into + main. -8. **For the next release**, create a new branch (e.g. `release/v0.0.2` or `release/v0.1.0`) and repeat from step 2. +8. **For the next release**, create a new branch (e.g. `release/v0.0.2` or + `release/v0.1.0`) and repeat from step 2. #### Version convention -Follow [Semantic Versioning](https://semver.org/): `vMAJOR.MINOR.PATCH` (e.g. `v0.0.1`, `v1.2.0`). -All version identifiers must start with `v`. Release branches use the `release/` prefix (e.g. `release/v0.0.1`). -Both release candidates (`v0.0.1-rc.1`) and final releases (`v0.0.1`) are git-tagged. +Follow [Semantic Versioning](https://semver.org/): `vMAJOR.MINOR.PATCH` (e.g. +`v0.0.1`, `v1.2.0`). All version identifiers must start with `v`. Release +branches use the `release/` prefix (e.g. `release/v0.0.1`). Both release +candidates (`v0.0.1-rc.1`) and final releases (`v0.0.1`) are git-tagged. -**Required secrets:** `QUAY_USERNAME`, `QUAY_PASSWORD` (org or repo level). Default registry is `quay.io/dcm-project`. Images are built for `linux/amd64` and `linux/arm64`; override with the `platforms` input if needed. +**Required secrets:** `QUAY_USERNAME`, `QUAY_PASSWORD` (org or repo level). +Default registry is `quay.io/dcm-project`. Images are built for `linux/amd64` +and `linux/arm64`; override with the `platforms` input if needed. #### Release tagging -Use `tag-release.yaml` to git-tag all service repos at once from their release branch HEAD. -Each tag push triggers CI, which builds and pushes the image. This works for both RC tags and final releases. +Use `tag-release.yaml` to git-tag all service repos at once from their release +branch HEAD. Each tag push triggers CI, which builds and pushes the image. This +works for both RC tags and final releases. -**GitHub workflow** -- trigger via the Actions UI (shared-workflows -> Actions -> "Tag Release" -> Run workflow) or the CLI: +**GitHub workflow** -- trigger via the Actions UI (shared-workflows -> Actions +-> "Tag Release" -> Run workflow) or the CLI: ```bash gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ @@ -150,12 +163,17 @@ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ ./hack/tag-release.sh v0.0.1 # final release ``` -The script resolves the HEAD of the release branch (derived from the tag: `v0.0.1-rc.1` or `v0.0.1` -> branch `release/v0.0.1`) -for each service repo via `gh api`, creates an annotated git tag at that commit, and pushes it. +The script resolves the HEAD of the release branch (derived from the tag: +`v0.0.1-rc.1` or `v0.0.1` -> branch `release/v0.0.1`) for each service repo via +`gh api`, creates an annotated git tag at that commit, and pushes it. ### Gitleaks secret scanning -Use `gitleaks.yaml` to scan for leaked secrets. It supports three scan modes: **diff** (PR commits), **push** (pushed commits), and **full** (entire history). Results are reported in SARIF format and uploaded to GitHub Code Scanning by default, so findings appear in the repository Security tab and as inline PR annotations. +Use `gitleaks.yaml` to scan for leaked secrets. It supports three scan modes: +**diff** (PR commits), **push** (pushed commits), and **full** (entire history). +Results are reported in SARIF format and uploaded to GitHub Code Scanning by +default, so findings appear in the repository Security tab and as inline PR +annotations. **Pull request scanning** -- create `.github/workflows/gitleaks.yaml`: @@ -208,7 +226,15 @@ jobs: | `artifact-name` | string | `'gitleaks-report'` | Artifact name when `upload-sarif` is false | | `artifact-retention-days` | number | `90` | Artifact retention in days | -**Custom config:** To override gitleaks rules, add a `.gitleaks.toml` to your repo and pass its path via `config-path`. When empty, gitleaks uses its built-in ruleset. - -**SARIF integration:** Results use [SARIF](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) format and are uploaded to GitHub Code Scanning via `github/codeql-action/upload-sarif@v4`. This makes findings visible in the repository's Security tab and as inline PR annotations, consistent with other GitHub security tooling. For full-history scans, set `upload-sarif: false` to download the report as a workflow artifact instead. +**Custom config:** To override gitleaks rules, add a `.gitleaks.toml` to your +repo and pass its path via `config-path`. When empty, gitleaks uses its built-in +ruleset. + +**SARIF integration:** Results use +[SARIF](https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning) +format and are uploaded to GitHub Code Scanning via +`github/codeql-action/upload-sarif@v4`. This makes findings visible in the +repository's Security tab and as inline PR annotations, consistent with other +GitHub security tooling. For full-history scans, set `upload-sarif: false` to +download the report as a workflow artifact instead. From c216087cc146237501238bc99794bf4e8846ade8 Mon Sep 17 00:00:00 2001 From: Gloria Ciavarrini Date: Fri, 19 Jun 2026 09:31:52 +0200 Subject: [PATCH 3/4] Minor rewording for build-push-quay description Signed-off-by: Gloria Ciavarrini --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4aeedcb..aa0b7a6 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Reusable GitHub Actions workflows for DCM project repositories. | `check-aep.yaml` | Validate OpenAPI specs against AEP standards | Repos with OpenAPI | | `check-generate.yaml` | Verify generated files are in sync | Repos with code generation | | `check-clean-commits.yaml` | Ensure PR commits are cleaned before merge | All repos | -| `build-push-quay.yaml` | Build container image and push to Quay.io | DCM repos with a Containerfile | +| `build-push-quay.yaml` | Build container image and push to Quay.io | `dcm-project` repos with a Containerfile | | `tag-release.yaml` | Git-tag all service repos with a release or RC version | shared-workflows (manual dispatch) | | `gitleaks.yaml` | Scan for leaked secrets using gitleaks | All repos | @@ -36,7 +36,7 @@ See individual workflow files for available options and inputs. ### Build and push to Quay.io -Use `build-push-quay.yaml` from DCM repos that have a `Containerfile`. Create +Use `build-push-quay.yaml` from `dcm-project` repos that have a `Containerfile`. Create `.github/workflows/build-push-quay.yaml`: ```yaml From 4af19e209d33166b75acce513e28ccf2f533d251 Mon Sep 17 00:00:00 2001 From: Gloria Ciavarrini Date: Fri, 19 Jun 2026 09:48:06 +0200 Subject: [PATCH 4/4] Clarify Quay secrets docs per PR review Assisted-By: Claude (Anthropic) Signed-off-by: Gloria Ciavarrini --- .github/workflows/build-push-quay.yaml | 11 +++++++---- README.md | 9 ++++++--- hack/tag-release.sh | 4 ++-- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/.github/workflows/build-push-quay.yaml b/.github/workflows/build-push-quay.yaml index 7a7ac54..4639d88 100644 --- a/.github/workflows/build-push-quay.yaml +++ b/.github/workflows/build-push-quay.yaml @@ -1,6 +1,9 @@ # Reusable workflow: build a container image and push to Quay.io. -# Call from DCM repos that publish container images (control-plane, service providers, etc.). -# Caller must pass QUAY_USERNAME and QUAY_PASSWORD (or robot token) as secrets. +# Call from `dcm-project` repos that publish container images (control-plane, +# service providers, etc.). +# Caller repo secrets: QUAY_USERNAME and QUAY_TOKEN (Quay login password for +# that account; robot token or user password). Map to quay-username and +# quay-password inputs below. name: Build and Push to Quay @@ -37,10 +40,10 @@ on: default: 'linux/amd64,linux/arm64' secrets: quay-username: - description: 'Quay.io username or robot account name' + description: 'Quay username or robot account. Caller maps secrets.QUAY_USERNAME here.' required: true quay-password: - description: 'Quay.io password or robot token' + description: 'Quay login password (robot token or user password). Caller maps secrets.QUAY_TOKEN here.' required: true jobs: diff --git a/README.md b/README.md index aa0b7a6..a5592fc 100644 --- a/README.md +++ b/README.md @@ -125,7 +125,10 @@ Follow [Semantic Versioning](https://semver.org/): `vMAJOR.MINOR.PATCH` (e.g. branches use the `release/` prefix (e.g. `release/v0.0.1`). Both release candidates (`v0.0.1-rc.1`) and final releases (`v0.0.1`) are git-tagged. -**Required secrets:** `QUAY_USERNAME`, `QUAY_PASSWORD` (org or repo level). +**Required secrets:** `QUAY_USERNAME`, `QUAY_TOKEN` (org or repo level). +`QUAY_TOKEN` is the Quay password for that account (robot token or user +password). Map both to the workflow `quay-username` and `quay-password` +inputs as in the example above. Default registry is `quay.io/dcm-project`. Images are built for `linux/amd64` and `linux/arm64`; override with the `platforms` input if needed. @@ -144,7 +147,7 @@ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ -f tag=v0.0.1-rc.2 \ - -f services="control-plane kubevirt-service-provider" + -f services="control-plane" gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ -f tag=v0.0.1 @@ -159,7 +162,7 @@ gh workflow run tag-release.yaml --repo dcm-project/shared-workflows \ ```bash ./hack/tag-release.sh v0.0.1-rc.1 # tag all services -./hack/tag-release.sh v0.0.1-rc.2 control-plane kubevirt-service-provider # specific services only +./hack/tag-release.sh v0.0.1-rc.2 control-plane # specific services only ./hack/tag-release.sh v0.0.1 # final release ``` diff --git a/hack/tag-release.sh b/hack/tag-release.sh index 4c094e9..4dcd16c 100755 --- a/hack/tag-release.sh +++ b/hack/tag-release.sh @@ -11,7 +11,7 @@ # # Usage: # ./hack/tag-release.sh v0.0.1-rc.1 -# ./hack/tag-release.sh v0.0.1-rc.2 control-plane kubevirt-service-provider +# ./hack/tag-release.sh v0.0.1-rc.2 control-plane # ./hack/tag-release.sh v0.0.1 set -euo pipefail @@ -31,7 +31,7 @@ usage() { echo "" echo "Examples:" echo " $0 v0.0.1-rc.1" - echo " $0 v0.0.1-rc.2 control-plane kubevirt-service-provider" + echo " $0 v0.0.1-rc.2 control-plane" echo " $0 v0.0.1" exit 1 }