diff --git a/.gitea/workflows/quality-checks-cpp.yml b/.gitea/workflows/quality-checks-cpp.yml new file mode 100644 index 0000000..1caf47e --- /dev/null +++ b/.gitea/workflows/quality-checks-cpp.yml @@ -0,0 +1,71 @@ +name: Quality checks C++ + +# Reusable workflow (ADR-0008). A repository calls it with +# +# jobs: +# quality: +# uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1 +# +# It checks the C and C++ files of the repository with clang-format and +# clang-tidy at exact versions, changes no file and needs no secret. The +# repository needs a .clang-format; a .clang-tidy and a compile database +# (compile-commands-dir) are optional. +on: + workflow_call: + inputs: + working-directory: + description: "Directory, relative to the repository root, whose files are checked" + type: string + default: "." + std: + description: "C++ standard for clang-tidy when no compile database is given, such as c++20" + type: string + default: "c++20" + compile-commands-dir: + description: "Directory with a compile_commands.json built by the caller before the call; empty uses the std input" + type: string + default: "" + clang-format-version: + description: "Exact version of the clang-format package" + type: string + default: "23.1.3" + clang-tidy-version: + description: "Exact version of the clang-tidy package" + type: string + default: "22.1.8" + tools-ref: + description: "Tag or branch of TirSystem/github-action whose tools/quality-checks-cpp.sh is run; pin it to the exact tag together with the workflow" + type: string + default: "v1" + +jobs: + quality-checks-cpp: + permissions: + contents: read + runs-on: ubuntu-latest + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + STD: ${{ inputs.std }} + COMPILE_COMMANDS_DIR: ${{ inputs.compile-commands-dir }} + CLANG_FORMAT_VERSION: ${{ inputs.clang-format-version }} + CLANG_TIDY_VERSION: ${{ inputs.clang-tidy-version }} + QC_VENV: /tmp/quality-tools + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Fetch the shared script + uses: actions/checkout@v4 + with: + repository: TirSystem/github-action + ref: ${{ inputs.tools-ref }} + path: shared-scripts + + - name: Validate the inputs + run: bash shared-scripts/tools/quality-checks-cpp.sh validate + + - name: Install the tools + run: bash shared-scripts/tools/quality-checks-cpp.sh install + + - name: Run the checks + run: bash shared-scripts/tools/quality-checks-cpp.sh run diff --git a/.gitea/workflows/quality-checks-csharp.yml b/.gitea/workflows/quality-checks-csharp.yml new file mode 100644 index 0000000..2a1e298 --- /dev/null +++ b/.gitea/workflows/quality-checks-csharp.yml @@ -0,0 +1,60 @@ +name: Quality checks C# + +# Reusable workflow (ADR-0008). A repository calls it with +# +# jobs: +# quality: +# uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1 +# +# It checks the C# solution or project of the repository with `dotnet format` +# and `dotnet build` (warnings as errors) on an exact .NET SDK 10 or later, +# changes no file and needs no secret. The package restore needs the network. +on: + workflow_call: + inputs: + working-directory: + description: "Directory, relative to the repository root, whose files are checked" + type: string + default: "." + project: + description: "Solution or project file, relative to working-directory; empty uses the single .sln, .slnx or .csproj found" + type: string + default: "" + dotnet-sdk-version: + description: "Exact .NET SDK version, 10 or later (no channel, no range)" + type: string + default: "10.0.401" + tools-ref: + description: "Tag or branch of TirSystem/github-action whose tools/quality-checks-csharp.sh is run; pin it to the exact tag together with the workflow" + type: string + default: "v1" + +jobs: + quality-checks-csharp: + permissions: + contents: read + runs-on: ubuntu-latest + env: + WORKING_DIRECTORY: ${{ inputs.working-directory }} + PROJECT: ${{ inputs.project }} + DOTNET_SDK_VERSION: ${{ inputs.dotnet-sdk-version }} + DOTNET_ROOT: /tmp/dotnet + steps: + - name: Check out the repository + uses: actions/checkout@v4 + + - name: Fetch the shared script + uses: actions/checkout@v4 + with: + repository: TirSystem/github-action + ref: ${{ inputs.tools-ref }} + path: shared-scripts + + - name: Validate the inputs + run: bash shared-scripts/tools/quality-checks-csharp.sh validate + + - name: Install the .NET SDK + run: bash shared-scripts/tools/quality-checks-csharp.sh install + + - name: Run the checks + run: bash shared-scripts/tools/quality-checks-csharp.sh run diff --git a/CHANGELOG.md b/CHANGELOG.md index 726d4d7..5e17fbe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,18 @@ Each release lists its changes in these groups, in this order: ## [Unreleased] +### Added + +- Reusable workflow `quality-checks-cpp.yml` (ADR-0008): `clang-format --dry-run + -Werror` and `clang-tidy` on the tracked C and C++ files, at exact versions + (inputs `clang-format-version`, `clang-tidy-version`), with the inputs + `working-directory`, `std`, `compile-commands-dir` and `tools-ref`. The + repository needs a `.clang-format`. +- Reusable workflow `quality-checks-csharp.yml` (ADR-0008): `dotnet format + --verify-no-changes` and `dotnet build -warnaserror` on an exact .NET SDK 10 + or later (input `dotnet-sdk-version`, default `10.0.401`), with the inputs + `working-directory`, `project` and `tools-ref`. + ### Changed - Every job has a `timeout-minutes` (5 to 15), so a hung step no longer holds a diff --git a/README.md b/README.md index a4f38f7..d117fc4 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,19 @@ A collection of reusable Gitea Actions workflows for DevOps professionals and so This repository is the central collection of reusable automation for TirSystem repositories. The primary repository lives at `git.tirsystem.com` (Gitea) and is push-mirrored to GitHub. -It contains six workflows: metadata sync to the GitHub mirror, wiki publishing, shared quality checks, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#-workflows). +It contains eight workflows: metadata sync to the GitHub mirror, wiki publishing, shared quality checks for Python and shell, for C and C++ and for C#, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#-workflows). ## 🔧 Workflows -Six workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. +Eight workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. | Workflow | File | Kind | Starts on | Purpose | | --- | --- | --- | --- | --- | | Sync GitHub mirror metadata | [`sync-github-metadata.yml`](.gitea/scoped_workflows/sync-github-metadata.yml) | Scoped | push to `main`, manual, daily 03:17 UTC | Copies the description and topics of a Gitea repository to its GitHub push mirror | | Publish wiki | [`publish-wiki.yml`](.gitea/scoped_workflows/publish-wiki.yml) | Scoped, opt-in with `wiki/.publish` | push to `main` that changes `wiki/**`, manual | Publishes the pages in `wiki/` to the repository wiki | | Quality checks | [`quality-checks.yml`](.gitea/workflows/quality-checks.yml) | Reusable (`workflow_call`) | a call from another repository | Runs ruff, mypy, shellcheck and shfmt on the tracked files and changes nothing | +| Quality checks C++ | [`quality-checks-cpp.yml`](.gitea/workflows/quality-checks-cpp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs clang-format and clang-tidy at exact versions on the tracked C and C++ files and changes nothing | +| Quality checks C# | [`quality-checks-csharp.yml`](.gitea/workflows/quality-checks-csharp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs `dotnet format` and `dotnet build` on an exact .NET SDK 10 or later and changes nothing | | Release | [`release.yml`](.gitea/workflows/release.yml) | This repository only | manual | Creates the version tag, the release page and the major tag after the checks | | Publish accepted milestones | [`sync-milestones.yml`](.gitea/workflows/sync-milestones.yml) | This repository only | push to `main` that changes `docs/milestones/**` or `docs/sqa/reviews/**`, manual (dry run by default) | Creates or updates the Gitea milestone and issues of every milestone that is Accepted with a Go review | | Validate workflows | [`validate-workflows.yml`](.gitea/workflows/validate-workflows.yml) | This repository only | pull request, push to `main`, manual | Parses every workflow and runs the offline tests | @@ -188,6 +190,96 @@ deactivate Workflow @enduml ``` +### 🧱 Quality checks C++ + +A reusable workflow for C and C++, called with `uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1` (ADR-0008). The repository needs a `.clang-format`; a `.clang-tidy` and a compile database are optional. + +#### 🔀 Workflow + +```plantuml +@startuml +title Shared quality checks for C and C++ (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-cpp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-cpp" as Workflow +participant "PyPI" as PyPI + +Caller -> Workflow: workflow_call with working-directory, std,\ncompile-commands-dir and tool versions +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or a version is not exact + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> PyPI: Install clang-format and clang-tidy at the exact versions +PyPI --> Workflow: Tools installed + +alt no C or C++ file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no .clang-format, or compile-commands-dir\nwithout compile_commands.json + Workflow --> Caller: Fail with [configuration] +else + Workflow -> Workflow: clang-format --dry-run -Werror on all files + Workflow -> Workflow: clang-tidy on the C++ files (-std=std)\nand on the C files (or with the compile database) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + +### 🟣 Quality checks C# + +A reusable workflow for C#, called with `uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1` (ADR-0008). It installs the exact .NET SDK given by `dotnet-sdk-version` (10 or later) and needs the network for the package restore. + +#### 🔀 Workflow + +```plantuml +@startuml +title Shared quality checks for C# (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-csharp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-csharp" as Workflow +participant "dot.net\n(dotnet-install.sh)" as DotNet + +Caller -> Workflow: workflow_call with working-directory,\nproject and dotnet-sdk-version +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or the SDK is not an exact\nstable version 10 or later + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> DotNet: Install the exact .NET SDK version +DotNet --> Workflow: SDK installed + +alt no C# file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no solution or project, or several without the project input + Workflow --> Caller: Fail with [configuration] that names them +else + Workflow -> Workflow: dotnet format --verify-no-changes + Workflow -> Workflow: dotnet build -warnaserror\n(package restore needs the network) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + ### 🏷️ Release Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](guides/release-process.md)). diff --git a/tests/test_quality_workflows_cpp_csharp.py b/tests/test_quality_workflows_cpp_csharp.py new file mode 100644 index 0000000..e04f3f1 --- /dev/null +++ b/tests/test_quality_workflows_cpp_csharp.py @@ -0,0 +1,634 @@ +"""Offline tests for the reusable C++ and C# quality-check workflows (ADR-0008). + +The shell steps run with stub tools in a throwaway git repository, so no +network, no clang and no .NET SDK are needed. +""" + +import os +import shutil +import subprocess +from pathlib import Path +from typing import Any + +import pytest +import yaml + +from workflow_source import REPO_ROOT + +CPP_WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "quality-checks-cpp.yml" +CSHARP_WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "quality-checks-csharp.yml" +CPP_SCRIPT = REPO_ROOT / "tools" / "quality-checks-cpp.sh" +CSHARP_SCRIPT = REPO_ROOT / "tools" / "quality-checks-csharp.sh" +BASH = shutil.which("bash") + +pytestmark = pytest.mark.skipif(BASH is None, reason="bash is needed to run the workflow steps") + +# A script run: the finished process and the tool calls the stubs logged. +Run = tuple[subprocess.CompletedProcess[str], list[str]] + + +def load(path: Path) -> dict[str, Any]: # parsed YAML has no fixed shape + return yaml.safe_load(path.read_text(encoding="utf-8")) + + +def inputs_of(path: Path) -> dict[str, Any]: + return load(path)[True]["workflow_call"]["inputs"] # PyYAML reads `on` as True + + +@pytest.mark.parametrize( + "workflow, script, job", + [ + (CPP_WORKFLOW, "quality-checks-cpp.sh", "quality-checks-cpp"), + (CSHARP_WORKFLOW, "quality-checks-csharp.sh", "quality-checks-csharp"), + ], + ids=["cpp", "csharp"], +) +class TestDefinition: + def test_has_least_privilege_and_needs_no_secret(self, workflow: Path, script: str, job: str) -> None: + document = load(workflow) + assert document["jobs"][job]["permissions"] == {"contents": "read"} + assert "secrets" not in workflow.read_text(encoding="utf-8").replace("needs no secret", "") + + def test_embeds_no_script_and_runs_the_file_in_three_steps( + self, + workflow: Path, + script: str, + job: str, + ) -> None: + steps = load(workflow)["jobs"][job]["steps"] + commands = [step["run"] for step in steps if "run" in step] + assert commands == [f"bash shared-scripts/tools/{script} {c}" for c in ("validate", "install", "run")] + + def test_fetches_the_script_at_the_tools_ref_input(self, workflow: Path, script: str, job: str) -> None: + steps = load(workflow)["jobs"][job]["steps"] + fetch = next(step for step in steps if step.get("name") == "Fetch the shared script") + assert fetch["with"] == { + "repository": "TirSystem/github-action", + "ref": "${{ inputs.tools-ref }}", + "path": "shared-scripts", + } + assert inputs_of(workflow)["tools-ref"]["default"] == "v1" + + def test_inputs_reach_the_script_only_through_the_environment( + self, + workflow: Path, + script: str, + job: str, + ) -> None: + for step in load(workflow)["jobs"][job]["steps"]: + assert "${{" not in step.get("run", "") + + def test_script_is_a_file_that_parses(self, workflow: Path, script: str, job: str) -> None: + result = subprocess.run( + [BASH, "-n", str(REPO_ROOT / "tools" / script)], text=True, capture_output=True + ) + assert result.returncode == 0, result.stderr + + def test_unknown_command_is_refused(self, workflow: Path, script: str, job: str) -> None: + result = subprocess.run( + [BASH, str(REPO_ROOT / "tools" / script), "bogus"], text=True, capture_output=True + ) + assert result.returncode == 2 + + +def test_cpp_inputs_and_exact_defaults() -> None: + inputs = inputs_of(CPP_WORKFLOW) + assert set(inputs) == { + "working-directory", + "std", + "compile-commands-dir", + "clang-format-version", + "clang-tidy-version", + "tools-ref", + } + assert inputs["std"]["default"] == "c++20" + assert inputs["compile-commands-dir"]["default"] == "" + assert inputs["clang-format-version"]["default"] == "23.1.3" + assert inputs["clang-tidy-version"]["default"] == "22.1.8" + + +def test_csharp_inputs_and_exact_sdk_default() -> None: + inputs = inputs_of(CSHARP_WORKFLOW) + assert set(inputs) == {"working-directory", "project", "dotnet-sdk-version", "tools-ref"} + assert inputs["dotnet-sdk-version"]["default"] == "10.0.401" + + +def test_cpp_script_and_workflow_do_not_mention_cppcheck() -> None: + for path in (CPP_SCRIPT, CPP_WORKFLOW): + assert "cppcheck" not in path.read_text(encoding="utf-8").lower(), path.name + + +# ----------------------------------------------------------------- helpers +def make_repo(tmp_path: Path) -> Path: + root = tmp_path / "repo" + root.mkdir() + subprocess.run(["git", "init", "-q"], cwd=root, check=True) + return root + + +def add_files(root: Path, *relative: str) -> None: + for name in relative: + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text("x\n", encoding="utf-8", newline="\n") + subprocess.run(["git", "add", "-A"], cwd=root, check=True) + + +def stub(directory: Path, name: str) -> Path: + path = directory / name + path.write_text( + "#!/usr/bin/env bash\n" + 'name="$(basename "$0")"\n' + '[ "$name" = dotnet ] && name="dotnet $1"\n' + 'echo "$name :: $*" >> "$STUB_LOG"\n' + 'case ",${STUB_FAIL}," in\n' + ' *",$name,"*) echo "src/a.cpp:3:1: stub finding from $name"; exit 1 ;;\n' + "esac\n" + "exit 0\n", + encoding="utf-8", + newline="\n", + ) + path.chmod(0o755) + return path + + +def run_script(script: Path, command: str, cwd: Path, tmp_path: Path, **env: str) -> Run: + log = tmp_path / "tools.log" + environment = dict(os.environ) + environment.update({"STUB_LOG": log.as_posix(), "STUB_FAIL": ""}) + environment.update(env) + result = subprocess.run( + [BASH, str(script), command], cwd=cwd, env=environment, text=True, capture_output=True + ) + calls = log.read_text(encoding="utf-8").splitlines() if log.exists() else [] + return result, calls + + +# --------------------------------------------------------------------- C++ +@pytest.fixture +def cpp_bin(tmp_path: Path) -> Path: + directory = tmp_path / "bin" + directory.mkdir() + for tool in ("clang-format", "clang-tidy"): + stub(directory, tool) + return directory + + +def cpp_env(cpp_bin: Path, **extra: str) -> dict[str, str]: + values = { + "WORKING_DIRECTORY": ".", + "STD": "c++20", + "COMPILE_COMMANDS_DIR": "", + "CLANG_FORMAT_VERSION": "23.1.3", + "CLANG_TIDY_VERSION": "22.1.8", + "QC_VENV": "/nonexistent", + "QC_BIN": cpp_bin.as_posix(), + } + values.update(extra) + return values + + +class TestCppValidate: + def run(self, tmp_path: Path, cpp_bin: Path, **extra: str) -> Run: + return run_script(CPP_SCRIPT, "validate", tmp_path, tmp_path, **cpp_env(cpp_bin, **extra)) + + def test_accepts_the_defaults(self, tmp_path: Path, cpp_bin: Path) -> None: + result, _ = self.run(tmp_path, cpp_bin) + assert result.returncode == 0, result.stdout + + @pytest.mark.parametrize("std", ["c++17", "c++23", "gnu++20"]) + def test_accepts_a_cpp_standard(self, tmp_path: Path, cpp_bin: Path, std: str) -> None: + assert self.run(tmp_path, cpp_bin, STD=std)[0].returncode == 0 + + @pytest.mark.parametrize("std", ["", "c17", "c++", "latest", "c++20; rm"]) + def test_rejects_a_value_that_is_not_a_cpp_standard( + self, + tmp_path: Path, + cpp_bin: Path, + std: str, + ) -> None: + result, _ = self.run(tmp_path, cpp_bin, STD=std) + assert result.returncode == 1 and "[configuration]" in result.stdout + + @pytest.mark.parametrize("directory", ["/etc", "..", "../x", "a/../..", ""]) + def test_rejects_a_working_directory_outside_the_repository( + self, + tmp_path: Path, + cpp_bin: Path, + directory: str, + ) -> None: + result, _ = self.run(tmp_path, cpp_bin, WORKING_DIRECTORY=directory) + assert result.returncode == 1 and "working-directory" in result.stdout + + @pytest.mark.parametrize("directory", ["/tmp/build", "../build"]) + def test_rejects_a_compile_commands_dir_outside_the_repository( + self, + tmp_path: Path, + cpp_bin: Path, + directory: str, + ) -> None: + result, _ = self.run(tmp_path, cpp_bin, COMPILE_COMMANDS_DIR=directory) + assert result.returncode == 1 and "compile-commands-dir" in result.stdout + + @pytest.mark.parametrize("name", ["CLANG_FORMAT_VERSION", "CLANG_TIDY_VERSION"]) + @pytest.mark.parametrize("version", ["", "latest", "23", ">=23", "23.1.3 --pre"]) + def test_rejects_a_version_that_is_not_exact( + self, + tmp_path: Path, + cpp_bin: Path, + name: str, + version: str, + ) -> None: + result, _ = self.run(tmp_path, cpp_bin, **{name: version}) + assert result.returncode == 1 and "exact version" in result.stdout + + +class TestCppRun: + @pytest.fixture + def repo(self, tmp_path: Path) -> Path: + root = make_repo(tmp_path) + add_files(root, ".clang-format") + return root + + def run(self, repo: Path, tmp_path: Path, cpp_bin: Path, **extra: str) -> Run: + return run_script(CPP_SCRIPT, "run", repo, tmp_path, **cpp_env(cpp_bin, **extra)) + + def test_runs_clang_format_once_and_clang_tidy_per_language( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "src/a.cpp", "src/a.hpp", "lib/c.c") + result, calls = self.run(repo, tmp_path, cpp_bin) + + assert result.returncode == 0, result.stdout + result.stderr + assert [c.split(" :: ")[0] for c in calls] == ["clang-format", "clang-tidy", "clang-tidy"] + assert "--dry-run -Werror" in calls[0] + assert "src/a.cpp" in calls[0] and "lib/c.c" in calls[0] + assert "-- -std=c++20" in calls[1] and "lib/c.c" not in calls[1] + assert "-- -std=c17" in calls[2] and "lib/c.c" in calls[2] + assert "All selected checks passed." in result.stdout + + def test_the_std_input_reaches_clang_tidy(self, repo: Path, tmp_path: Path, cpp_bin: Path) -> None: + add_files(repo, "a.cpp") + _, calls = self.run(repo, tmp_path, cpp_bin, STD="c++17") + assert "-- -std=c++17" in calls[1] + + def test_a_compile_database_is_used_instead_of_a_standard( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "a.cpp", "build/compile_commands.json") + result, calls = self.run(repo, tmp_path, cpp_bin, COMPILE_COMMANDS_DIR="build") + assert result.returncode == 0, result.stdout + assert len(calls) == 2 + assert "-p build" in calls[1] and "-std=" not in calls[1] + + def test_a_missing_compile_database_is_a_configuration_error( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "a.cpp") + result, calls = self.run(repo, tmp_path, cpp_bin, COMPILE_COMMANDS_DIR="build") + assert result.returncode == 1 and "compile_commands.json" in result.stdout + assert calls == [] + + def test_a_missing_clang_format_file_is_a_configuration_error( + self, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + repo = make_repo(tmp_path) + add_files(repo, "a.cpp") + result, calls = self.run(repo, tmp_path, cpp_bin) + assert result.returncode == 1 + assert "[configuration]" in result.stdout and ".clang-format" in result.stdout + assert calls == [] + + def test_a_clang_format_file_above_the_working_directory_is_found( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "service/a.cpp") + result, _ = run_script( + CPP_SCRIPT, + "run", + repo, + tmp_path, + **cpp_env(cpp_bin, WORKING_DIRECTORY="service"), + ) + assert result.returncode == 0, result.stdout + + def test_a_failing_tool_fails_the_job_and_the_other_still_runs( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "src/a.cpp") + result, calls = self.run(repo, tmp_path, cpp_bin, STUB_FAIL="clang-format") + assert result.returncode == 1 + assert len(calls) == 2, "clang-tidy runs after clang-format failed" + assert "::group::clang-format" in result.stdout + assert "src/a.cpp:3:1: stub finding from clang-format" in result.stdout + assert "FAIL clang-format" in result.stdout and "PASS clang-tidy (C++)" in result.stdout + assert "Quality checks failed: clang-format" in result.stdout + + def test_a_repository_without_c_or_cpp_files_passes_with_a_notice( + self, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + repo = make_repo(tmp_path) + add_files(repo, "README.md") + result, calls = self.run(repo, tmp_path, cpp_bin) + assert result.returncode == 0 and calls == [] + assert "No C or C++ files" in result.stdout + + def test_only_tracked_files_under_the_working_directory_are_checked( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + add_files(repo, "top.cpp", "service/api.cpp") + (repo / "service" / "untracked.cpp").write_text("x\n", encoding="utf-8") + _, calls = self.run(repo, tmp_path, cpp_bin, WORKING_DIRECTORY="service") + text = "\n".join(calls) + assert "api.cpp" in text and "top.cpp" not in text and "untracked" not in text + + def test_no_file_is_changed(self, repo: Path, tmp_path: Path, cpp_bin: Path) -> None: + add_files(repo, "a.cpp") + + def snapshot() -> dict[str, bytes]: + return {p.name: p.read_bytes() for p in repo.iterdir() if p.is_file()} + + before = snapshot() + self.run(repo, tmp_path, cpp_bin, STUB_FAIL="clang-tidy (C++)") + assert snapshot() == before + + def test_a_missing_working_directory_is_a_configuration_error( + self, + repo: Path, + tmp_path: Path, + cpp_bin: Path, + ) -> None: + result, _ = self.run(repo, tmp_path, cpp_bin, WORKING_DIRECTORY="nope") + assert result.returncode == 1 and "[configuration]" in result.stdout + + +def test_cpp_install_pins_both_tools_to_the_exact_versions(tmp_path: Path) -> None: + """`install` creates the venv and installs `==` versions; checked with a fake pip.""" + venv = tmp_path / "venv" + fake_python = tmp_path / "bin" + fake_python.mkdir() + log = tmp_path / "pip.log" + python3 = fake_python / "python3" + python3.write_text( + "#!/usr/bin/env bash\n" + 'mkdir -p "$3/bin"\n' + "printf '#!/usr/bin/env bash\\necho \"$*\" >> \"%s\"\\n' > \"$3/bin/pip\"\n" % log.as_posix() + + 'chmod +x "$3/bin/pip"\n', + encoding="utf-8", + newline="\n", + ) + python3.chmod(0o755) + environment = dict(os.environ) + environment["PATH"] = f"{fake_python.as_posix()}:{environment['PATH']}" + environment.update( + { + "QC_VENV": venv.as_posix(), + "CLANG_FORMAT_VERSION": "23.1.3", + "CLANG_TIDY_VERSION": "22.1.8", + } + ) + result = subprocess.run( + [BASH, str(CPP_SCRIPT), "install"], env=environment, text=True, capture_output=True + ) + assert result.returncode == 0, result.stdout + result.stderr + installed = log.read_text(encoding="utf-8") + assert "clang-format==23.1.3" in installed + assert "clang-tidy==22.1.8" in installed + + +# --------------------------------------------------------------------- C# +@pytest.fixture +def dotnet_stub(tmp_path: Path) -> Path: + directory = tmp_path / "dotnet-bin" + directory.mkdir() + return stub(directory, "dotnet") + + +def csharp_env(dotnet_stub: Path, tmp_path: Path, **extra: str) -> dict[str, str]: + values = { + "WORKING_DIRECTORY": ".", + "PROJECT": "", + "DOTNET_SDK_VERSION": "10.0.401", + "DOTNET_ROOT": (tmp_path / "dotnet-root").as_posix(), + "QC_DOTNET": dotnet_stub.as_posix(), + } + values.update(extra) + return values + + +class TestCsharpValidate: + def run(self, tmp_path: Path, dotnet_stub: Path, **extra: str) -> Run: + return run_script( + CSHARP_SCRIPT, "validate", tmp_path, tmp_path, **csharp_env(dotnet_stub, tmp_path, **extra) + ) + + @pytest.mark.parametrize("version", ["10.0.401", "10.0.100", "11.0.100", "100.1.2"]) + def test_accepts_an_exact_sdk_of_version_10_or_later( + self, + tmp_path: Path, + dotnet_stub: Path, + version: str, + ) -> None: + assert self.run(tmp_path, dotnet_stub, DOTNET_SDK_VERSION=version)[0].returncode == 0 + + @pytest.mark.parametrize( + "version", + ["", "8.0.425", "9.0.100", "10.0", "10", "10.0.x", "latest", "10.0.401-rc.1", "10.0.401 --verbose"], + ) + def test_rejects_an_old_or_inexact_sdk(self, tmp_path: Path, dotnet_stub: Path, version: str) -> None: + result, _ = self.run(tmp_path, dotnet_stub, DOTNET_SDK_VERSION=version) + assert result.returncode == 1 + assert "[configuration]" in result.stdout and "dotnet-sdk-version" in result.stdout + + @pytest.mark.parametrize("directory", ["/etc", "..", "../x", ""]) + def test_rejects_a_working_directory_outside_the_repository( + self, + tmp_path: Path, + dotnet_stub: Path, + directory: str, + ) -> None: + result, _ = self.run(tmp_path, dotnet_stub, WORKING_DIRECTORY=directory) + assert result.returncode == 1 and "working-directory" in result.stdout + + @pytest.mark.parametrize("project", ["/etc/x.sln", "../x.sln"]) + def test_rejects_a_project_outside_the_working_directory( + self, + tmp_path: Path, + dotnet_stub: Path, + project: str, + ) -> None: + result, _ = self.run(tmp_path, dotnet_stub, PROJECT=project) + assert result.returncode == 1 and "project" in result.stdout + + +class TestCsharpInstall: + def test_installs_the_exact_version_with_the_official_script( + self, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + root = tmp_path / "dotnet-root" + installer = tmp_path / "dotnet-install.sh" + installer.write_text( + "#!/usr/bin/env bash\n" + 'echo "$*" >> "$STUB_LOG"\n' + 'dir=""\n' + 'while [ "$#" -gt 0 ]; do [ "$1" = --install-dir ] && dir="$2"; shift; done\n' + 'mkdir -p "$dir"\n' + "printf '#!/usr/bin/env bash\\necho 10.0.401\\n' > \"$dir/dotnet\"\n" + 'chmod +x "$dir/dotnet"\n', + encoding="utf-8", + newline="\n", + ) + result, calls = run_script( + CSHARP_SCRIPT, + "install", + tmp_path, + tmp_path, + **csharp_env(dotnet_stub, tmp_path, QC_DOTNET_INSTALL_SCRIPT=installer.as_posix()), + ) + assert result.returncode == 0, result.stdout + result.stderr + assert calls and "--version 10.0.401" in calls[0] + assert f"--install-dir {root.as_posix()}" in calls[0] + assert "10.0.401" in result.stdout + + +class TestCsharpRun: + @pytest.fixture + def repo(self, tmp_path: Path) -> Path: + return make_repo(tmp_path) + + def run(self, repo: Path, tmp_path: Path, dotnet_stub: Path, **extra: str) -> Run: + return run_script( + CSHARP_SCRIPT, "run", repo, tmp_path, **csharp_env(dotnet_stub, tmp_path, **extra) + ) + + def test_runs_format_and_build_on_the_single_solution( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "App.sln", "src/App.csproj", "src/Program.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub) + + assert result.returncode == 0, result.stdout + result.stderr + assert [c.split(" :: ")[0] for c in calls] == ["dotnet format", "dotnet build"] + assert "App.sln" in calls[0] and "--verify-no-changes" in calls[0] + assert "App.sln" in calls[1] and "-warnaserror" in calls[1] + assert "All selected checks passed." in result.stdout + + def test_uses_the_single_project_when_there_is_no_solution( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "src/App.csproj", "src/Program.cs") + _, calls = self.run(repo, tmp_path, dotnet_stub) + assert "src/App.csproj" in calls[0] + + def test_several_projects_without_a_solution_need_the_project_input( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "a/A.csproj", "b/B.csproj", "a/P.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub) + assert result.returncode == 1 and calls == [] + assert "[configuration]" in result.stdout + assert "a/A.csproj" in result.stdout and "b/B.csproj" in result.stdout + + def test_the_project_input_picks_one(self, repo: Path, tmp_path: Path, dotnet_stub: Path) -> None: + add_files(repo, "a/A.csproj", "b/B.csproj", "a/P.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub, PROJECT="b/B.csproj") + assert result.returncode == 0 and "b/B.csproj" in calls[0] + + def test_a_missing_project_file_is_a_configuration_error( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "a/P.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub, PROJECT="nope.csproj") + assert result.returncode == 1 and "does not exist" in result.stdout and calls == [] + + def test_csharp_files_without_a_project_are_a_configuration_error( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "Program.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub) + assert result.returncode == 1 and calls == [] + assert "[configuration]" in result.stdout and "no solution or project" in result.stdout + + def test_a_repository_without_csharp_files_passes_with_a_notice( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "README.md") + result, calls = self.run(repo, tmp_path, dotnet_stub) + assert result.returncode == 0 and calls == [] + assert "No C# files" in result.stdout + + def test_a_failing_tool_fails_the_job_and_the_other_still_runs( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "App.sln", "Program.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub, STUB_FAIL="dotnet format") + assert result.returncode == 1 and len(calls) == 2 + assert "::group::dotnet format --verify-no-changes" in result.stdout + assert "FAIL dotnet format --verify-no-changes" in result.stdout + assert "PASS dotnet build -warnaserror" in result.stdout + assert "Quality checks failed: dotnet format --verify-no-changes" in result.stdout + + def test_only_files_under_the_working_directory_are_considered( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + add_files(repo, "Top.sln", "svc/Svc.csproj", "svc/P.cs", "Top.cs") + result, calls = self.run(repo, tmp_path, dotnet_stub, WORKING_DIRECTORY="svc") + assert result.returncode == 0 + assert "Svc.csproj" in calls[0] and "Top.sln" not in calls[0] + + def test_a_missing_working_directory_is_a_configuration_error( + self, + repo: Path, + tmp_path: Path, + dotnet_stub: Path, + ) -> None: + result, _ = self.run(repo, tmp_path, dotnet_stub, WORKING_DIRECTORY="nope") + assert result.returncode == 1 and "[configuration]" in result.stdout diff --git a/tools/quality-checks-cpp.sh b/tools/quality-checks-cpp.sh new file mode 100644 index 0000000..de4a85a --- /dev/null +++ b/tools/quality-checks-cpp.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# Steps of the reusable workflow .gitea/workflows/quality-checks-cpp.yml +# (ADR-0008). The workflow checks out the calling repository, fetches this file +# into shared-scripts/ and runs one command per step: +# +# quality-checks-cpp.sh validate check the inputs, before anything is installed +# quality-checks-cpp.sh install install the exact clang tool versions into a venv +# quality-checks-cpp.sh run run clang-format and clang-tidy on the tracked files +# +# Settings come from the environment, never from interpolated expressions: +# WORKING_DIRECTORY, STD, COMPILE_COMMANDS_DIR, CLANG_FORMAT_VERSION, +# CLANG_TIDY_VERSION, QC_VENV (and QC_BIN for tests). +# Every failure is printed as ::error::[category] message. + +C_STANDARD="${C_STANDARD:-c17}" + +validate_inputs() { + set -u + case "$WORKING_DIRECTORY" in + "" | /* | .. | ../* | */.. | */../*) + echo "::error::[configuration] working-directory must be a relative path inside the repository." + exit 1 + ;; + esac + if ! [[ "$STD" =~ ^(c\+\+|gnu\+\+)[0-9]{2}$ ]]; then + echo "::error::[configuration] std '$STD' is not a C++ standard such as c++20 or gnu++20." + exit 1 + fi + case "$COMPILE_COMMANDS_DIR" in + /* | .. | ../* | */.. | */../*) + echo "::error::[configuration] compile-commands-dir must be a relative path inside the repository." + exit 1 + ;; + esac + local name version + for name in CLANG_FORMAT_VERSION CLANG_TIDY_VERSION; do + version="${!name}" + if ! [[ "$version" =~ ^[0-9]+(\.[0-9]+){1,3}$ ]]; then + echo "::error::[configuration] ${name,,} '$version' is not an exact version such as 23.1.3." + exit 1 + fi + done + echo "Checking C and C++ in ${WORKING_DIRECTORY} with std ${STD}" +} + +install_tools() { + set -eu + python3 -m venv "$QC_VENV" + "$QC_VENV/bin/pip" install --quiet \ + "clang-format==${CLANG_FORMAT_VERSION}" "clang-tidy==${CLANG_TIDY_VERSION}" +} + +# A .clang-format must exist in the working directory or above it, up to the +# top of the repository: clang-format finds its style the same way. +find_clang_format_config() { + local top dir parent + top="$(cd "$(git rev-parse --show-toplevel)" && pwd -P)" + dir="$(pwd -P)" + while true; do + if [ -f "$dir/.clang-format" ] || [ -f "$dir/_clang-format" ]; then + return 0 + fi + parent="$(dirname "$dir")" + # stop at the top of the repository, or at the file system root + if [ "$dir" = "$top" ] || [ "$parent" = "$dir" ]; then + return 1 + fi + dir="$parent" + done +} + +run_checks() { + set -u + QC_BIN="${QC_BIN:-$QC_VENV/bin}" + if [ ! -d "$WORKING_DIRECTORY" ]; then + echo "::error::[configuration] working-directory '$WORKING_DIRECTORY' does not exist." + exit 1 + fi + cd "$WORKING_DIRECTORY" + + local all_files cpp_files c_files + mapfile -t all_files < <(git ls-files -- '*.c' '*.cc' '*.cpp' '*.cxx' '*.h' '*.hh' '*.hpp' '*.hxx') + if [ "${#all_files[@]}" -eq 0 ]; then + echo "::notice::No C or C++ files in '$WORKING_DIRECTORY'; nothing was checked." + echo "All selected checks passed." + exit 0 + fi + if ! find_clang_format_config; then + echo "::error::[configuration] No .clang-format found in '$WORKING_DIRECTORY' or above it; add the style of the project." + exit 1 + fi + if [ -n "$COMPILE_COMMANDS_DIR" ] && [ ! -f "$COMPILE_COMMANDS_DIR/compile_commands.json" ]; then + echo "::error::[configuration] compile-commands-dir '$COMPILE_COMMANDS_DIR' has no compile_commands.json; build it before the call." + exit 1 + fi + + # .c files are C; everything else (including headers) is checked as C++. + mapfile -t c_files < <(git ls-files -- '*.c') + mapfile -t cpp_files < <(git ls-files -- '*.cc' '*.cpp' '*.cxx' '*.h' '*.hh' '*.hpp' '*.hxx') + + local failed=() + run_tool() { + local name="$1" + shift + echo "::group::$name" + if "$@"; then + echo "::endgroup::" + echo "PASS $name" + else + echo "::endgroup::" + echo "FAIL $name" + failed+=("$name") + fi + } + + run_tool "clang-format" "$QC_BIN/clang-format" --dry-run -Werror "${all_files[@]}" + + if [ -n "$COMPILE_COMMANDS_DIR" ]; then + run_tool "clang-tidy" "$QC_BIN/clang-tidy" --quiet -p "$COMPILE_COMMANDS_DIR" "${all_files[@]}" + else + if [ "${#cpp_files[@]}" -gt 0 ]; then + run_tool "clang-tidy (C++)" "$QC_BIN/clang-tidy" --quiet "${cpp_files[@]}" -- "-std=${STD}" + fi + if [ "${#c_files[@]}" -gt 0 ]; then + run_tool "clang-tidy (C)" "$QC_BIN/clang-tidy" --quiet "${c_files[@]}" -- "-std=${C_STANDARD}" + fi + fi + + if [ "${#failed[@]}" -gt 0 ]; then + echo "::error::Quality checks failed: ${failed[*]}" + exit 1 + fi + echo "All selected checks passed." +} + +case "${1:-}" in + validate) validate_inputs ;; + install) install_tools ;; + run) run_checks ;; + *) + echo "::error::[configuration] usage: quality-checks-cpp.sh validate|install|run" >&2 + exit 2 + ;; +esac diff --git a/tools/quality-checks-csharp.sh b/tools/quality-checks-csharp.sh new file mode 100644 index 0000000..8f8e254 --- /dev/null +++ b/tools/quality-checks-csharp.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# Steps of the reusable workflow .gitea/workflows/quality-checks-csharp.yml +# (ADR-0008). The workflow checks out the calling repository, fetches this file +# into shared-scripts/ and runs one command per step: +# +# quality-checks-csharp.sh validate check the inputs, before anything is installed +# quality-checks-csharp.sh install install the exact .NET SDK version +# quality-checks-csharp.sh run run dotnet format and dotnet build on the project +# +# Settings come from the environment, never from interpolated expressions: +# WORKING_DIRECTORY, PROJECT, DOTNET_SDK_VERSION, DOTNET_ROOT (and +# QC_DOTNET_INSTALL_SCRIPT, QC_DOTNET for tests). +# Every failure is printed as ::error::[category] message. + +DOTNET_INSTALL_URL="https://dot.net/v1/dotnet-install.sh" + +validate_inputs() { + set -u + case "$WORKING_DIRECTORY" in + "" | /* | .. | ../* | */.. | */../*) + echo "::error::[configuration] working-directory must be a relative path inside the repository." + exit 1 + ;; + esac + case "$PROJECT" in + /* | .. | ../* | */.. | */../*) + echo "::error::[configuration] project must be a relative path inside the working directory." + exit 1 + ;; + esac + # An exact stable version of the .NET SDK, 10 or later (no channel, no range). + if ! [[ "$DOTNET_SDK_VERSION" =~ ^([1-9][0-9]+)\.[0-9]+\.[0-9]+$ ]] || [ "${BASH_REMATCH[1]}" -lt 10 ]; then + echo "::error::[configuration] dotnet-sdk-version '$DOTNET_SDK_VERSION' is not an exact stable version 10 or later, such as 10.0.401." + exit 1 + fi + echo "Checking C# in ${WORKING_DIRECTORY} with .NET SDK ${DOTNET_SDK_VERSION}" +} + +install_sdk() { + set -eu + local script="${QC_DOTNET_INSTALL_SCRIPT:-}" + if [ -z "$script" ]; then + script="$(mktemp)" + curl -fsSL "$DOTNET_INSTALL_URL" -o "$script" + fi + bash "$script" --version "$DOTNET_SDK_VERSION" --install-dir "$DOTNET_ROOT" --no-path + "$DOTNET_ROOT/dotnet" --version +} + +run_checks() { + set -u + if [ ! -d "$WORKING_DIRECTORY" ]; then + echo "::error::[configuration] working-directory '$WORKING_DIRECTORY' does not exist." + exit 1 + fi + cd "$WORKING_DIRECTORY" + local dotnet="${QC_DOTNET:-$DOTNET_ROOT/dotnet}" + + local sources + mapfile -t sources < <(git ls-files -- '*.cs') + if [ "${#sources[@]}" -eq 0 ]; then + echo "::notice::No C# files in '$WORKING_DIRECTORY'; nothing was checked." + echo "All selected checks passed." + exit 0 + fi + + local target="$PROJECT" candidates + if [ -z "$target" ]; then + mapfile -t candidates < <(git ls-files -- '*.sln' '*.slnx') + if [ "${#candidates[@]}" -eq 0 ]; then + mapfile -t candidates < <(git ls-files -- '*.csproj') + fi + case "${#candidates[@]}" in + 0) + echo "::error::[configuration] C# files exist but no solution or project was found in '$WORKING_DIRECTORY'; add one or set the input project." + exit 1 + ;; + 1) target="${candidates[0]}" ;; + *) + echo "::error::[configuration] Several solutions or projects were found (${candidates[*]}); set the input project." + exit 1 + ;; + esac + elif [ ! -f "$target" ]; then + echo "::error::[configuration] project '$target' does not exist in '$WORKING_DIRECTORY'." + exit 1 + fi + echo "Checking $target" + + export DOTNET_CLI_TELEMETRY_OPTOUT=1 DOTNET_NOLOGO=1 + local failed=() + run_tool() { + local name="$1" + shift + echo "::group::$name" + if "$@"; then + echo "::endgroup::" + echo "PASS $name" + else + echo "::endgroup::" + echo "FAIL $name" + failed+=("$name") + fi + } + + run_tool "dotnet format --verify-no-changes" "$dotnet" format "$target" --verify-no-changes + run_tool "dotnet build -warnaserror" "$dotnet" build "$target" --nologo -warnaserror + + if [ "${#failed[@]}" -gt 0 ]; then + echo "::error::Quality checks failed: ${failed[*]}" + exit 1 + fi + echo "All selected checks passed." +} + +case "${1:-}" in + validate) validate_inputs ;; + install) install_sdk ;; + run) run_checks ;; + *) + echo "::error::[configuration] usage: quality-checks-csharp.sh validate|install|run" >&2 + exit 2 + ;; +esac diff --git a/wiki/Home.da.md b/wiki/Home.da.md index 5f1d0d5..bac4073 100644 --- a/wiki/Home.da.md +++ b/wiki/Home.da.md @@ -1,4 +1,4 @@ - + > Dette er en oversættelse af [`Home`](Home). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. @@ -13,18 +13,20 @@ En samling af genbrugelige Gitea Actions-workflows til DevOps-professionelle og Dette repository er den centrale samling af genbrugelig automatisering til TirSystems repositories. Det primære repository ligger på `git.tirsystem.com` (Gitea) og spejles med push til GitHub. -Det indeholder seks workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki, fælles kvalitetskontrol, et udgivelsesworkflow, publicering af accepterede milepæle og repositoryets egen validering. De er listet med formål og diagram under [Workflows](#workflows). +Det indeholder otte workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki, fælles kvalitetskontrol for Python og shell, for C og C++ og for C#, et udgivelsesworkflow, publicering af accepterede milepæle og repositoryets egen validering. De er listet med formål og diagram under [Workflows](#workflows). ## Workflows -Der er seks workflows i dette repository. Et **scoped** workflow leveres af instansen til alle repositories i TirSystem-organisationerne og kræver intet i repositoryet; et **genbrugeligt** workflow kaldes fra et repository med `uses:`; de øvrige kører kun her. Diagrammerne er PlantUML-kildekode og er på engelsk. +Der er otte workflows i dette repository. Et **scoped** workflow leveres af instansen til alle repositories i TirSystem-organisationerne og kræver intet i repositoryet; et **genbrugeligt** workflow kaldes fra et repository med `uses:`; de øvrige kører kun her. Diagrammerne er PlantUML-kildekode og er på engelsk. | Workflow | Fil | Type | Starter ved | Formål | | --- | --- | --- | --- | --- | | Sync GitHub mirror metadata | [`sync-github-metadata.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/sync-github-metadata.yml) | Scoped | push til `main`, manuelt, dagligt kl. 03:17 UTC | Kopierer beskrivelsen og emnerne for et Gitea-repository til dets GitHub push-spejl | | Publish wiki | [`publish-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/publish-wiki.yml) | Scoped, tilmelding med `wiki/.publish` | push til `main`, der ændrer `wiki/**`, manuelt | Publicerer siderne i `wiki/` til repositoryets wiki | | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører ruff, mypy, shellcheck og shfmt på de sporede filer og ændrer intet | +| Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører clang-format og clang-tidy i præcise versioner på de sporede C- og C++-filer og ændrer intet | +| Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Kører `dotnet format` og `dotnet build` på et præcist .NET SDK 10 eller nyere og ændrer intet | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | Kun dette repository | manuelt | Opretter versionstagget, udgivelsessiden og major-tagget efter kontrollerne | | Publicering af accepterede milepæle | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | Kun dette repository | push til `main`, der ændrer `docs/milestones/**` eller `docs/sqa/reviews/**`, manuelt (tørkørsel som standard) | Opretter eller opdaterer Gitea-milepælen og issues for hver milepæl, der er Accepted med en Go-gennemgang | | Validate workflows | [`validate-workflows.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/validate-workflows.yml) | Kun dette repository | pull request, push til `main`, manuelt | Læser hvert workflow som YAML og kører offline-testene | @@ -185,6 +187,96 @@ deactivate Workflow @enduml ``` +### Kvalitetskontrol C++ + +Et genbrugeligt workflow til C og C++, kaldt med `uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1` (ADR-0008). Repositoryet skal have en `.clang-format`; en `.clang-tidy` og en compile-database er valgfrie. + +#### Workflow + +```plantuml +@startuml +title Shared quality checks for C and C++ (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-cpp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-cpp" as Workflow +participant "PyPI" as PyPI + +Caller -> Workflow: workflow_call with working-directory, std,\ncompile-commands-dir and tool versions +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or a version is not exact + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> PyPI: Install clang-format and clang-tidy at the exact versions +PyPI --> Workflow: Tools installed + +alt no C or C++ file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no .clang-format, or compile-commands-dir\nwithout compile_commands.json + Workflow --> Caller: Fail with [configuration] +else + Workflow -> Workflow: clang-format --dry-run -Werror on all files + Workflow -> Workflow: clang-tidy on the C++ files (-std=std)\nand on the C files (or with the compile database) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + +### Kvalitetskontrol C# + +Et genbrugeligt workflow til C#, kaldt med `uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1` (ADR-0008). Det installerer det præcise .NET SDK, som `dotnet-sdk-version` angiver (10 eller nyere), og skal bruge netværket til pakkegendannelsen. + +#### Workflow + +```plantuml +@startuml +title Shared quality checks for C# (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-csharp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-csharp" as Workflow +participant "dot.net\n(dotnet-install.sh)" as DotNet + +Caller -> Workflow: workflow_call with working-directory,\nproject and dotnet-sdk-version +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or the SDK is not an exact\nstable version 10 or later + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> DotNet: Install the exact .NET SDK version +DotNet --> Workflow: SDK installed + +alt no C# file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no solution or project, or several without the project input + Workflow --> Caller: Fail with [configuration] that names them +else + Workflow -> Workflow: dotnet format --verify-no-changes + Workflow -> Workflow: dotnet build -warnaserror\n(package restore needs the network) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + ### Udgivelse Startes manuelt af en vedligeholder; med `dry_run` `true` (standard) kontrollerer det kun (se [udgivelsesprocessen](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)). diff --git a/wiki/Home.md b/wiki/Home.md index fcc8c82..49afc80 100644 --- a/wiki/Home.md +++ b/wiki/Home.md @@ -13,18 +13,20 @@ A collection of reusable Gitea Actions workflows for DevOps professionals and so This repository is the central collection of reusable automation for TirSystem repositories. The primary repository lives at `git.tirsystem.com` (Gitea) and is push-mirrored to GitHub. -It contains six workflows: metadata sync to the GitHub mirror, wiki publishing, shared quality checks, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#workflows). +It contains eight workflows: metadata sync to the GitHub mirror, wiki publishing, shared quality checks for Python and shell, for C and C++ and for C#, a release workflow, the publishing of accepted milestones and the repository's own validation. They are listed with their purpose and a diagram under [Workflows](#workflows). ## Workflows -Six workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. +Eight workflows live in this repository. A **scoped** workflow is delivered by the instance to every repository of the TirSystem organizations and needs nothing in the repository; a **reusable** workflow is called by a repository with `uses:`; the others run only here. The diagrams are PlantUML source. | Workflow | File | Kind | Starts on | Purpose | | --- | --- | --- | --- | --- | | Sync GitHub mirror metadata | [`sync-github-metadata.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/sync-github-metadata.yml) | Scoped | push to `main`, manual, daily 03:17 UTC | Copies the description and topics of a Gitea repository to its GitHub push mirror | | Publish wiki | [`publish-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/publish-wiki.yml) | Scoped, opt-in with `wiki/.publish` | push to `main` that changes `wiki/**`, manual | Publishes the pages in `wiki/` to the repository wiki | | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Reusable (`workflow_call`) | a call from another repository | Runs ruff, mypy, shellcheck and shfmt on the tracked files and changes nothing | +| Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs clang-format and clang-tidy at exact versions on the tracked C and C++ files and changes nothing | +| Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Reusable (`workflow_call`) | a call from another repository | Runs `dotnet format` and `dotnet build` on an exact .NET SDK 10 or later and changes nothing | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | This repository only | manual | Creates the version tag, the release page and the major tag after the checks | | Publish accepted milestones | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | This repository only | push to `main` that changes `docs/milestones/**` or `docs/sqa/reviews/**`, manual (dry run by default) | Creates or updates the Gitea milestone and issues of every milestone that is Accepted with a Go review | | Validate workflows | [`validate-workflows.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/validate-workflows.yml) | This repository only | pull request, push to `main`, manual | Parses every workflow and runs the offline tests | @@ -185,6 +187,96 @@ deactivate Workflow @enduml ``` +### Quality checks C++ + +A reusable workflow for C and C++, called with `uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1` (ADR-0008). The repository needs a `.clang-format`; a `.clang-tidy` and a compile database are optional. + +#### Workflow + +```plantuml +@startuml +title Shared quality checks for C and C++ (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-cpp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-cpp" as Workflow +participant "PyPI" as PyPI + +Caller -> Workflow: workflow_call with working-directory, std,\ncompile-commands-dir and tool versions +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or a version is not exact + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> PyPI: Install clang-format and clang-tidy at the exact versions +PyPI --> Workflow: Tools installed + +alt no C or C++ file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no .clang-format, or compile-commands-dir\nwithout compile_commands.json + Workflow --> Caller: Fail with [configuration] +else + Workflow -> Workflow: clang-format --dry-run -Werror on all files + Workflow -> Workflow: clang-tidy on the C++ files (-std=std)\nand on the C files (or with the compile database) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + +### Quality checks C# + +A reusable workflow for C#, called with `uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1` (ADR-0008). It installs the exact .NET SDK given by `dotnet-sdk-version` (10 or later) and needs the network for the package restore. + +#### Workflow + +```plantuml +@startuml +title Shared quality checks for C# (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-csharp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-csharp" as Workflow +participant "dot.net\n(dotnet-install.sh)" as DotNet + +Caller -> Workflow: workflow_call with working-directory,\nproject and dotnet-sdk-version +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or the SDK is not an exact\nstable version 10 or later + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> DotNet: Install the exact .NET SDK version +DotNet --> Workflow: SDK installed + +alt no C# file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no solution or project, or several without the project input + Workflow --> Caller: Fail with [configuration] that names them +else + Workflow -> Workflow: dotnet format --verify-no-changes + Workflow -> Workflow: dotnet build -warnaserror\n(package restore needs the network) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + ### Release Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)). diff --git a/wiki/Home.ms.md b/wiki/Home.ms.md index 493769b..6c9d0dd 100644 --- a/wiki/Home.ms.md +++ b/wiki/Home.ms.md @@ -1,4 +1,4 @@ - + > Ini ialah terjemahan [`Home`](Home). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. @@ -13,18 +13,20 @@ Koleksi aliran kerja (workflow) Gitea Actions yang boleh digunakan semula untuk Repositori ini ialah koleksi pusat bagi automasi yang boleh digunakan semula untuk repositori TirSystem. Repositori utama berada di `git.tirsystem.com` (Gitea) dan dicerminkan dengan push ke GitHub. -Ia mengandungi enam aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki, semakan kualiti bersama, aliran kerja keluaran, penerbitan pencapaian (milestone) yang diterima dan pengesahan repositori sendiri. Ia disenaraikan dengan tujuan dan rajah di bawah [Aliran kerja](#aliran-kerja). +Ia mengandungi lapan aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki, semakan kualiti bersama untuk Python dan shell, untuk C dan C++ serta untuk C#, aliran kerja keluaran, penerbitan pencapaian (milestone) yang diterima dan pengesahan repositori sendiri. Ia disenaraikan dengan tujuan dan rajah di bawah [Aliran kerja](#aliran-kerja). ## Aliran kerja -Terdapat enam aliran kerja dalam repositori ini. Aliran kerja **scoped** dihantar oleh instans ke setiap repositori organisasi TirSystem dan tidak memerlukan apa-apa dalam repositori; aliran kerja **boleh guna semula** dipanggil oleh sebuah repositori dengan `uses:`; yang lain hanya berjalan di sini. Rajah ialah kod sumber PlantUML dan dalam bahasa Inggeris. +Terdapat lapan aliran kerja dalam repositori ini. Aliran kerja **scoped** dihantar oleh instans ke setiap repositori organisasi TirSystem dan tidak memerlukan apa-apa dalam repositori; aliran kerja **boleh guna semula** dipanggil oleh sebuah repositori dengan `uses:`; yang lain hanya berjalan di sini. Rajah ialah kod sumber PlantUML dan dalam bahasa Inggeris. | Aliran kerja | Fail | Jenis | Bermula apabila | Tujuan | | --- | --- | --- | --- | --- | | Sync GitHub mirror metadata | [`sync-github-metadata.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/sync-github-metadata.yml) | Scoped | push ke `main`, manual, setiap hari 03:17 UTC | Menyalin penerangan dan topik repositori Gitea ke push mirror GitHub-nya | | Publish wiki | [`publish-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/scoped_workflows/publish-wiki.yml) | Scoped, pilih menyertai dengan `wiki/.publish` | push ke `main` yang mengubah `wiki/**`, manual | Menerbitkan halaman dalam `wiki/` ke wiki repositori | | Quality checks | [`quality-checks.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan ruff, mypy, shellcheck dan shfmt pada fail yang dijejaki dan tidak mengubah apa-apa | +| Quality checks C++ | [`quality-checks-cpp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-cpp.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan clang-format dan clang-tidy pada versi tepat pada fail C dan C++ yang dijejaki dan tidak mengubah apa-apa | +| Quality checks C# | [`quality-checks-csharp.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/quality-checks-csharp.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Menjalankan `dotnet format` dan `dotnet build` pada .NET SDK 10 atau lebih baharu yang tepat dan tidak mengubah apa-apa | | Release | [`release.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/release.yml) | Repositori ini sahaja | manual | Mencipta tag versi, halaman keluaran dan tag major selepas semakan | | Menerbitkan milestone yang diterima | [`sync-milestones.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/sync-milestones.yml) | Repositori ini sahaja | push ke `main` yang mengubah `docs/milestones/**` atau `docs/sqa/reviews/**`, manual (larian percubaan secara lalai) | Mencipta atau mengemas kini milestone Gitea dan isu bagi setiap milestone yang Accepted dengan semakan Go | | Validate workflows | [`validate-workflows.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/validate-workflows.yml) | Repositori ini sahaja | pull request, push ke `main`, manual | Menghurai setiap aliran kerja sebagai YAML dan menjalankan ujian luar talian | @@ -185,6 +187,96 @@ deactivate Workflow @enduml ``` +### Semakan kualiti C++ + +Aliran kerja boleh guna semula untuk C dan C++, dipanggil dengan `uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1` (ADR-0008). Repositori mesti mempunyai `.clang-format`; `.clang-tidy` dan pangkalan data kompilasi adalah pilihan. + +#### Aliran kerja + +```plantuml +@startuml +title Shared quality checks for C and C++ (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-cpp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-cpp" as Workflow +participant "PyPI" as PyPI + +Caller -> Workflow: workflow_call with working-directory, std,\ncompile-commands-dir and tool versions +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or a version is not exact + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> PyPI: Install clang-format and clang-tidy at the exact versions +PyPI --> Workflow: Tools installed + +alt no C or C++ file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no .clang-format, or compile-commands-dir\nwithout compile_commands.json + Workflow --> Caller: Fail with [configuration] +else + Workflow -> Workflow: clang-format --dry-run -Werror on all files + Workflow -> Workflow: clang-tidy on the C++ files (-std=std)\nand on the C files (or with the compile database) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + +### Semakan kualiti C# + +Aliran kerja boleh guna semula untuk C#, dipanggil dengan `uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1` (ADR-0008). Ia memasang .NET SDK tepat yang diberikan oleh `dotnet-sdk-version` (10 atau lebih baharu) dan memerlukan rangkaian untuk pemulihan pakej. + +#### Aliran kerja + +```plantuml +@startuml +title Shared quality checks for C# (reusable workflow) + +autonumber + +actor "Calling repository\nuses: ...quality-checks-csharp.yml@v1" as Caller +participant "Gitea Actions\nquality-checks-csharp" as Workflow +participant "dot.net\n(dotnet-install.sh)" as DotNet + +Caller -> Workflow: workflow_call with working-directory,\nproject and dotnet-sdk-version +activate Workflow + +Workflow -> Workflow: Check out the caller and the shared script +Workflow -> Workflow: Validate the inputs +alt an input is invalid or the SDK is not an exact\nstable version 10 or later + Workflow --> Caller: Fail with [configuration] +end + +Workflow -> DotNet: Install the exact .NET SDK version +DotNet --> Workflow: SDK installed + +alt no C# file is tracked + Workflow --> Caller: Notice: nothing was checked, pass +else no solution or project, or several without the project input + Workflow --> Caller: Fail with [configuration] that names them +else + Workflow -> Workflow: dotnet format --verify-no-changes + Workflow -> Workflow: dotnet build -warnaserror\n(package restore needs the network) + alt any tool failed + Workflow --> Caller: Fail with tool, file and line in the log + else + Workflow --> Caller: All selected checks passed + end +end +deactivate Workflow +@enduml +``` + ### Keluaran Dimulakan secara manual oleh penyelenggara; dengan `dry_run` `true` (lalai) ia hanya menyemak (lihat [proses keluaran](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/release-process.md)).