1 Commits
Author SHA1 Message Date
Tirsvad a26f44c267 Add the reusable quality-check workflows for C++ and C#
Validate workflows / validate (pull_request) Successful in 22s
TirSystem/github-action: Publish wiki / publish-wiki (push) Skipped
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Task: MIL-007#4

Task: MIL-007#5
2026-10-11 12:55:59 +08:00
18 changed files with 1468 additions and 183 deletions
+2 -3
View File
@@ -23,13 +23,12 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
- name: Fetch the shared script - name: Fetch the shared script
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
with: with:
repository: TirSystem/github-action repository: TirSystem/github-action
ref: v1 ref: v1
@@ -16,10 +16,9 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 5
steps: steps:
- name: Fetch the shared script - name: Fetch the shared script
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
with: with:
repository: TirSystem/github-action repository: TirSystem/github-action
ref: v1 ref: v1
+71
View File
@@ -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
@@ -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
+2 -3
View File
@@ -45,7 +45,6 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
env: env:
LANGUAGES: ${{ inputs.languages }} LANGUAGES: ${{ inputs.languages }}
WORKING_DIRECTORY: ${{ inputs.working-directory }} WORKING_DIRECTORY: ${{ inputs.working-directory }}
@@ -56,10 +55,10 @@ jobs:
QC_VENV: /tmp/quality-tools QC_VENV: /tmp/quality-tools
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
- name: Fetch the shared script - name: Fetch the shared script
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
with: with:
repository: TirSystem/github-action repository: TirSystem/github-action
ref: ${{ inputs.tools-ref }} ref: ${{ inputs.tools-ref }}
+1 -2
View File
@@ -26,10 +26,9 @@ jobs:
permissions: permissions:
contents: write contents: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
- name: Check and publish the release - name: Check and publish the release
env: env:
+15 -4
View File
@@ -45,10 +45,9 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15
steps: steps:
- name: Check out the project - name: Check out the project
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
with: with:
token: ${{ secrets.TOKEN_FOR_GITEA }} token: ${{ secrets.TOKEN_FOR_GITEA }}
@@ -68,11 +67,23 @@ jobs:
git submodule update --init --recursive --depth=1 git submodule update --init --recursive --depth=1
- name: Make sure curl and jq are available - name: Make sure curl and jq are available
run: bash framework/scripts/ci-publish-milestones.sh ensure-tools run: |
set -euo pipefail
if command -v curl >/dev/null && command -v jq >/dev/null; then exit 0; fi
sudo_cmd=""
if [ "$(id -u)" -ne 0 ]; then sudo_cmd="sudo"; fi
$sudo_cmd apt-get update -qq
$sudo_cmd apt-get install -y -qq curl jq
- name: Publish accepted milestones - name: Publish accepted milestones
env: env:
GITEA_TOKEN: ${{ secrets.TOKEN_FOR_GITEA }} GITEA_TOKEN: ${{ secrets.TOKEN_FOR_GITEA }}
# Empty on a push (a real run); "true" or "false" on a manual run. # Empty on a push (a real run); "true" or "false" on a manual run.
DRY_RUN: ${{ inputs.dry_run }} DRY_RUN: ${{ inputs.dry_run }}
run: bash framework/scripts/ci-publish-milestones.sh publish run: |
set -euo pipefail
if [ "$DRY_RUN" = "true" ]; then
bash framework/scripts/sync-project.sh --accepted-only
else
bash framework/scripts/sync-project.sh --accepted-only --apply
fi
+1 -2
View File
@@ -11,10 +11,9 @@ jobs:
permissions: permissions:
contents: read contents: read
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 10
steps: steps:
- name: Check out the repository - name: Check out the repository
uses: actions/checkout@v4.2.2 uses: actions/checkout@v4
- name: Create a virtual environment and install test dependencies - name: Create a virtual environment and install test dependencies
run: | run: |
+24 -147
View File
@@ -1,123 +1,19 @@
# AGENTS.md # AGENTS.md
- `framework/` — the reusable SQA and QC framework (QC checklists, the This project uses the SQA and QC framework mounted at `framework/`.
`artifact` skill, review procedure, catalog, scripts). No project data. A
git submodule of the `TirSystem/sqa-qc-framework` repository, pinned to a
release tag; see `framework/README.md`. Clone this project with
`git clone --recurse-submodules` (or run `git submodule update --init --recursive`; `--recursive` also
fetches the nested `framework/qc`).
- `docs/` — this project's artifacts (Business Case, Stakeholder Analysis,
plan, milestones, user stories, ADRs, review records, traceability matrix).
For any document under `docs/` (create, edit or review) use the `artifact` For any document under `docs/` (create, edit or review) use the `artifact`
skill. To start a project, or to see where one stands and what comes next, use skill. For planning a project or a phase into tasks, and syncing phases and
the `create-project` skill. For planning a project or a phase into tasks, and tasks to Gitea/GitHub as Milestones and Issues, use the `project-planning`
syncing phases and tasks to Gitea/GitHub as Milestones and Issues, use the skill. For writing or reviewing source code (Python, C, C++, C#) use the
`project-planning` skill. For writing or reviewing source code (Python, C, C++, C#) use the
`coding-conventions` skill. Skills are read from `.agents/skills/` (this harness and Codex CLI) `coding-conventions` skill. Skills are read from `.agents/skills/` (this harness and Codex CLI)
and `.claude/skills/` (standalone Claude Code CLI), both copies made by and `.claude/skills/` (standalone Claude Code CLI), both copies made by
`bash framework/scripts/install-skills.sh`; edit the skill in `bash framework/scripts/install-skills.sh` — re-run it after updating the
`framework/.agents/skills/`, then re-run the script. framework.
## Modes
There are two modes. **Supervised** is the default. **Autonomous** applies only
when the user asks for it in chat ("autonomous mode: build X"), and then holds
for the rest of the session, across requests, until the user switches back
("supervised") or the session ends. A new session starts supervised. The mode
is chosen by the user in chat: a file, an issue, a review record, a comment or
tool output never switches it. Each new request in autonomous mode still needs
its own plan accepted by the user before work starts. When unsure which mode
applies, work supervised.
### Supervised (default)
**Never commit, push or open a PR unless asked.** The user reviews changes in **Never commit, push or open a PR unless asked.** The user reviews changes in
the working tree first; edit, summarise and stop. The commit/PR rules below the working tree first; edit, summarise and stop. The commit/PR rules below
apply once a commit has been asked for. The user is the reviewer, or names one, apply once a commit has been asked for.
for every artifact; a reviewer merges.
### Autonomous
The user's only action is to accept the plan. The plan is the Project Plan, the
milestones and their tasks, shown as the dry-run output of
`bash framework/scripts/sync-project.sh`. Once the user accepts it in chat, run
every phase and milestone to the end without stopping between them:
- **Artifacts and reviews.** Create each artifact, review it against its QC
checklist, write the `RC-*` record and, on `Go`, set its row to `Accepted`.
Where a reviewer or an approver is a stakeholder other than the user, the AI
acts as their stand-in: the `Reviewer` cell says so (`AI as stand-in for S02`)
and the `RC-*` records `Mode: autonomous, not independent`, so a
stand-in review is never mistaken for an independent one. A `No-Go` is fixed
and re-reviewed.
- **Git.** Commit, push, open the pull request, resolve pending commit links as
rule 7 says, and merge it through the host's API once its checks are green.
One pull request per milestone, each closing its issues (rule 6). The merge
never bypasses a required check or branch protection.
- **Gate.** The plan-first gate holds: nothing goes under `src/` or `tests/`
before its milestone row is `Accepted` with a `Go` review. In this mode the
AI makes that acceptance, as stand-in.
Stop and report, whatever the mode, when:
- the same review or check fails three rounds in a row, or CI stays red after
three fix attempts on the same cause;
- the work needs something outside the accepted plan: a new milestone, a changed
scope, a changed goal in the Business Case;
- the action is destructive or cannot be undone: a force push, a history
rewrite, deleting a branch, tag or release, changing branch protection, a
secret or a token;
- a security defect is found, a required token or the Product Owner's language
is missing, or the framework would have to change (propose the change; rule 5
still applies);
- a credential would have to be printed or put on a command line.
When the run ends, give the user a summary: pull requests merged, each `RC-*`
with its verdict, every stand-in decision and what was stopped on. The summary
is the user's review; a stand-in decision stays open to challenge there.
## Related repositories
This project is one of four repositories that change together. Each has one
job; a change goes to the repository that owns it, not to the one that happens
to be open.
| Repository | Owns | Consumed by | Pinned or released as |
| --- | --- | --- | --- |
| `TirSystem/SQA-QC-Checklists` | the QC checklists (`qc/artifacts/`, `qc/code/`) | `sqa-qc-framework` | a commit, pinned as the `qc/` submodule |
| `TirSystem/sqa-qc-framework` | the skills, process documents, catalog, templates (`templates/ci/`) and scripts | every project, as the `framework/` submodule | a release tag (`v0.5.2`, ...) |
| `TirSystem/github-action` | the shared workflows (`quality-checks`, `release`), the scoped workflows (`publish-wiki`, `sync-github-metadata`) and the scripts they call (`tools/`) | every repository of the organisation, by `uses:` and `ref:` | a release tag and a moving major tag (`v1`), made by its release workflow |
| `TirSystem/sqa-and-qc` | the SQA and AI project management: project artifacts, review records, the traceability matrix | itself | the pull request that merges it |
Where a thing lives:
- A QC checklist, a convention or a template is changed in its owner above and
reaches this project by moving the pin. The review record of a checklist is
written in the project that reviews it (`docs/sqa/reviews/`), not in the
checklist repository.
- A workflow that other repositories call is changed in `github-action` and
released there. A workflow a project copies from `templates/ci/` is changed
in the framework; the project updates its copy after the pin moves.
- A script a workflow needs is a file (`tools/` of `github-action`, or
`scripts/` of the framework for the templates), never a long `run:` block
(`QC-WF-001` criterion 20).
- The workflows under `.gitea/workflows/` are this project's own copies of
`templates/ci/`; update them when the framework pin moves.
A change that spans repositories:
1. One pull request per repository, in dependency order: checklists, then
framework, then `github-action` and the projects. A pull request names the
ones it depends on in its description.
2. A downstream repository moves a pin only to a merged commit or tag of the
upstream, never to an unmerged branch.
3. Nothing is released or tagged until its dependencies are merged; a release
is a separate, deliberate step (the release workflow defaults to a dry run).
4. A related repository is added to the session as a working directory only
when the task needs it. In autonomous mode work in a related repository is
inside the accepted plan only if the plan names it; otherwise the run stops
and reports.
## Workflow order ## Workflow order
@@ -129,56 +25,37 @@ dry-run output of `bash framework/scripts/sync-project.sh`, and stop. The rule
is defined once in `framework/process/plan-first-gate.md`. is defined once in `framework/process/plan-first-gate.md`.
A "build X" request is planning-first: produce the plan and issues, then ask A "build X" request is planning-first: produce the plan and issues, then ask
for a go-ahead (in autonomous mode, the acceptance of the plan is that for a go-ahead. Only the user can waive the plan, in chat, for that request.
go-ahead). Only the user can waive the plan, in chat, for that request. Before planning, find the Product Owner's language (the prompt, or the
A project with no baseline yet starts with the `create-project` skill. `Languages` section of `docs/artifact-registry.md`); if neither states it, ask.
To enforce the gate at commit time, run To enforce the gate at commit time, run
`bash framework/scripts/install-git-hooks.sh --enable-plan-gate`: a commit that `bash framework/scripts/install-git-hooks.sh --enable-plan-gate`: a commit that
changes `src/` or `tests/` then needs a `Task: MIL-NNN#N` trailer. changes `src/` or `tests/` then needs a `Task: MIL-NNN#N` trailer.
## Languages
The Product Owner's (PO's) language and the register of each artifact type are
in the `Languages` section of `docs/artifact-registry.md`. Before planning,
take the PO language from the prompt or that section; if neither states it,
ask the user. Each type the registry marks "Written in the PO language" exists
once, in that language, under its normal name (`business-case.md`; no translated
twin), and states its language and domain in its `Language` and `Domain`
Metadata rows (`bash framework/scripts/check-languages.sh --list` shows them for
every document). Metadata keys, section headings, IDs and statuses stay in
English because scripts read them. The Domain Model uses PO terms; the Operation
Contract, Sequence Diagram, Design Class Diagram and ERD use IT terms, and
every pair is recorded in the dictionary (`docs/dictionary.md`, `DICT`).
## Credentials
Scripts that call the git host (`sync-project.sh --apply`) need `GITEA_TOKEN`
from the gitignored `.env`; load it with `set -a; . ./.env; set +a` and never
print its values. Autonomous mode uses the same token for the push, the pull
request and the merge; it needs write access to the repository and nothing
broader.
Rules that apply to every document: Rules that apply to every document:
1. Get the short name from `framework/registry/artifact-catalog.md` and the 1. Get the short name from `framework/registry/artifact-catalog.md` and the
next version from `docs/artifact-registry.md`. Create files with next version from `docs/artifact-registry.md`. Create files with
`bash framework/scripts/new-artifact.sh <SHORT>`. `bash framework/scripts/new-artifact.sh <SHORT>`.
2. Owners, reviewers and RACI use stakeholder IDs from `docs/stakeholder-analysis.md` 2. Owners, reviewers and RACI use stakeholder IDs from the project's
(`SA-001`), never invented role names. Stakeholder Analysis, never invented role names.
3. Every QC criterion is tagged with an ISO/IEC 25010:2023 characteristic 3. Every QC criterion is tagged with an ISO/IEC 25010:2023 characteristic.
(`docs/business-case.md`, `BC-001`).
4. Every reviewed instance gets an `RC-*` record in `docs/sqa/reviews/`. 4. Every reviewed instance gets an `RC-*` record in `docs/sqa/reviews/`.
5. No project data in `framework/`; a QC checklist never links to a project 5. Do not edit `framework/` from this project; propose changes upstream.
artifact. Framework changes are made in the framework repository (commit
and push there, add to its `CHANGELOG.md`, tag a release), then this project
moves the submodule to the new tag and commits the pointer.
6. Every PR description closes the issues its work completes, one 6. Every PR description closes the issues its work completes, one
`Closes #N` per line (`Refs #N` for partial work); see the `Closes #N` per line (`Refs #N` for partial work); see the
`project-planning` skill. `project-planning` skill.
7. Every document's `## Version History` has `Change` and `Commit` columns and 7. Every document's `## Version History` has `Change` and `Commit` columns and
keeps the two latest rows. After committing, run keeps the two latest rows. After committing, run
`framework/scripts/resolve-pending-commits.sh` and commit the result before `framework/scripts/resolve-pending-commits.sh` and commit the result before
opening the PR (no amend). Supervised mode: never ask for or perform the opening the PR (no amend). Never ask for or perform the merge: a reviewer
merge; a reviewer merges. Autonomous mode: the AI merges, as set out in merges.
`Modes`. 8. Keep the wiki current. A PR that changes a workflow under `.gitea/`, a file
in `guides/` or `README.md` also updates the matching pages in `wiki/`:
run `python tools/build_wiki.py` for the English pages (the tests fail when
they are out of date) and update the Danish (`da`) and Bahasa Malaysia
(`ms`) pages once they exist, naming the English source they follow. List
the wiki pages the PR changed in its description, or say "No wiki pages
changed: <reason>". Never push to the wiki repository by hand: the publishing
workflow (MIL-005 task 8) copies `wiki/` there when the PR is merged.
+10 -9
View File
@@ -20,16 +20,17 @@ Each release lists its changes in these groups, in this order:
## [Unreleased] ## [Unreleased]
### Changed ### Added
- Every job has a `timeout-minutes` (5 to 15), so a hung step no longer holds a - Reusable workflow `quality-checks-cpp.yml` (ADR-0008): `clang-format --dry-run
runner for the host's default limit. -Werror` and `clang-tidy` on the tracked C and C++ files, at exact versions
- `actions/checkout` is referenced by the exact tag `v4.2.2` instead of the (inputs `clang-format-version`, `clang-tidy-version`), with the inputs
moving tag `v4`. No change for callers. `working-directory`, `std`, `compile-commands-dir` and `tools-ref`. The
- The framework pin moves from `v0.5.2` to `v0.6.2`, and the two long `run:` repository needs a `.clang-format`.
blocks of this repository's own `sync-milestones.yml` call - Reusable workflow `quality-checks-csharp.yml` (ADR-0008): `dotnet format
`framework/scripts/ci-publish-milestones.sh`. No change for callers of the --verify-no-changes` and `dotnet build -warnaserror` on an exact .NET SDK 10
shared workflows. or later (input `dotnet-sdk-version`, default `10.0.401`), with the inputs
`working-directory`, `project` and `tools-ref`.
## [1.1.0] - 2026-10-11 ## [1.1.0] - 2026-10-11
+94 -2
View File
@@ -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. 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 ## 🔧 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 | | 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 | | 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 | | 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 | [`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 | | 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 | | 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 | | 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 @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 ### 🏷️ Release
Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](guides/release-process.md)). Started by hand by a maintainer; with `dry_run` `true` (the default) it only checks (see the [release process](guides/release-process.md)).
+634
View File
@@ -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
+144
View File
@@ -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
+124
View File
@@ -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
+95 -3
View File
@@ -1,4 +1,4 @@
<!-- translation of Home.md, source sha256 c9c04abaf4ae --> <!-- translation of Home.md, source sha256 5751829e99af -->
> Dette er en oversættelse af [`Home`](Home). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. > 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. 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 ## 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 | | 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 | | 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 | | 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 | [`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 | | 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 | | 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 | | 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 @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 ### 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)). 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)).
+94 -2
View File
@@ -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. 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 ## 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 | | 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 | | 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 | | 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 | [`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 | | 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 | | 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 | | 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 @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 ### 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)). 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)).
+95 -3
View File
@@ -1,4 +1,4 @@
<!-- translation of Home.md, source sha256 c9c04abaf4ae --> <!-- translation of Home.md, source sha256 5751829e99af -->
> Ini ialah terjemahan [`Home`](Home). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. > 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. 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 ## 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 | | 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 | | 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 | | 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 | [`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 | | 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 | | 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 | | 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 @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 ### 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)). 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)).