From 639bc0707ab476b09072fd8ecf2f6fc029300e70 Mon Sep 17 00:00:00 2001 From: Ringo De Smet Date: Mon, 18 May 2026 15:37:39 +0200 Subject: [PATCH 1/3] Add policies validation workflow --- .ci/scripts/docgen.py | 85 ++++++++++++++++ .ci/scripts/release.bash | 168 ++++++++++++++++++++++++++++++++ .github/workflows/validate.yaml | 36 +++++++ Makefile | 15 +++ 4 files changed, 304 insertions(+) create mode 100644 .ci/scripts/docgen.py create mode 100755 .ci/scripts/release.bash create mode 100644 .github/workflows/validate.yaml create mode 100644 Makefile diff --git a/.ci/scripts/docgen.py b/.ci/scripts/docgen.py new file mode 100644 index 0000000..4e07035 --- /dev/null +++ b/.ci/scripts/docgen.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 + +import os +import yaml +from dataclasses import dataclass +from typing import List +from urllib.parse import urlparse, urlunparse + +@dataclass +class PolicyMetadata: + dir: str + version: str + description: str + path: str + +def find_policy_yaml(start_dir=".") -> List[str]: + """Recursively search for files named 'Policy.yaml'.""" + matches = [] + for root, _, files in os.walk(start_dir): + if "Policy.yaml" in files: + matches.append(os.path.join(root, "Policy.yaml")) + return sorted(matches) + +def load_policy_metadata(file_path: str) -> PolicyMetadata: + """Load a Policy.yaml file and unmarshal it into a PolicyMetadata object.""" + with open(file_path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) or {} + + dir_name = os.path.dirname(file_path) + return PolicyMetadata( + dir=data.get("dir", dir_name), + version=data.get("version", ""), + description=data.get("description", ""), + path=file_path, + ) + +def generate_markdown_table(policies: List[PolicyMetadata]) -> str: + """Generate a Markdown table from a list of PolicyMetadata objects.""" + header = "| URL | Description | Link |\n" + separator = "|------------|-----------|---------| \n" + rows = [] + for p in policies: + description = p.description.replace("\n", " ").strip() + ghcr_path = f"ghcr.io/{os.path.normpath(os.path.dirname(p.path))}" + readme_url = replace_filename_in_url(f"https://github.com/ContainerCraft/updatecli-policies/tree/main/{p.path}", "README.md") + rows.append(f"| `{ghcr_path}:{p.version}` | {description or '-'} | {f"[link]({readme_url})" } |") + return header + separator + "\n".join(rows) + +def replace_filename_in_url(url: str, new_filename: str) -> str: + # Parse the URL + parsed = urlparse(url) + + # Split the path and replace the last part with the new filename + path_parts = parsed.path.split('/') + path_parts[-1] = new_filename + new_path = '/'.join(path_parts) + + # Rebuild the URL with the new path + new_url = urlunparse(parsed._replace(path=new_path)) + return new_url + +# Example usage +original_url = "https://github.com/ContainerCraft/updatecli-policies/blob/main/updatecli/policies/crd-pulumi-sdk/Policy.yaml" +new_url = replace_filename_in_url(original_url, "README.md") + +def main(): + policies = [] + for policy_file in find_policy_yaml("."): + try: + metadata = load_policy_metadata(policy_file) + policies.append(metadata) + except Exception as e: + print(f"⚠️ Error parsing {policy_file}: {e}") + + if not policies: + print("No Policy.yaml files found.") + return + + markdown = generate_markdown_table(policies) + + with open("POLICIES.md", "w", encoding="utf-8") as f: + f.write(markdown) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.ci/scripts/release.bash b/.ci/scripts/release.bash new file mode 100755 index 0000000..7bf8c27 --- /dev/null +++ b/.ci/scripts/release.bash @@ -0,0 +1,168 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Ensure we work from the Updatecli directory +# This is even more important as we use the policy path to generate the policy reference +pushd updatecli + +: "${POLICIES_ROOT_DIR:=policies}" +: "${POLICY_ERROR:=false}" +: "${OCI_REPOSITORY:=ghcr.io/ContainerCraft/updatecli-policies}" + +: "${GITHUB_REGISTRY:=}" + +POLICIES=$(find "$POLICIES_ROOT_DIR" -name "Policy.yaml") + +# release publish an Updatecli policy version to the registry +function release(){ + local POLICY_ROOT_DIR="$1" + # Trim policy path with root directory path + local POLICY_DIR="${1#"$POLICIES_ROOT_DIR/"}" + + updatecli manifest push \ + --config updatecli.d \ + --values values.yaml \ + --policy Policy.yaml \ + --tag "$OCI_REPOSITORY/$POLICY_DIR" \ + "$POLICY_ROOT_DIR" +} + +function runUpdatecliDiff(){ + local POLICY_ROOT_DIR="" + POLICY_ROOT_DIR="$1" + + updatecli diff \ + --config "$POLICY_ROOT_DIR/updatecli.d" \ + --values "$POLICY_ROOT_DIR/values.yaml" \ + --values "$POLICY_ROOT_DIR/testdata/values.yaml" +} + +function validateRequiredFile(){ + local POLICY_ROOT_DIR="$1" + local POLICY_VALUES="$POLICY_ROOT_DIR/values.yaml" + local POLICY_README="$POLICY_ROOT_DIR/README.md" + local POLICY_METADATA="$POLICY_ROOT_DIR/Policy.yaml" + local POLICY_CHANGELOG="$POLICY_ROOT_DIR/CHANGELOG.md" + + echo "* validating policy $POLICY_ROOT_DIR" + + + # Checking for files + for POLICY_FILE in "$POLICY_VALUES" "$POLICY_CHANGELOG" "$POLICY_README" "$POLICY_METADATA" + do + if [[ ! -f "$POLICY_FILE" ]]; then + + POLICY_ERROR=true + echo " * file '$POLICY_FILE' missing for policy $POLICY_ROOT_DIR" + true + fi + done + + local POLICY_MANIFEST="$POLICY_ROOT_DIR/updatecli.d" + # Checking for directories + if [[ ! -d "$POLICY_MANIFEST" ]]; then + + POLICY_ERROR=true + echo " * directory '$POLICY_MANIFEST' missing for policy $POLICY_ROOT_DIR" + true + fi + + ## Testing that Policy.yaml contains the required information + local sourceInformation="" + sourceInformation=$(sed -n -e 's/^source: //p' "$POLICY_METADATA" ) + local expectedSourceInformation="\"https://github.com/ContainerCraft/updatecli-policies/tree/main/updatecli/$POLICY_ROOT_DIR/\"" + if [[ ! $sourceInformation == "$expectedSourceInformation" ]]; then + POLICY_ERROR=true + echo " * policy $POLICY_ROOT_DIR missing the right source information in Policy.yaml" + echo " expected: $expectedSourceInformation" + echo " got: $sourceInformation" + fi + + local documentationInformation="" + documentationInformation=$(sed -n -e 's/^documentation: //p' "$POLICY_METADATA") + local expectedDocumentationInformation="\"https://github.com/ContainerCraft/updatecli-policies/tree/main/updatecli/$POLICY_ROOT_DIR/README.md\"" + if [[ ! $documentationInformation == "$expectedDocumentationInformation" ]]; then + POLICY_ERROR=true + echo " * policy $POLICY_ROOT_DIR missing the right documentation information in Policy.yaml" + echo " expected: $expectedDocumentationInformation" + echo " got: $documentationInformation" + fi + + # Testing url annotation is defined + local urlInformation="" + urlInformation=$(sed -n -e 's/^url: //p' "$POLICY_METADATA") + local expectedUrlInformation="\"https://github.com/ContainerCraft/updatecli-policies/\"" + if [[ ! $urlInformation == "$expectedUrlInformation" ]]; then + POLICY_ERROR=true + echo " * policy $POLICY_ROOT_DIR missing the right url information in Policy.yaml" + echo " expected: $expectedUrlInformation" + echo " got: $urlInformation" + fi + + # Testing version annotation is defined + local versionInformation="" + versionInformation=$(sed -n -e 's/^version: //p' "$POLICY_METADATA") + if [[ $versionInformation == "" ]]; then + POLICY_ERROR=true + echo " * policy $POLICY_ROOT_DIR missing a version information in Policy.yaml" + fi + + # Testing that the latest version has a changelog entry + local versionChangelogEntry="" + versionChangelogEntry=$(grep " $versionInformation" "$POLICY_CHANGELOG") + if [[ $versionChangelogEntry == "" ]]; then + POLICY_ERROR=true + echo " * Changelog missing a version entry such as '## $versionInformation' in $POLICY_CHANGELOG" + fi + + # Testing that the latest changelog version is used in the Policy.yaml + latestVersionChangelogEntry=$(sed -n -e '1,4s/^.*## //p' "$POLICY_CHANGELOG") + if [[ "$latestVersionChangelogEntry" != "$versionInformation" ]]; then + POLICY_ERROR=true + echo " * Latest Changelog version isn't the one used in Policy.yaml" + echo " '## $latestVersionChangelogEntry' in $POLICY_CHANGELOG" + echo " '## $versionInformation' in $POLICY_METADATA" + fi +} + +function main(){ + + PARAM="$1" + + GLOBAL_ERROR=0 + + for POLICY in $POLICIES + do + echo "" + + POLICY_ROOT_DIR=$(dirname "$POLICY") + POLICY_ERROR=false + + if [[ "$PARAM" == "--e2e-test" ]]; then + runUpdatecliDiff "$POLICY_ROOT_DIR" + fi + + if [[ "$PARAM" == "--unit-test" || "$PARAM" == "" ]]; then + validateRequiredFile "$POLICY_ROOT_DIR" + fi + + if [[ "$POLICY_ERROR" = "false" ]]; then + echo " => all is good" + + if [[ "$PARAM" == "--publish" ]]; then + release "$POLICY_ROOT_DIR" + fi + + else + echo "" + echo " => validation test not passing" + + GLOBAL_ERROR=1 + fi + + done + + exit "$GLOBAL_ERROR" +} + +main "${1:-}" diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml new file mode 100644 index 0000000..f47e9c5 --- /dev/null +++ b/.github/workflows/validate.yaml @@ -0,0 +1,36 @@ +--- +name: Validate Policies +on: + workflow_dispatch: + pull_request: + push: +jobs: + validate: + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - name: Setup updatecli + uses: updatecli/updatecli-action@2c3221bc5f4499a99fec2c87d9de4a83cb30e990 # v3.1.3 + - name: Setup releasepost + uses: updatecli/releasepost-action@864390bddae97db06ee881ab4a08d159b4464643 # v0.5.0 + - name: Validate + run: make test + - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0 + # Only run e2e tests from the main branch as we need some credentials + # that we don't want to risk leaking from pullrequest opened by random contributors + if: github.ref == 'refs/heads/main' + id: generate_testing_token + with: + client-id: ${{ secrets.CONTAINERCRAFTER_APP_CLIENT_ID }} + private-key: ${{ secrets.CONTAINERCRAFTER_APP_PRIVATE_KEY }} + - name: e2e tests + # Only run e2e tests from the main branch as we need some credentials + # that we don't want to risk leaking from pullrequest opened by random contributors + if: github.ref == 'refs/heads/main' + run: make e2e-test + env: + GITHUB_TOKEN: ${{ steps.generate_testing_token.outputs.token }} + RELEASEPOST_GITHUB_TOKEN: ${{ steps.generate_testing_token.outputs.token }} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..11b4afa --- /dev/null +++ b/Makefile @@ -0,0 +1,15 @@ +.PHONY: release +release: ## Release checks for each policy if they can be published on ghcr.io + .ci/scripts/release.bash --publish + +.PHONY: validate +test: ## Release checks for each policy if they can be published on ghcr.io + .ci/scripts/release.bash + +.PHONY: validate +e2e-test: ## Release checks for each policy if they can be published on ghcr.io + .ci/scripts/release.bash --e2e-test + +.PHONY: help +help: ## Show this Makefile's help + @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' From 8db155d9b67495c0d581f66b6f8e9f92ce033340 Mon Sep 17 00:00:00 2001 From: Ringo De Smet Date: Mon, 18 May 2026 15:47:24 +0200 Subject: [PATCH 2/3] Can't nest an f-string inside another f-string. --- .ci/scripts/docgen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.ci/scripts/docgen.py b/.ci/scripts/docgen.py index 4e07035..e74dec8 100644 --- a/.ci/scripts/docgen.py +++ b/.ci/scripts/docgen.py @@ -43,7 +43,7 @@ def generate_markdown_table(policies: List[PolicyMetadata]) -> str: description = p.description.replace("\n", " ").strip() ghcr_path = f"ghcr.io/{os.path.normpath(os.path.dirname(p.path))}" readme_url = replace_filename_in_url(f"https://github.com/ContainerCraft/updatecli-policies/tree/main/{p.path}", "README.md") - rows.append(f"| `{ghcr_path}:{p.version}` | {description or '-'} | {f"[link]({readme_url})" } |") + rows.append(f"| `{ghcr_path}:{p.version}` | {description or '-'} | [link]({readme_url}) |") return header + separator + "\n".join(rows) def replace_filename_in_url(url: str, new_filename: str) -> str: From aeb7d8a5c44a35075da5788e04d19e8115948ae9 Mon Sep 17 00:00:00 2001 From: Ringo De Smet Date: Mon, 18 May 2026 15:52:05 +0200 Subject: [PATCH 3/3] Properly indent & fix pattern for help target --- Makefile | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Makefile b/Makefile index 11b4afa..16a18ab 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ .PHONY: release -release: ## Release checks for each policy if they can be published on ghcr.io +release: ## Publish policies on ghcr.io .ci/scripts/release.bash --publish .PHONY: validate @@ -7,9 +7,9 @@ test: ## Release checks for each policy if they can be published on ghcr.io .ci/scripts/release.bash .PHONY: validate -e2e-test: ## Release checks for each policy if they can be published on ghcr.io +e2e-test: ## End-to-end checks for each policy if they can be published on ghcr.io .ci/scripts/release.bash --e2e-test .PHONY: help help: ## Show this Makefile's help - @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' + @grep -E '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'