39 Commits
Author SHA1 Message Date
Tirsvad 8ed0d0203e Merge pull request 'Add workflow adoption rules and move the framework to v0.16.1' (#217) from TirSystem/workflow-rules-sqa-qc-d0d86e into main
Validate workflows / validate (push) Failing after 1m8s
TirSystem/git-workflows: Sync GitHub mirror metadata / sync-metadata (push) Failing after 7s
Reviewed-on: #217
2026-10-26 15:27:32 +01:00
Tirsvad 80eec4e9db Adopt framework v0.16.1
Validate workflows / validate (pull_request) Failing after 1m15s
2026-10-26 22:24:35 +08:00
Tirsvad acc358a509 Add adoption rules: when a repository uses each workflow in SQA and QC 2026-10-26 22:18:33 +08:00
Tirsvad e33b9977a3 Merge pull request 'Prepare release 2.2.0: changelog section and guides at @v2' (#216) from release-2.2.0 into main
Publish wiki / publish-wiki (push) Successful in 12s
Publish own wiki / wiki (push) Successful in 13s
Validate workflows / validate (push) Successful in 57s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
2026-10-21 13:40:33 +02:00
Tirsvad 01178370cb Prepare release 2.2.0: changelog section and guides at @v2
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 58s
The unreleased entries become the 2.2.0 section (the English-comments workflow
and the WireGuard and Tailscale actions). The guides no longer say to call
@main until a release: the examples use @v2, and the VPN guides state that the
actions are released but have not run on a real tunnel yet.

Refs #188
Refs #191
Refs #207
2026-10-21 19:39:23 +08:00
Tirsvad 8017bc6b39 Merge pull request 'Move the framework submodule from v0.7.0 to v0.14.0' (#215) from update-framework-v0.14.0 into main
Validate workflows / validate (push) Successful in 1m11s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 7s
2026-10-21 12:30:34 +02:00
Tirsvad a772088730 Merge pull request 'Fix three defects of the English-comments check found in the trial (task 8)' (#212) from mil-013-judge-each-line into main
Validate workflows / validate (push) Successful in 56s
Publish wiki / publish-wiki (push) Successful in 15s
Publish own wiki / wiki (push) Successful in 15s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: TirSystem/github-action#212
2026-10-21 05:47:53 +02:00
Tirsvad 35f3c4301f Merge pull request 'Record the MIL-013 results: the trial on the instance, RC-085' (#214) from mil-013-task-8-results into main
Publish accepted milestones / publish (push) Successful in 1m13s
Validate workflows / validate (push) Successful in 51s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: TirSystem/github-action#214
2026-10-21 05:47:40 +02:00
Tirsvad b8d5bae3ae Resolve pending commit links for the MIL-013 results
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 1m5s
2026-10-21 11:00:16 +08:00
Tirsvad 88f852af93 Record the MIL-013 results: the trial on the instance and RC-085
Adds the verification results of tasks 1 to 8, 10 and 13 to MIL-013: the three
defects the trial found (a Danish line beside an English one, the skipped
author step, the run link), the runs, and the search of the logs for
credentials. RC-085 reviews the results; the milestone is Accepted again.

Refs #204
Refs #210
Task: MIL-013#8
Task: MIL-013#14
2026-10-21 11:00:15 +08:00
Tirsvad 39bcaa5bac Link the run by its id in the comment on the pull request
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 1m2s
The trial showed that the web address of a run on the instance holds the id of
the run, not its number, so the link in the comment pointed to another run.

Refs #201
Refs #204
Task: MIL-013#5
2026-10-21 10:29:20 +08:00
Tirsvad 3d46c4b60a Tell the author in a called workflow: ask for the pull request, not the event
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 1m2s
The trial on the instance showed that gitea.event_name is workflow_call inside
a called workflow, so the step that comments on the pull request was skipped.
The pull request number, its author and the base ref are available there, and
the step now runs when the number is set.

Refs #201
Refs #204
Task: MIL-013#5
2026-10-21 10:26:13 +08:00
Tirsvad 198830588c Judge each line of a comment block, then the block
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 1m0s
The trial of the English-comments check found that a Danish comment line next
to an English one was missed: the joined block counted as English. A block of
several lines is now judged line by line first (at least four words and a
probability of 0.7), and as a whole when no line is flagged. Found and fixed
while preparing MIL-013 task 8; the guide, the wiki and ADR-0012 say so.

Refs #200
Refs #204
Task: MIL-013#4
Task: MIL-013#10
2026-10-21 10:18:30 +08:00
Tirsvad 98a79c7b62 Move the framework submodule from v0.7.0 to v0.14.0
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 59s
2026-10-21 10:15:26 +08:00
Tirsvad eeaf6f5361 Merge pull request 'Add the English-comments check: corpus, ADR-0012, detector, report and reusable workflow' (#211) from mil-013-english-comments into main
Validate workflows / validate (push) Successful in 50s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Publish wiki / publish-wiki (push) Successful in 21s
Publish own wiki / wiki (push) Successful in 21s
Reviewed-on: TirSystem/github-action#211
2026-10-19 06:48:53 +02:00
Tirsvad 57d9b63b3c Document the English-comments workflow: guide, README, wiki in three languages
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 57s
Adds the guide, the README section with its diagram, the wiki page in English,
Danish and Bahasa Malaysia, the Home and Capabilities rows, the sidebar entry
and the changelog entry under Unreleased.

Refs #206
Task: MIL-013#10
2026-10-19 12:34:15 +08:00
Tirsvad a767ded5e1 Add the reporting step and the reusable english-comments workflow
tools/english_comments_report.py keeps one comment on the pull request that
mentions the author, updates it on a re-run and marks it resolved when the
check passes. english-comments.yml runs the check on the full history, tells
the author even when the check fails, and then fails the run. A script that no
release holds yet no longer counts as missing from the default tools-ref.

Refs #201
Refs #202
Task: MIL-013#5
Task: MIL-013#6
2026-10-19 12:34:07 +08:00
Tirsvad b8e63d0a29 Add the comment extractor and the language detector of the English-comments check
tools/english_comments.py reads the comments and docstrings of the languages of
ADR-0010 (the scanner of count_loc.py now returns the comment text), joins the
lines of a block, removes everything that is not a word and judges the block
with langid (ADR-0012). It checks the lines a branch adds, or every tracked
file, honours an ignore marker and an allowed-words file, and prints each
finding as an annotation. The rates of the corpus are tests.

Refs #199
Refs #200
Refs #203
Task: MIL-013#3
Task: MIL-013#4
2026-10-19 12:34:00 +08:00
Tirsvad 7d8cc4522e Choose the detection method for the English-comments check (ADR-0012)
Compares a rule-based method, langdetect, langid and lingua on the corpus.
langid with a pre-filter and a 0.6 threshold flags 0 of 300 English comments,
44/45 Danish and 41/45 Malay. The comparison script is kept next to the corpus.

Refs #198
Task: MIL-013#2
2026-10-19 12:21:23 +08:00
Tirsvad 0603c61602 Add the labelled comment corpus for the English-comments check
300 English comments from this repository and 138 comments in Danish, Bahasa
Malaysia, German, Swedish, Norwegian and French, written for the purpose and
not checked by a native reader. The detection method is measured on it.

Refs #197
Task: MIL-013#1
2026-10-19 12:21:22 +08:00
Tirsvad 51af1466e0 Merge pull request 'Plan MIL-013: source code comments are in English' (#196) from mil-013-english-comments-plan into main
Publish accepted milestones / publish (push) Successful in 1m31s
Validate workflows / validate (push) Successful in 44s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: TirSystem/github-action#196
2026-10-19 06:16:52 +02:00
Tirsvad ff292e2938 Resolve pending commit links for MIL-013
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 39s
2026-10-19 12:15:13 +08:00
Tirsvad e35b1c5674 Plan MIL-013: source code comments are in English
Adds MIL-013 (accepted, RC-083), objective O7 to the Business Case (RC-084),
the schedule row, risk and dependency to the Project Plan, and the
traceability rows. No code is added by this commit.
2026-10-19 12:15:10 +08:00
Tirsvad 5dedb0ee9b Merge pull request 'Fix the review findings of the VPN actions, lint them in CI, record MIL-012 results' (#195) from mil-012-review-fixes-and-docs into main
Publish accepted milestones / publish (push) Successful in 59s
Validate workflows / validate (push) Successful in 38s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: TirSystem/github-action#195
2026-10-19 06:08:11 +02:00
Tirsvad b75ed03d3f Resolve pending commit links for MIL-012 results
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 40s
2026-10-19 12:01:24 +08:00
Tirsvad 3325bea772 Record the MIL-012 results of tasks 1 and 6, ADR-0011 and RC-082
Task 1: both forms of uses: resolve a subfolder action at main. Adds ADR-0011
(Proposed, not yet reviewed), the review record RC-082 for the milestone
results, and the traceability rows.

Refs #182
Refs #190
2026-10-19 12:01:18 +08:00
Tirsvad a91e0233c1 Lint the shell scripts of the actions in the validation run
tools/lint-action-scripts.sh runs shellcheck and shfmt on actions/**/*.sh with
pinned tools; validate-workflows.yml runs it, and .editorconfig sets the shell
format. The tests show a shellcheck problem and a badly formatted script fail.

Refs #187
Task: MIL-012#6
2026-10-19 12:01:11 +08:00
Tirsvad 7a30560a74 Fix the review findings in connect-tailscale
tailscaled is waited for before tailscale up, a tailscaled that already runs on
the runner is not taken over and down leaves a daemon it did not start, sudo is
used only when the job is not root, an exact version is required to install
(with an optional sha256 of the archive), and the guide states when the node is
ephemeral and calls @main until the release.

Refs #185
Task: MIL-012#4
2026-10-19 12:01:06 +08:00
Tirsvad 9e1cfa5fa8 Fix the review findings in connect-wireguard
The wireguard-go choice now survives sudo (it is passed through env), sudo is
used only when the job is not root, a configuration field with a line break is
refused, only the key values of a full configuration are masked, the key file
is removed when wg-quick fails, and the guide calls @main until the release.

Refs #184
Task: MIL-012#3
2026-10-19 12:01:00 +08:00
Tirsvad f0d9d79f4c Merge pull request 'Add the connect-wireguard and connect-tailscale actions (MIL-012, unverified on a runner)' (#194) from wireguard-vpn-action into main
Validate workflows / validate (push) Successful in 36s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: TirSystem/github-action#194
2026-10-19 05:27:01 +02:00
Tirsvad 2689264500 Note the two VPN actions in the changelog under Unreleased
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 39s
Refs #188
Task: MIL-012#7
2026-10-19 11:24:39 +08:00
Tirsvad 8eaba331ec Add the connect-tailscale composite action for Gitea (not yet verified on a runner)
actions/connect-tailscale joins a Tailscale network for a deployment job, with
a guide and offline tests. Whether tailscaled survives to the next step is
still open (MIL-012 task 4).

Refs #185
Task: MIL-012#4
2026-10-19 11:24:36 +08:00
Tirsvad fce3cf8c6b Add the connect-wireguard composite action for Gitea (not yet verified on a runner)
actions/connect-wireguard brings a WireGuard tunnel up for a deployment job and
takes it down again, with a guide and offline tests. The runs on a real runner
(MIL-012 task 3) are still to do.

Refs #184
Task: MIL-012#3
2026-10-19 11:24:33 +08:00
Tirsvad 54e83770a5 Merge pull request 'Plan MIL-012: VPN actions for deployment (WireGuard and Tailscale)' (#181) from wireguard-vpn-action into main
Publish accepted milestones / publish (push) Successful in 1m20s
Validate workflows / validate (push) Successful in 34s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: TirSystem/github-action#181
2026-10-19 04:49:02 +02:00
Tirsvad 8e093821f1 Resolve pending commit links for MIL-012
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 30s
2026-10-19 10:38:02 +08:00
Tirsvad c5d5941869 Plan MIL-012: VPN actions for deployment (WireGuard and Tailscale)
Adds MIL-012 (accepted, RC-080), objective O6 to the Business Case (RC-081),
the schedule row, risk and dependency to the Project Plan, and the
traceability rows. The action code on this branch is not part of this commit.
2026-10-19 10:37:56 +08:00
Tirsvad 241c26432b Merge pull request 'MIL-011 outcome: release 2.1.1, the framework page and RC-079' (#180) from mil-011-close into main
Publish accepted milestones / publish (push) Successful in 53s
Validate workflows / validate (push) Successful in 31s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: TirSystem/github-action#180
2026-10-14 11:56:41 +02:00
Tirsvad 251e18dfc3 Resolve pending commit links for RC-079
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Skipped
Validate workflows / validate (pull_request) Successful in 36s
2026-10-14 17:30:49 +08:00
Tirsvad 4aa3fd38f9 MIL-011 outcome: release 2.1.1 and the framework page, RC-079
The Release workflow created v2.1.1 and moved v2 (v1 unchanged); a call
at @v2 with the defaults ran after the release (run 1526). The
framework Badges page has no tools-ref workaround (framework pull
request 57). RC-079: Go, all eight criteria pass.
2026-10-14 17:30:48 +08:00
57 changed files with 4782 additions and 44 deletions
+5
View File
@@ -5,3 +5,8 @@ root = true
end_of_line = lf end_of_line = lf
charset = utf-8 charset = utf-8
insert_final_newline = true insert_final_newline = true
[*.sh]
indent_style = space
indent_size = 2
switch_case_indent = true
+117
View File
@@ -0,0 +1,117 @@
name: English comments
# Reusable workflow (MIL-013, ADR-0012). A repository calls it on its pull
# requests with
#
# on:
# pull_request:
# jobs:
# english-comments:
# uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
#
# It reads the comments and docstrings that the pull request adds, judges their
# language, fails the run when one is not English and writes one comment on the
# pull request that mentions the author. It changes no file. Mark the check as
# required in the branch protection of the target branch to block the merge
# (the workflow cannot do that).
on:
workflow_call:
inputs:
scope:
description: "changed: the comments on the lines the pull request adds; all: every tracked file"
type: string
default: "changed"
exclude:
description: "Comma list of file name or path patterns that are not checked (* and ** are wildcards)"
type: string
default: ""
allowed-words-file:
description: "File of the repository with words that are never judged, one per line"
type: string
default: ""
threshold:
description: "Probability, above 0 and at most 1, from which a comment counts as not English (ADR-0012)"
type: string
default: "0.6"
min-words:
description: "A comment with fewer words is not judged (ADR-0012)"
type: string
default: "3"
langid-version:
description: "Version of the langid package"
type: string
default: "1.1.6"
numpy-version:
description: "Version of numpy, which langid needs"
type: string
default: "2.1.3"
tools-ref:
description: "Tag or branch of TirSystem/github-action whose tools/english_comments.py is run; pin it to the exact tag together with the workflow"
type: string
default: "v2"
secrets:
REPORT_TOKEN:
description: "Optional Gitea token that may comment on pull requests; without it the token of the run is used"
required: false
jobs:
english-comments:
permissions:
contents: read
issues: write
pull-requests: write
runs-on: ubuntu-latest
timeout-minutes: 15
env:
SCOPE: ${{ inputs.scope }}
EXCLUDE: ${{ inputs.exclude }}
ALLOWED_WORDS: ${{ inputs.allowed-words-file }}
THRESHOLD: ${{ inputs.threshold }}
MIN_WORDS: ${{ inputs.min-words }}
LANGID_VERSION: ${{ inputs.langid-version }}
NUMPY_VERSION: ${{ inputs.numpy-version }}
BASE_REF: ${{ gitea.base_ref }}
FINDINGS_FILE: /tmp/english-comments-findings.json
steps:
- name: Check out the repository
uses: actions/checkout@v4.2.2
with:
fetch-depth: 0
- name: Fetch the shared scripts
uses: actions/checkout@v4.2.2
with:
repository: TirSystem/github-action
ref: ${{ inputs.tools-ref }}
path: shared-scripts
- name: Install the language detector
run: |
python3 -m venv /tmp/english-comments-venv
/tmp/english-comments-venv/bin/pip install --quiet "langid==${LANGID_VERSION}" "numpy==${NUMPY_VERSION}"
- name: Check the comments
id: check
continue-on-error: true
run: >-
/tmp/english-comments-venv/bin/python shared-scripts/tools/english_comments.py check
--scope "$SCOPE" --base "origin/$BASE_REF" --exclude "shared-scripts/**,$EXCLUDE"
--allowed-words "$ALLOWED_WORDS" --threshold "$THRESHOLD" --min-words "$MIN_WORDS"
--output "$FINDINGS_FILE"
- name: Tell the author
# in a called workflow gitea.event_name is workflow_call, so ask for the pull request itself
if: ${{ always() && gitea.event.pull_request.number != '' }}
continue-on-error: true
env:
GITEA_API_URL: ${{ gitea.api_url }}
SOURCE_REPOSITORY: ${{ gitea.repository }}
PR_NUMBER: ${{ gitea.event.pull_request.number }}
PR_AUTHOR: ${{ gitea.event.pull_request.user.login }}
RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}
REPORT_TOKEN: ${{ secrets.REPORT_TOKEN || gitea.token }}
run: python3 shared-scripts/tools/english_comments_report.py
- name: Fail the run when a comment is not in English
if: ${{ steps.check.outcome == 'failure' }}
run: exit 1
+6
View File
@@ -26,3 +26,9 @@ jobs:
- name: Check the coverage of the tools - name: Check the coverage of the tools
run: .venv/bin/python -m coverage report run: .venv/bin/python -m coverage report
- name: Lint the shell scripts of the actions
env:
SHELLCHECK: .venv/bin/shellcheck
SHFMT: .venv/bin/shfmt
run: bash tools/lint-action-scripts.sh actions
+17
View File
@@ -119,6 +119,23 @@ A change that spans repositories:
inside the accepted plan only if the plan names it; otherwise the run stops inside the accepted plan only if the plan names it; otherwise the run stops
and reports. and reports.
## Workflow adoption rules
When a workflow is added to this repository (`.gitea/workflows/`,
`.gitea/scoped_workflows/`) or a composite action to `actions/`, the same pull
request adds its rule for when a repository uses it in SQA and QC:
1. A row in `guides/adoption-rules.md` with the columns *Use when*, *Do not use
when* and *What it gives SQA and QC*. The condition is checkable from the
repository itself (a file type, a test report, a wiki folder), never
"when useful".
2. The workflow's guide and its README section link to that row, and the
README workflow table and count stay in step.
3. A workflow that applies to every repository (scoped) or only to this one
says so in the row instead of leaving it empty.
4. The rule is changed in the same pull request as the workflow when the
workflow's behaviour changes; the CHANGELOG entry names it.
## Workflow order ## Workflow order
Business Case, Stakeholder Analysis, Project Plan, milestones, tasks synced as Business Case, Stakeholder Analysis, Project Plan, milestones, tasks synced as
+12
View File
@@ -20,6 +20,18 @@ Each release lists its changes in these groups, in this order:
## [Unreleased] ## [Unreleased]
### Added
- The [adoption rules](guides/adoption-rules.md): for each workflow, when a repository uses it in SQA and QC. A new workflow adds its rule in the same pull request (`AGENTS.md`).
## [2.2.0] - 2026-10-21
### Added
- `english-comments.yml`, a reusable workflow that judges the language of the comments and docstrings a pull request adds, fails the run when one is not English and writes one comment on the pull request that mentions the author (ADR-0012), with the scripts `tools/english_comments.py` and `tools/english_comments_report.py`, the guide `guides/english-comments.md` and wiki pages in three languages. The scanner of `tools/count_loc.py` can return the text of the comments it finds.
- `actions/connect-wireguard`, a composite action that brings a WireGuard tunnel up on a Gitea Actions runner for a deployment, with the guide `guides/vpn-wireguard.md`. Experimental.
- `actions/connect-tailscale`, a composite action for Gitea Actions that joins a Tailscale network for a deployment (the Gitea counterpart of the marketplace action `tailscale/github-action`), with the guide `guides/vpn-tailscale.md`. Experimental.
## [2.1.1] - 2026-10-14 ## [2.1.1] - 2026-10-14
### Fixed ### Fixed
+46 -2
View File
@@ -17,11 +17,13 @@ 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 ten workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, 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 eleven workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, a check that comments are in English, 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
Ten 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. When a repository should use each workflow in its SQA and QC is set out in the [adoption rules](guides/adoption-rules.md).
Eleven 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 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
@@ -31,6 +33,7 @@ Ten workflows live in this repository. A **scoped** workflow is delivered by the
| 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-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 | | 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 |
| Code metrics | [`code-metrics.yml`](.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch | | Code metrics | [`code-metrics.yml`](.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch |
| English comments | [`english-comments.yml`](.gitea/workflows/english-comments.yml) | Reusable (`workflow_call`) | a call from another repository, on a pull request | Judges the language of the comments and docstrings a pull request adds, fails the run when one is not English and writes one comment on the pull request that mentions the author |
| Publish own wiki | [`publish-own-wiki.yml`](.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository | | Publish own wiki | [`publish-own-wiki.yml`](.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository |
| 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 |
@@ -282,6 +285,46 @@ deactivate Workflow
@enduml @enduml
``` ```
### 🔤 English comments
A reusable workflow that checks that the comments and docstrings a pull request adds are written in English, fails the run when one is not, and writes one comment on the pull request that mentions the author. It changes no file and sends no source text outside the instance; marking the check as required in the branch protection is what blocks the merge ([guide](guides/english-comments.md), ADR-0012).
#### 🔀 Workflow
```plantuml
@startuml
title English comments check (reusable workflow)
autonumber
actor "Pull request
uses: ...english-comments.yml@v2" as Caller
participant "Gitea Actions
english-comments" as Workflow
participant "Gitea REST API" as Gitea
Caller -> Workflow: workflow_call with scope, exclude,
allowed-words-file, threshold and min-words
activate Workflow
Workflow -> Workflow: Check out the full history and the shared scripts
Workflow -> Workflow: Install the pinned language detector
Workflow -> Workflow: Read the comments on the added lines
Workflow -> Workflow: Remove paths, options, identifiers and allowed words
Workflow -> Workflow: Judge the language of each comment block
alt a block is not English
Workflow -> Gitea: Create or update one comment that mentions the author
Workflow --> Caller: run failed (the required check blocks the merge)
else all blocks are English
Workflow -> Gitea: Mark an earlier comment resolved
Workflow --> Caller: run passed
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)).
@@ -546,6 +589,7 @@ Rows that say Implemented or Guides written exist today. The rest is planned and
| Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](guides/quality-checks.md)) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](guides/quality-checks.md)) |
| Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](guides/quality-checks-cpp.md), [C# guide](guides/quality-checks-csharp.md)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](guides/quality-checks-cpp.md), [C# guide](guides/quality-checks-csharp.md)) |
| Code metrics and coverage badges | Implemented and released in 2.1.0 as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](guides/code-metrics.md)) | | Code metrics and coverage badges | Implemented and released in 2.1.0 as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](guides/code-metrics.md)) |
| English comments | Implemented as the reusable workflow `.gitea/workflows/english-comments.yml` with a comment extractor, a language detector measured on a corpus (ADR-0012), a pull request comment and offline tests; not yet run on the instance or required by any repository (MIL-013) ([guide](guides/english-comments.md)) |
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](guides/upgrading.md)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](guides/upgrading.md)) |
## 📄 License ## 📄 License
+82
View File
@@ -0,0 +1,82 @@
name: Connect Tailscale
description: >-
Joins a Tailscale network (tailnet) from a Gitea Actions runner, so the next
steps of the job (a deployment) can reach hosts on the tailnet. Written for
Gitea Actions; it replaces the GitHub marketplace action tailscale/github-action,
which is not verified on Gitea. Run it again with action "down" in a last step
with if always().
inputs:
action:
description: "up joins the tailnet; down logs the node out and stops tailscaled"
required: false
default: "up"
authkey:
description: "A Tailscale auth key (a secret). Use an ephemeral, tagged, pre-authorized key. Give authkey or oauth-secret, not both"
required: false
default: ""
oauth-secret:
description: "The secret of a Tailscale OAuth client with the scope auth_keys (a secret). Needs tags"
required: false
default: ""
tags:
description: "ACL tags of the node, comma separated, e.g. tag:ci. Required with oauth-secret"
required: false
default: ""
hostname:
description: "Name of the node in the tailnet. Empty uses the runner name"
required: false
default: ""
version:
description: "Exact Tailscale version to install when tailscale is not on the runner, e.g. 1.80.2. Empty uses the installed tailscale and fails when there is none"
required: false
default: ""
sha256:
description: "Optional SHA-256 of the downloaded archive tailscale_<version>_<arch>.tgz; the install fails when it differs"
required: false
default: ""
userspace-networking:
description: >-
true runs tailscaled without a TUN device (no NET_ADMIN needed); tailnet
hosts are then reached through the SOCKS5 proxy localhost:1055, e.g. ssh -o
ProxyCommand="nc -X 5 -x localhost:1055 %h %p". false creates a TUN device.
required: false
default: "false"
args:
description: "Extra arguments for tailscale up, e.g. --accept-routes. Never put a secret here"
required: false
default: ""
timeout:
description: "Seconds to wait for the node to come up"
required: false
default: "60"
health-check-host:
description: "Optional tailnet host that must answer a Tailscale ping, else the step fails"
required: false
default: ""
runs:
using: composite
steps:
- name: Mask the secrets
shell: bash
env:
TS_AUTHKEY: ${{ inputs.authkey }}
TS_OAUTH_SECRET: ${{ inputs.oauth-secret }}
run: bash "${{ gitea.action_path }}/connect-tailscale.sh" mask
- name: Run the Tailscale step
shell: bash
env:
TS_ACTION: ${{ inputs.action }}
TS_AUTHKEY: ${{ inputs.authkey }}
TS_OAUTH_SECRET: ${{ inputs.oauth-secret }}
TS_TAGS: ${{ inputs.tags }}
TS_HOSTNAME: ${{ inputs.hostname }}
TS_VERSION: ${{ inputs.version }}
TS_SHA256: ${{ inputs.sha256 }}
TS_USERSPACE: ${{ inputs.userspace-networking }}
TS_ARGS: ${{ inputs.args }}
TS_TIMEOUT: ${{ inputs.timeout }}
TS_HEALTH_CHECK_HOST: ${{ inputs.health-check-host }}
run: bash "${{ gitea.action_path }}/connect-tailscale.sh" "$TS_ACTION"
@@ -0,0 +1,196 @@
#!/usr/bin/env bash
# Steps of the composite action actions/connect-tailscale/action.yml.
#
# connect-tailscale.sh mask print ::add-mask:: for every secret input
# connect-tailscale.sh up install tailscale if needed, start tailscaled and join the tailnet
# connect-tailscale.sh down log the node out and stop tailscaled
#
# Settings come from the environment, never from interpolated expressions:
# TS_AUTHKEY, TS_OAUTH_SECRET, TS_TAGS, TS_HOSTNAME, TS_VERSION, TS_USERSPACE,
# TS_SHA256, TS_ARGS, TS_TIMEOUT, TS_HEALTH_CHECK_HOST. TS_TAILSCALE,
# TS_TAILSCALED, TS_SUDO, TS_STATE_DIR, TS_START_WAIT (tries) and
# TS_WAIT_INTERVAL (seconds) name the commands, the work folder and the waits,
# for the tests.
# The auth key is passed to tailscale in a file of mode 600, never on a command
# line. Every failure is printed as ::error::[category] message.
TS_TAILSCALE="${TS_TAILSCALE:-tailscale}"
TS_TAILSCALED="${TS_TAILSCALED:-tailscaled}"
# sudo is used only when the job is not root: a job container runs as root and
# often has no sudo. TS_SUDO overrides it (empty for none).
if [ -n "${TS_SUDO+x}" ]; then
SUDO="$TS_SUDO"
elif [ "$(id -u)" = "0" ]; then
SUDO=""
else
SUDO="sudo"
fi
STATE_DIR="${TS_STATE_DIR:-${RUNNER_TEMP:-/tmp}/tailscale}"
fail() {
echo "::error::[$1] $2"
exit 1
}
mask_secrets() {
local value
for value in "${TS_AUTHKEY:-}" "${TS_OAUTH_SECRET:-}"; do
[ -n "$value" ] && echo "::add-mask::$value"
done
return 0
}
validate_inputs() {
if [ -n "${TS_AUTHKEY:-}" ] && [ -n "${TS_OAUTH_SECRET:-}" ]; then
fail configuration "give authkey or oauth-secret, not both."
fi
if [ -z "${TS_AUTHKEY:-}" ] && [ -z "${TS_OAUTH_SECRET:-}" ]; then
fail configuration "give authkey or oauth-secret."
fi
if [ -n "${TS_OAUTH_SECRET:-}" ] && [ -z "${TS_TAGS:-}" ]; then
fail configuration "tags is required with oauth-secret (for example tag:ci)."
fi
if [ -n "${TS_TAGS:-}" ]; then
[[ "$TS_TAGS" =~ ^tag:[a-zA-Z0-9-]+(,tag:[a-zA-Z0-9-]+)*$ ]] ||
fail configuration "tags must look like tag:ci or tag:ci,tag:deploy."
fi
[[ "$TS_TIMEOUT" =~ ^[0-9]+$ ]] ||
fail configuration "timeout must be a number of seconds."
[[ "$TS_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)?$ ]] ||
fail configuration "version must be an exact version like 1.80.2."
[[ "${TS_SHA256:-}" =~ ^([0-9a-fA-F]{64})?$ ]] ||
fail configuration "sha256 must be 64 hexadecimal digits."
case "$TS_USERSPACE" in
true | false) ;;
*) fail configuration "userspace-networking must be true or false." ;;
esac
[[ "${TS_HOSTNAME:-}" =~ ^[a-zA-Z0-9-]*$ ]] ||
fail configuration "hostname may hold letters, digits and hyphens only."
}
machine_arch() {
case "$(uname -m)" in
x86_64 | amd64) echo amd64 ;;
aarch64 | arm64) echo arm64 ;;
*) fail environment "no Tailscale build for $(uname -m)." ;;
esac
}
install_tools() {
command -v "$TS_TAILSCALE" >/dev/null 2>&1 && command -v "$TS_TAILSCALED" >/dev/null 2>&1 && return 0
[ -n "$TS_VERSION" ] ||
fail configuration "tailscale is not on the runner; set version (for example 1.80.2) so it can be installed."
local arch url tmp
arch="$(machine_arch)"
url="https://pkgs.tailscale.com/stable/tailscale_${TS_VERSION}_${arch}.tgz"
echo "Installing Tailscale $TS_VERSION"
tmp="$(mktemp -d)"
curl -fsSL "$url" -o "$tmp/tailscale.tgz" ||
fail environment "could not download $url."
if [ -n "${TS_SHA256:-}" ]; then
echo "${TS_SHA256} $tmp/tailscale.tgz" | sha256sum -c --quiet - ||
fail environment "the downloaded archive does not match sha256."
fi
tar -xzf "$tmp/tailscale.tgz" -C "$tmp" --strip-components=1 ||
fail environment "could not unpack the Tailscale archive."
$SUDO install -m 755 "$tmp/tailscale" "$tmp/tailscaled" /usr/local/bin/ ||
fail environment "could not install tailscale (root or sudo is needed)."
rm -rf "$tmp"
}
daemon_answers() {
"$TS_TAILSCALE" version --daemon >/dev/null 2>&1
}
start_daemon() {
# a daemon that already runs belongs to the host; this action would log it out
if daemon_answers; then
fail environment "a tailscaled already runs on this runner; the action starts its own and must not take over the host's. Use it on a job container."
fi
local args=(--state=mem:)
if [ "$TS_USERSPACE" = "true" ]; then
args+=(--tun=userspace-networking --socks5-server=localhost:1055)
fi
mkdir -p "$STATE_DIR"
$SUDO nohup "$TS_TAILSCALED" "${args[@]}" >"$STATE_DIR/tailscaled.log" 2>&1 &
: >"$STATE_DIR/started-by-action"
wait_for_daemon
}
# wait_for_daemon: tailscaled needs a moment to create its socket
wait_for_daemon() {
local tries="${TS_START_WAIT:-30}" i
for ((i = 0; i < tries; i++)); do
daemon_answers && return 0
sleep "${TS_WAIT_INTERVAL:-1}"
done
fail connection "tailscaled did not answer in $tries tries; the last lines of its log: $(tail -n 5 "$STATE_DIR/tailscaled.log" 2>/dev/null | tr '
' ' ')"
}
# write_key_file: the auth key in a file of mode 600; prints its path
write_key_file() {
local file="$STATE_DIR/authkey" key
if [ -n "${TS_OAUTH_SECRET:-}" ]; then
key="${TS_OAUTH_SECRET}?ephemeral=true&preauthorized=true"
else
key="$TS_AUTHKEY"
fi
mkdir -p "$STATE_DIR"
(umask 077 && printf '%s' "$key" >"$file")
echo "$file"
}
tailnet_up() {
validate_inputs
install_tools
start_daemon
local keyfile up_args extra
keyfile="$(write_key_file)"
up_args=("--auth-key=file:$keyfile" "--timeout=${TS_TIMEOUT}s")
[ -z "${TS_HOSTNAME:-}" ] || up_args+=("--hostname=$TS_HOSTNAME")
[ -z "${TS_TAGS:-}" ] || up_args+=("--advertise-tags=$TS_TAGS")
# TS_ARGS is split on spaces on purpose: it holds flags, never a secret
# shellcheck disable=SC2206
extra=(${TS_ARGS:-})
if ! $SUDO "$TS_TAILSCALE" up "${up_args[@]}" "${extra[@]}"; then
rm -f "$keyfile"
fail connection "tailscale up failed; the last lines of tailscaled: $(tail -n 5 "$STATE_DIR/tailscaled.log" 2>/dev/null | tr '\n' ' ')"
fi
rm -f "$keyfile"
if [ -n "${TS_HEALTH_CHECK_HOST:-}" ]; then
"$TS_TAILSCALE" ping --c 3 --timeout 10s "$TS_HEALTH_CHECK_HOST" >/dev/null 2>&1 ||
fail connection "health-check-host '$TS_HEALTH_CHECK_HOST' does not answer on the tailnet."
fi
echo "Tailscale is up."
}
tailnet_down() {
# only a daemon this action started is logged out and stopped
if [ ! -f "$STATE_DIR/started-by-action" ]; then
echo "This action did not start tailscaled on this runner; nothing to do."
return 0
fi
$SUDO "$TS_TAILSCALE" logout 2>/dev/null || echo "The node was not logged in."
$SUDO "$TS_TAILSCALE" down 2>/dev/null || true
$SUDO pkill -x "$(basename "$TS_TAILSCALED")" 2>/dev/null || true
rm -rf "$STATE_DIR"
echo "Tailscale is down."
}
main() {
set -uo pipefail
TS_USERSPACE="${TS_USERSPACE:-false}"
TS_TIMEOUT="${TS_TIMEOUT:-60}"
TS_VERSION="${TS_VERSION:-}"
case "${1:-}" in
mask) mask_secrets ;;
up) tailnet_up ;;
down) tailnet_down ;;
*) fail configuration "unknown command '${1:-}'; use mask, up or down." ;;
esac
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
+85
View File
@@ -0,0 +1,85 @@
name: Connect WireGuard
description: >-
Brings up a WireGuard tunnel on a Gitea Actions runner, so the next steps of
the job (a deployment) can reach hosts on a private network. Run it again
with action "down" in a last step with if always().
inputs:
action:
description: "up brings the tunnel up; down takes it down and removes the configuration"
required: false
default: "up"
config:
description: >-
A complete wg-quick configuration, stored as a Gitea secret. When set, the
individual inputs below are ignored.
required: false
default: ""
private-key:
description: "Private key of this runner (a secret)"
required: false
default: ""
address:
description: "Address of this runner inside the tunnel, e.g. 10.8.0.2/32"
required: false
default: ""
peer-public-key:
description: "Public key of the WireGuard server"
required: false
default: ""
endpoint:
description: "Address of the WireGuard server as host:port"
required: false
default: ""
allowed-ips:
description: "Networks routed through the tunnel, comma separated. Never 0.0.0.0/0 unless the deployment needs it"
required: false
default: ""
preshared-key:
description: "Optional preshared key (a secret)"
required: false
default: ""
dns:
description: "Optional DNS server for the tunnel"
required: false
default: ""
persistent-keepalive:
description: "Seconds between keepalive packets, 0 turns them off"
required: false
default: "25"
interface:
description: "Name of the interface"
required: false
default: "wg0"
health-check-host:
description: "Optional host behind the tunnel that must answer a ping, else the step fails"
required: false
default: ""
runs:
using: composite
steps:
- name: Mask the secrets
shell: bash
env:
WG_CONFIG: ${{ inputs.config }}
WG_PRIVATE_KEY: ${{ inputs.private-key }}
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" mask
- name: Run the WireGuard step
shell: bash
env:
WG_ACTION: ${{ inputs.action }}
WG_CONFIG: ${{ inputs.config }}
WG_PRIVATE_KEY: ${{ inputs.private-key }}
WG_ADDRESS: ${{ inputs.address }}
WG_PEER_PUBLIC_KEY: ${{ inputs.peer-public-key }}
WG_ENDPOINT: ${{ inputs.endpoint }}
WG_ALLOWED_IPS: ${{ inputs.allowed-ips }}
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
WG_DNS: ${{ inputs.dns }}
WG_KEEPALIVE: ${{ inputs.persistent-keepalive }}
WG_INTERFACE: ${{ inputs.interface }}
WG_HEALTH_CHECK_HOST: ${{ inputs.health-check-host }}
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" "$WG_ACTION"
@@ -0,0 +1,160 @@
#!/usr/bin/env bash
# Steps of the composite action actions/connect-wireguard/action.yml.
#
# connect-wireguard.sh mask print ::add-mask:: for every secret input
# connect-wireguard.sh up write the configuration and bring the tunnel up
# connect-wireguard.sh down take the tunnel down and delete the configuration
#
# Settings come from the environment, never from interpolated expressions:
# WG_CONFIG, WG_PRIVATE_KEY, WG_ADDRESS, WG_PEER_PUBLIC_KEY, WG_ENDPOINT,
# WG_ALLOWED_IPS, WG_PRESHARED_KEY, WG_DNS, WG_KEEPALIVE, WG_INTERFACE,
# WG_HEALTH_CHECK_HOST. WG_DIR is the configuration directory (/etc/wireguard
# by default); WG_QUICK, WG_SUDO and WG_PING name the commands, for the tests.
# Every failure is printed as ::error::[category] message. A key is never printed.
WG_DIR="${WG_DIR:-/etc/wireguard}"
WG_QUICK="${WG_QUICK:-wg-quick}"
WG_PING="${WG_PING:-ping}"
# sudo is used only when the job is not root: a job container runs as root and
# often has no sudo. WG_SUDO overrides it (empty for none).
if [ -n "${WG_SUDO+x}" ]; then
SUDO="$WG_SUDO"
elif [ "$(id -u)" = "0" ]; then
SUDO=""
else
SUDO="sudo"
fi
# variables that must reach wg-quick through sudo, which resets the environment
WG_QUICK_ENV=()
fail() {
echo "::error::[$1] $2"
exit 1
}
mask_secrets() {
local value line key
for value in "${WG_PRIVATE_KEY:-}" "${WG_PRESHARED_KEY:-}"; do
[ -n "$value" ] && echo "::add-mask::$value"
done
# a full configuration: mask the value of every PrivateKey and PresharedKey
# line, and nothing else, so the rest of the log stays readable
while IFS= read -r line; do
line="${line//$'␍'/}"
key="${line%%=*}"
key="${key//[[:space:]]/}"
case "${key,,}" in
privatekey | presharedkey)
value="${line#*=}"
value="${value#"${value%%[![:space:]]*}"}"
value="${value%"${value##*[![:space:]]}"}"
[ -n "$value" ] && echo "::add-mask::$value"
;;
esac
done <<<"${WG_CONFIG:-}"
return 0
}
check_interface() {
[[ "$WG_INTERFACE" =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]] ||
fail configuration "interface '$WG_INTERFACE' is not a valid interface name."
}
# render_config: the wg-quick configuration on stdout
render_config() {
if [ -n "${WG_CONFIG:-}" ]; then
printf '%s\n' "$WG_CONFIG"
return 0
fi
local name
for name in WG_PRIVATE_KEY WG_ADDRESS WG_PEER_PUBLIC_KEY WG_ENDPOINT WG_ALLOWED_IPS; do
[ -n "${!name:-}" ] ||
fail configuration "set config, or all of private-key, address, peer-public-key, endpoint and allowed-ips ($name is empty)."
done
for name in WG_PRIVATE_KEY WG_ADDRESS WG_PEER_PUBLIC_KEY WG_ENDPOINT WG_ALLOWED_IPS WG_PRESHARED_KEY WG_DNS; do
# a line break would add directives (PostUp runs as root) to the file
[[ "${!name:-}" != *[[:cntrl:]]* ]] ||
fail configuration "$name must be one line without control characters."
done
[[ "$WG_KEEPALIVE" =~ ^[0-9]+$ ]] ||
fail configuration "persistent-keepalive must be a number of seconds."
printf '[Interface]\nPrivateKey = %s\nAddress = %s\n' "$WG_PRIVATE_KEY" "$WG_ADDRESS"
[ -z "${WG_DNS:-}" ] || printf 'DNS = %s\n' "$WG_DNS"
printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = %s\n' \
"$WG_PEER_PUBLIC_KEY" "$WG_ENDPOINT" "$WG_ALLOWED_IPS" "$WG_KEEPALIVE"
[ -z "${WG_PRESHARED_KEY:-}" ] || printf 'PresharedKey = %s\n' "$WG_PRESHARED_KEY"
}
install_tools() {
command -v "$WG_QUICK" >/dev/null 2>&1 && return 0
echo "Installing wireguard-tools"
if ! { $SUDO apt-get update -qq &&
$SUDO apt-get install -y -qq wireguard-tools resolvconf iproute2 iputils-ping; }; then
fail environment "wireguard-tools is not installed and could not be installed (a Debian or Ubuntu runner with root is needed)."
fi
}
# Use the user-space implementation when the kernel has no WireGuard module,
# which is the usual case in a Gitea runner container.
choose_implementation() {
if $SUDO ip link add dev "wgprobe$$" type wireguard 2>/dev/null; then
$SUDO ip link delete dev "wgprobe$$" 2>/dev/null || true
return 0
fi
if command -v wireguard-go >/dev/null 2>&1 || $SUDO apt-get install -y -qq wireguard-go 2>/dev/null; then
WG_QUICK_ENV=(WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard-go)
echo "No kernel WireGuard module; using wireguard-go."
else
fail environment "the runner has no WireGuard kernel module and wireguard-go could not be installed; the runner container needs NET_ADMIN and /dev/net/tun (see guides/vpn-wireguard.md)."
fi
}
tunnel_up() {
check_interface
local conf="$WG_DIR/$WG_INTERFACE.conf" rendered
# on failure the captured output is the ::error:: line, which must be shown
rendered="$(render_config)" || {
echo "$rendered"
exit 1
}
install_tools
choose_implementation
$SUDO install -d -m 700 "$WG_DIR"
(umask 077 && printf '%s\n' "$rendered" | $SUDO tee "$conf" >/dev/null)
$SUDO chmod 600 "$conf"
# env keeps the userspace variable through sudo; the file holds the key, so
# it is removed when the interface does not come up
if ! $SUDO env ${WG_QUICK_ENV[@]+"${WG_QUICK_ENV[@]}"} "$WG_QUICK" up "$WG_INTERFACE"; then
$SUDO rm -f "$conf"
fail connection "wg-quick could not bring $WG_INTERFACE up."
fi
if [ -n "${WG_HEALTH_CHECK_HOST:-}" ]; then
"$WG_PING" -c 3 -W 5 "$WG_HEALTH_CHECK_HOST" >/dev/null 2>&1 ||
fail connection "health-check-host '$WG_HEALTH_CHECK_HOST' does not answer through the tunnel."
fi
echo "WireGuard interface $WG_INTERFACE is up."
}
tunnel_down() {
check_interface
local conf="$WG_DIR/$WG_INTERFACE.conf"
$SUDO "$WG_QUICK" down "$WG_INTERFACE" 2>/dev/null || echo "Interface $WG_INTERFACE was not up."
$SUDO rm -f "$conf"
echo "WireGuard interface $WG_INTERFACE is down."
}
main() {
set -uo pipefail
WG_INTERFACE="${WG_INTERFACE:-wg0}"
WG_KEEPALIVE="${WG_KEEPALIVE:-25}"
case "${1:-}" in
mask) mask_secrets ;;
up) tunnel_up ;;
down) tunnel_down ;;
*) fail configuration "unknown command '${1:-}'; use mask, up or down." ;;
esac
}
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
main "$@"
fi
@@ -0,0 +1,68 @@
# ADR-0011: A VPN for deployment is delivered as a composite action
## Metadata
| Key | Value |
| --- | --- |
| ID | ADR-0011 |
| CrossReference | [MIL-012], [ADR-0005], [ADR-0006] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [3325bea] |
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
---
## Context
A deployment job often has to reach hosts that are not on the internet. [MIL-012] adds a way to bring a VPN up in the job, for WireGuard and for Tailscale. The GitHub marketplace has `tailscale/github-action` for Tailscale; it is written for GitHub Actions, has not been verified on Gitea Actions, and WireGuard has no such action. Every other shared automation of this repository is a reusable workflow ([ADR-0005], [ADR-0009], [ADR-0010]), so the question is whether a VPN is one too.
Options:
1. **A reusable workflow.** Consistent with the rest of the repository. But a called workflow runs as a separate job, on its own runner and network; a tunnel brought up there is gone when that job ends and is never seen by the caller's deployment job. It cannot work.
2. **The marketplace action `tailscale/github-action`.** No code to maintain. Written for GitHub, not verified on Gitea, nothing for WireGuard, and an action from outside the instance that receives the VPN secret.
3. **A composite action of this repository**, one for each VPN, in `actions/<name>/`, called as a step of the caller's own job with `uses:` and `gitea.action_path` for its script. It runs in the job, so the tunnel is there for the steps after it.
Facts: a composite action in a subfolder of this repository resolves on the instance in both the short form (`TirSystem/github-action/actions/<name>@<ref>`) and with the full address, and `gitea.action_path` points at its folder (verified in [MIL-012] task 1, runs 417 and 418 of the instance). A key must not reach a log or a command line. A tunnel needs a network interface, so the runner container needs `NET_ADMIN` and `/dev/net/tun`, which this repository cannot give.
## Decision
A VPN for deployment is delivered as **a composite action of this repository**, one per VPN: `actions/connect-wireguard` and `actions/connect-tailscale`.
- Each action has a script file next to its `action.yml` that does the work. The `run:` lines only call it, and every input reaches the script as an environment variable, never as an interpolated expression ([ADR-0005]).
- Each action has an `up` and a `down` mode. The caller runs `down` in a last step with `if: always()`.
- Secrets are masked first, written only to a file of mode 600 where a file is needed, never printed and never put on a command line.
- The scripts are linted (shellcheck and shfmt) and tested offline with stub commands on every pull request.
- The actions are versioned with the repository ([ADR-0006]); a new action is a minor release, and callers pin the tag.
- The runner is outside the scope of the action: the operator (S03) provides a runner whose container has `NET_ADMIN` and `/dev/net/tun`. The actions state the requirement and fail with a message that names it.
## Consequences
**Positive:**
- The tunnel lives in the job that deploys, which a reusable workflow cannot do.
- One implementation for Gitea, covered by offline tests; no GitHub-only action receives the VPN secret.
- WireGuard and Tailscale share one contract (`up`, `down`, a health check, masked secrets, categorised errors).
**Negative:**
- Two scripts to maintain, and behaviour that stubs cannot show (daemon start-up, `sudo`, the runner's capabilities) is verified only by runs on a real runner ([MIL-012] tasks 3 and 4).
- A composite action cannot declare a post step, so the caller must remember the `down` step; a cancelled job can leave a node or a key file behind until the runner cleans up.
- The action needs a privileged runner, which widens the attack surface of that runner; the guides tell the operator to keep it to a dedicated label.
## Affected Artifacts
- [MIL-012] — the decision behind its deliverable and its tasks 3 and 4.
- [ADR-0005] — the same contract style (script file, environment variables, categorised failures) is used for the actions.
- [ADR-0006] — a new action is a minor release.
---
[MIL-012]: ../milestones/mil-012-vpn-actions-for-deployment.md
[ADR-0005]: ./adr-0005-quality-check-contract.md
[ADR-0006]: ./adr-0006-release-process-and-version-pinning.md
[ADR-0009]: ./adr-0009-wiki-publishing-reusable-workflow.md
[ADR-0010]: ./adr-0010-code-metrics-badges-contract.md
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
@@ -0,0 +1,89 @@
# ADR-0012: How a comment is judged not to be English
## Metadata
| Key | Value |
| --- | --- |
| ID | ADR-0012 |
| CrossReference | [MIL-013], [ADR-0005], [ADR-0010] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | pending |
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
---
## Context
[MIL-013] adds a check that fails a pull request when a comment in its source code is not written in English. A comment is short (most have three to twelve words), is full of identifiers, paths and option names, and Danish and Malay words are often close to English ones. The method has to be offline (no source text leaves the instance, criterion 6 of the milestone), small enough to install on every run, deterministic (the same comment gives the same answer on every run), and measured on a corpus, not chosen by taste (criterion 1: recall at least 90 % for Danish and Malay, at most one false positive per 200 English comments).
**Corpus** (`tests/fixtures/comments/`): 300 English comments taken from this repository (`english.tsv`: comments and first docstring lines of `tools/`, `tests/`, `actions/` and the workflows, including fragments, paths and option names), and 138 comments in other languages written for the purpose (`foreign.tsv`): 45 Danish, 45 Bahasa Malaysia, and 12 each of German, Swedish, Norwegian and French. The foreign comments are written by the AI, not collected from real code (the TirSystem repositories have none) and not checked by a native reader. They are typical code comments of three to fifteen words, some with identifiers.
**Methods compared** (`tests/fixtures/comments/compare_detectors.py`; Python 3.13):
1. A rule-based one: letters outside English (`æ ø å ä ö ü ß é ...`) and word lists for English, Danish, Malay and others.
2. `langdetect` 1.0.9, a port of Google's detector (random start unless the seed is fixed).
3. `langid` 1.1.6, restricted to the candidate languages (`en da no sv de fr ms id nl`).
4. `lingua-language-detector` 2.2.0, restricted to the same candidates.
**Results** on the corpus, as the number of comments flagged as not English:
| Method | Danish | Malay | Other (48) | English flagged (of 300) | Install size | Time per 438 comments |
| --- | --- | --- | --- | --- | --- | --- |
| Rule-based (word lists written after the corpus, so optimistic) | 42/45 | 26/45 | 39/48 | 0 | none | 0.01 s |
| `langdetect`, probability at least 0.8 | 41/45 | 42/45 | 47/48 | 14 | 1 MB | 1.45 s |
| `langid`, no threshold | 45/45 | 45/45 | 48/48 | 2 | 2 MB and `numpy` | 0.02 s |
| `lingua`, confidence at least 0.5 | 29/45 | 41/45 | 47/48 | 0 | 170 MB wheel | 0.14 s |
Lowering the `lingua` threshold trades one error for the other: at 0.4 it flags 36/45 Danish and 2 English, at 0.2 it flags all Danish and Malay and 14 English. Raising the `langid` threshold on its normalised probability does the same (`0.5`: one English comment flagged; `0.6`: none).
The two English comments `langid` flagged without a threshold are lines that are mostly a script name and an option (`connect-tailscale.sh mask print ::add-mask:: for every secret input`). A pre-filter that removes tokens that are not words (paths, options, `::` tokens, tokens with digits or underscores, camelCase) and refuses to judge fewer than three words lowers the language signal of such lines, but it made the result worse at a threshold of 0.5 (three English comments flagged), so the threshold and the pre-filter go together:
| `langid` with the pre-filter | Danish | Malay | Other (48) | English flagged | English not judged (under 3 words) |
| --- | --- | --- | --- | --- | --- |
| probability at least 0.5, 3 words | 45/45 | 45/45 | 48/48 | 3 | 35 of 300 |
| probability at least 0.6, 3 words | 44/45 | 41/45 | 48/48 | 0 | 35 of 300 |
## Decision
A comment is judged **not English** when `langid` 1.1.6, restricted to `en da no sv de fr ms id nl`, with normalised probabilities, ranks a language other than English first with a probability of at least **0.6**, after this pre-filter, and only when the filter leaves **at least three words**:
- drop every token that is not a word: a token with a code character (`/ \ { } [ ] " ` = < > | $ @ # % & + ^ ~ *`), an underscore, a digit or `::` (a path, a URL, a JSON key, an identifier); a token that starts with `-` (an option), contains `.` (a file name), is written in capitals (an acronym or a constant) or is camelCase; a leading label such as `Usage:` or `Note:`; and the words of the allowed-words file. Split what is left on `-`;
- keep a token only if it is letters, with at most one inner apostrophe (letters include `æ ø å ä ö ü` and the other letters of Latin scripts);
- a block of several consecutive comment lines is judged **line by line first**, each line only with at least four words and a probability of at least 0.7, so an English line cannot hide a Danish one next to it (the first version judged the joined block and missed exactly that case in the trial of [MIL-013] task 8); when no line is flagged, the joined block is judged with the values above, so a sentence cut over short lines is not missed. A single comment line is judged once, with the values above;
- a comment with fewer than three words left is **not judged** (it passes), and so is a comment that holds only identifiers, URLs or numbers.
On the corpus this gives 0 false positives in 300 English comments, Danish 44/45 (97.8 %), Malay 41/45 (91.1 %), and 48/48 for the other languages, meeting criterion 1 of [MIL-013]. The threshold, the minimum of three words and the candidate languages are inputs of the workflow with these values as defaults; the rates are a test that fails when the code changes them (task 7).
The versions are pinned (`langid==1.1.6`, `numpy` at a pinned version) and installed in the job's virtual environment like the other tools ([ADR-0005]). Nothing is downloaded at run time and nothing is sent to a service.
The rules of the pre-filter came from the build, not from the first measurement. A scan of this repository with the first version of the filter found two false positives that the corpus did not contain: a docstring with a JSON example (`{"files": 3, "lines": 120, ...}`, judged French with probability 1.0) and `Usage:` lines with options and a file name (judged French with 0.6 and 0.88). The code-character, option, file-name, acronym and label rules above remove them; the corpus rates stayed the same, and the repository (31 source files) now gives no finding. Tests of the check keep both cases (the JSON docstring and the `Usage:` lines), so a change to the filter is measured against them.
## Consequences
**Positive:**
- Offline, deterministic, about 2 MB plus `numpy`, 0.06 s per 1 000 comments; no model download.
- The cases that matter most for TirSystem (Danish and Malay) are caught at or above the milestone's bar, and the rule is explainable to an author: "these words look Danish".
**Negative:**
- **The rates are an estimate, not a guarantee.** The foreign comments were written by the AI and are not checked by a native reader, the threshold and the filter were chosen on the same corpus they are measured on, and the English comments come from one repository. Real Danish and Malay comments will differ; the corpus must grow with every miss or false positive found in use (the allowed-words file and the exception marker of the workflow are the answer to a false positive in the meantime).
- Malay recall of 91.1 % leaves little margin over the bar of 90 %; short Malay comments and comments with many English technical terms are the cases it misses.
- A short comment (under three words after the filter) is never judged, so a Danish "TODO: ret" passes. This is the price of a low false positive rate.
- `langid` is not actively developed (its last release is 1.1.6). It is pinned, and `lingua` is the recorded alternative (better on long text, much larger, and weaker on short Danish at a comparable false positive rate).
- The method judges the language of words, not the quality of English: a comment in poor English passes.
## Affected Artifacts
- [MIL-013] — the decision behind tasks 3, 4 and 7, and criterion 1.
- [ADR-0005] — the tools are pinned and installed in the job's environment, like the quality-check tools.
- [ADR-0010] — the comment scanner of the code counter is reused to extract the comments.
---
[MIL-013]: ../milestones/mil-013-english-comments-check.md
[ADR-0005]: ./adr-0005-quality-check-contract.md
[ADR-0010]: ./adr-0010-code-metrics-badges-contract.md
+3 -3
View File
@@ -10,10 +10,10 @@ each document lives in *this* project and the next version to use.
| BC | Business Case | docs/business-case.md | 002 | | BC | Business Case | docs/business-case.md | 002 |
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 | | SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
| PP | Project Plan | docs/project-plan.md | 002 | | PP | Project Plan | docs/project-plan.md | 002 |
| MIL | Milestone / Gateway | docs/milestones/*.md | 012 | | MIL | Milestone / Gateway | docs/milestones/*.md | 014 |
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 079 | | RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 086 |
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 | | TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0011 | | ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0013 |
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 | | UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
| US | User Story | docs/user-stories.md | 002 | | US | User Story | docs/user-stories.md | 002 |
| UC | Use Case | docs/uc-NNN/uc.md | 009 | | UC | Use Case | docs/uc-NNN/uc.md | 009 |
+18 -4
View File
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Scope: wiki pages in English, Danish and Bahasa Malaysia | [08ef26a] | | 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Objective O6, scope item, assumption, constraint and success criterion 6: shared actions that bring a VPN up for a deployment job (MIL-012); reviewed (RC-081), accepted by the author in chat as stand-in for S01 | [c5d5941] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Objective O5, scope item and success criterion 5: shared workflow for code metrics and coverage badges (MIL-010); reviewed (RC-067) | [5002450] | | 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Objective O7, scope item, assumption, constraint and success criterion 7: a shared workflow that checks that the comments in source code are in English (MIL-013); reviewed (RC-084), accepted by the author in chat as stand-in for S01 | [e35b1c5] |
--- ---
@@ -62,6 +62,12 @@ made once.
- **O5:** Offer a shared workflow that counts the code files and the lines of - **O5:** Offer a shared workflow that counts the code files and the lines of
code (without comment and blank lines) of a repository, reads its coverage code (without comment and blank lines) of a repository, reads its coverage
report and publishes the three results as SVG badges. report and publishes the three results as SVG badges.
- **O6:** Offer shared, versioned actions that bring a VPN (WireGuard or
Tailscale) up for a deployment job on a Gitea runner, so the job can reach a
private network.
- **O7:** Offer a shared workflow that checks that the comments in the source
code of a pull request are written in English, blocks the pull request when
they are not, and tells its author which comment and why.
## Scope ## Scope
@@ -72,6 +78,8 @@ made once.
- A Gitea wiki with the same documentation in English, Danish and Bahasa Malaysia (MIL-005). - A Gitea wiki with the same documentation in English, Danish and Bahasa Malaysia (MIL-005).
- Validation of workflow files and their documentation. - Validation of workflow files and their documentation.
- A shared workflow for code metrics and coverage badges (MIL-010). - A shared workflow for code metrics and coverage badges (MIL-010).
- Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale (MIL-012).
- A shared workflow that checks that comments in source code are in English and tells the author (MIL-013).
### Out of Scope ### Out of Scope
@@ -109,6 +117,8 @@ accepted plans.
| 3 | Documented use cases that are implemented or marked planned | 100% | README capability table against workflows present | | 3 | Documented use cases that are implemented or marked planned | 100% | README capability table against workflows present |
| 4 | Repositories consuming a shared workflow | At least 1 besides this one by MIL-002 | Consumer list in README | | 4 | Repositories consuming a shared workflow | At least 1 besides this one by MIL-002 | Consumer list in README |
| 5 | Repositories that show the code metrics and coverage badges from the shared workflow | At least 1 besides this one by MIL-010 | README of the consuming repository | | 5 | Repositories that show the code metrics and coverage badges from the shared workflow | At least 1 besides this one by MIL-010 | README of the consuming repository |
| 6 | VPN actions that connect a deployment job on the instance, with no secret in the run log | 2 (WireGuard and Tailscale) by MIL-012 | Recorded runs of MIL-012 tasks 3 to 5 |
| 7 | Comments in source code that are not in English and reach the default branch of a repository that calls the check | 0 after MIL-013 | The check's run on the default branch; a throwaway pull request with a finding that cannot be merged |
## Risks ## Risks
@@ -124,11 +134,15 @@ accepted plans.
- The Gitea instance has Actions enabled and an `ubuntu-latest` runner with `python3`. - The Gitea instance has Actions enabled and an `ubuntu-latest` runner with `python3`.
- A mirrored repository has at most one GitHub push mirror. Repositories without a GitHub mirror are skipped successfully. - A mirrored repository has at most one GitHub push mirror. Repositories without a GitHub mirror are skipped successfully.
- S01 (Product Owner) and S05 (DevOps owner and security authority) are both maintainers. - S01 (Product Owner) and S05 (DevOps owner and security authority) are both maintainers.
- For the VPN actions (MIL-012), S03 provides a runner whose container has the capability `NET_ADMIN` and the device `/dev/net/tun`, and the test peers; the actions do not configure the runner.
- For the English-comments check (MIL-013), S03 provides a Gitea token that may comment on pull requests, as an organization secret, and S01 or S03 decide which repositories mark the check as required; the workflow does not change branch protection.
## Constraints ## Constraints
- Workflows use only tools available on the runner; the current workflow uses the Python standard library only. - Workflows use only tools available on the runner; the current workflow uses the Python standard library only.
- Secrets are supplied as repository or organization secrets, never committed. - Secrets are supplied as repository or organization secrets, never committed.
- A VPN key or token never appears in a log, an artifact or a command line (MIL-012).
- The English-comments check (MIL-013) runs without sending source text to a service outside the instance, and its token never appears in a log.
- Nothing is written under `src/` or `tests/` before the matching milestone is accepted. - Nothing is written under `src/` or `tests/` before the matching milestone is accepted.
## Cost–Benefit Assessment ## Cost–Benefit Assessment
@@ -154,5 +168,5 @@ Proceed — the first milestone fixes a known defect in the only existing workfl
--- ---
[SA-001]: ./stakeholder-analysis.md [SA-001]: ./stakeholder-analysis.md
[08ef26a]: https://git.tirsystem.com/TirSystem/github-action/commit/08ef26a6d3eb98c066b45634c2271e578e6c0a0b [c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
[5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e [e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
@@ -9,8 +9,8 @@
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of task 1 (reproduction and the defaults of the five workflows); reviewed (RC-077), accepted by the AI as stand-in for S01 in autonomous mode | [e11c43f] | | 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | S01 | Added the results of task 1 (reproduction and the defaults of the five workflows); reviewed (RC-077), accepted by the AI as stand-in for S01 in autonomous mode | [e11c43f] |
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added the results of tasks 2 to 5 (the fix, the test, the guides, the run on the instance); reviewed in RC-078, the release follows | [a6892c3] | | 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 2 to 5 (the fix, the test, the guides, the run on the instance); reviewed in RC-078, the release follows; outcome reviewed (RC-079), accepted by the AI as stand-in for S01 in autonomous mode | [a6892c3] |
--- ---
@@ -107,6 +107,13 @@ Tasks 2 to 5 were done on 2026-10-14 on the branch `mil-011-fix`:
- **Guides (task 4):** `guides/code-metrics.md`, `guides/upgrading.md`, the three quality-check guides, `README.md` and the wiki pages in English, Danish and Bahasa Malaysia; the amendments of ADR-0005, ADR-0008, ADR-0009 and ADR-0010. A search for `code-metrics.yml@v1` finds none. - **Guides (task 4):** `guides/code-metrics.md`, `guides/upgrading.md`, the three quality-check guides, `README.md` and the wiki pages in English, Danish and Bahasa Malaysia; the amendments of ADR-0005, ADR-0008, ADR-0009 and ADR-0010. A search for `code-metrics.yml@v1` finds none.
- **Instance (task 5):** the workflow at `@mil-011-fix` with every input at its default except `badges-branch` ran as run 1509 (job 1649): `tools-ref` resolved to `v2`, the scripts were fetched, and the badges were committed to `badges-verify` (commit `041d991`). - **Instance (task 5):** the workflow at `@mil-011-fix` with every input at its default except `badges-branch` ran as run 1509 (job 1649): `tools-ref` resolved to `v2`, the scripts were fetched, and the badges were committed to `badges-verify` (commit `041d991`).
Tasks 6 and 7 were done on 2026-10-14:
- **Release (task 6):** the Release workflow ran in dry-run mode (run 1524) and then for real (run 1525); `v2.1.1` and `v2` are `3023461`, `v1` is unchanged at `9bf0a2b` (1.2.2), and the release page has the changelog entry. A call at `@v2` with the defaults ran after the release (run 1526, job 1670): `tools-ref` resolved to `v2`, the scripts were fetched at `3023461`, and the badges were published.
- **Framework page (task 7):** `TirSystem/sqa-qc-framework` pull request 57 removed the `tools-ref` workaround from the `Badges` pages in three languages; the wiki is published.
- **Outcome:** [RC-079], Go, all eight criteria pass.
--- ---
[BC-001]: ../business-case.md [BC-001]: ../business-case.md
@@ -115,5 +122,6 @@ Tasks 2 to 5 were done on 2026-10-14 on the branch `mil-011-fix`:
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md [ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md [ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
[PP-001]: ../project-plan.md [PP-001]: ../project-plan.md
[RC-079]: ../sqa/reviews/rc-079-mil-011-outcome.md
[e11c43f]: https://git.tirsystem.com/TirSystem/github-action/commit/e11c43f0a41e7dd9647f7804947d6e464407a597 [e11c43f]: https://git.tirsystem.com/TirSystem/github-action/commit/e11c43f0a41e7dd9647f7804947d6e464407a597
[a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb [a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb
@@ -0,0 +1,129 @@
# MIL-012: VPN actions for deployment (WireGuard and Tailscale)
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-012 |
| CrossReference | [BC-001], [US-001], [MIL-004], [MIL-011], [ADR-0006], [ADR-0011] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version; reviewed (RC-080), accepted by the author in chat as stand-in for S01 with target date 2026-11-06 | [c5d5941] |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 1, 6 and 9 and the fixes from the review of the first pull request; reviewed (RC-082), accepted by the author in chat as stand-in for S01 | [3325bea] |
---
## Purpose
Decide whether a deployment job on a Gitea runner can reach a private network through a shared, versioned action, and release it if it can.
A deployment often has to reach hosts that are not on the internet. The GitHub marketplace has the action `tailscale/github-action` for a Tailscale network; it is written for GitHub Actions and has not been verified on Gitea Actions. WireGuard has no equivalent action at all. Two experimental composite actions for Gitea Actions already exist on the unreleased branch `wireguard-vpn-action` (`actions/connect-wireguard`, `actions/connect-tailscale`), with guides and offline tests (574 tests pass), but neither has run on a real runner, and the one question that decides whether the Tailscale action works is open: whether `tailscaled`, started in the background of one step, is still alive in the next step of the job.
This milestone runs both on a real runner of the instance, fixes what that shows, and releases them. It adds a capability, so it is a minor release ([ADR-0006]).
## Deliverable
Release `2.2.0` of this repository with two composite actions, `actions/connect-wireguard` and `actions/connect-tailscale`, each with a guide, a README section, wiki pages in English, Danish and Bahasa Malaysia, a changelog entry under `Added`, and an offline test. A record of one successful deployment-shaped job per action on the instance. An ADR for the decision to deliver a VPN as a composite action. In `TirSystem/sqa-qc-framework`: the templates `deploy-wireguard.yml` and `deploy-tailscale.yml` pinned to the release tag instead of the branch, in a pull request of their own.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | A `uses:` of a composite action in a subfolder of this repository (`TirSystem/github-action/actions/<name>@<ref>`) resolves on the instance; the form that works (short, or the full address) is recorded | A run starts and executes the action; the form is recorded | No form resolves |
| 2 | `connect-wireguard` brings a tunnel up on a runner of the instance, a following step reaches a host behind the tunnel, and the `down` step removes the interface and the file | Recorded run; `wg show` and the file are absent afterwards | The tunnel is not up in the following step, or anything remains |
| 3 | `connect-tailscale` joins a tailnet on a runner of the instance in TUN mode and in userspace mode, a following step reaches a tailnet host, and `down` leaves no node in the tailnet | Recorded run for both modes; the node list shows no node afterwards | `tailscaled` does not survive to the next step, or a node remains |
| 4 | No secret appears in any log of the runs above (auth key, OAuth secret, WireGuard keys), checked by a search of the run logs for the secret values | Count is 0 | A secret value appears in a log |
| 5 | The runner requirements are recorded as verified: the container options, the label, and whether `wireguard-go` or the kernel module was used | Recorded in both guides | A requirement is only assumed |
| 6 | The scripts of the actions are linted (shellcheck, shfmt) and their tests run on every pull request in `validate-workflows.yml`; a seeded error fails the run | Fails on a seeded error, passes on the fix | Not part of the validation |
| 7 | The README, both guides and the wiki pages (English, Danish, Bahasa Malaysia) describe the actions at `@v2` and no longer call them experimental; a search of `guides/`, `README.md` and `docs/wiki/` finds no `@wireguard-vpn-action` | Count is 0 | A page still names the branch |
| 8 | The release is a minor (`2.2.0`): the changelog has an `Added` entry, the major tag `v2` moves to it, and `v1` is not touched | Released | Another level, or `v1` moved |
| 9 | The framework templates name the release tag, not the branch, in a pull request of `TirSystem/sqa-qc-framework` opened after release 2.2.0 | Pull request open with the tag | A template names the branch |
| 10 | Reviewed | A review record (`RC-*`) for this milestone, the actions, the tests and the guides, each with a verdict of Go | An instance without a record |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-004] | The release process and the Release workflow that publish `2.2.0` |
| [MIL-011] | The default `tools-ref` and the `MAJOR_TAG` file; the release is the first after the fix |
| [ADR-0006] | A new capability is a minor release, and a moving major tag follows it |
| A runner with `NET_ADMIN`, `/dev/net/tun` and a label of its own (S03) | Both actions need a network interface; outside this repository, set up by the runner's operator |
| A WireGuard peer and a Tailscale tag with an OAuth client (S03) | Test peers for criteria 2 and 3; secrets held by the operator, never in this repository |
| `TirSystem/sqa-qc-framework`, branch `ssdf-workflow-selection-vpn` | Holds the templates and the rules that call the actions; criterion 9 changes it in its own pull request |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Business Case objective O6: a shared, versioned action that brings a VPN up for a deployment job on a Gitea runner | [BC-001] |
| User stories | [US-001] (no new story: the tasks below are technical; the actions are used by deployment jobs, not by a person) |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S05 |
| Approving reviewer | S01 |
## Target Date
2026-11-06 (Friday), proposed: two weeks of work from 2026-10-26, because the trials need a runner with extra privileges and test peers that the operator must set up. It overlaps [MIL-002] (2026-10-19 to 2026-10-31), which is documentation and does not touch the runners' network. The Business Case sets no deadline. S01 confirmed the date when accepting this milestone, in chat on 2026-10-19.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Check that a subfolder action resolves | On a throwaway branch of a repository, call `actions/connect-wireguard` and `actions/connect-tailscale` with the short form and with the full address, at the branch. Record which form starts a run and the message when one does not. | No | |
| 2 | Prepare the trial runner | With the operator: a runner labelled `vpn` whose container has `NET_ADMIN` and `/dev/net/tun`; a WireGuard peer and a host behind it; a Tailscale tag, an OAuth client and a host in the tailnet. Secrets are stored as organization secrets and are not recorded here. | No | |
| 3 | Verify `connect-wireguard` | Run a deployment-shaped job (connect, reach the host over the tunnel, disconnect with `if: always()`) with a kernel module and with `wireguard-go`. Record the runs and what remains on the runner. Fix the script where it fails and extend the tests. | No | [MIL-012] |
| 4 | Verify `connect-tailscale` | Run the same job in TUN and in userspace mode. Decide whether `tailscaled` survives to the next step; if it does not, change how it is started (for example `setsid`, or a post step) and test it. Record the node list after the run. | No | [MIL-012] |
| 5 | Search the logs for secrets | Search every run log of tasks 3 and 4 for the secret values and for the key file path; fix any leak. | No | |
| 6 | Lint and test the action scripts in CI | Add shellcheck and shfmt for `actions/**/*.sh` and the two test files to `validate-workflows.yml`; show a seeded error fails the run. | No | [MIL-001] |
| 7 | Remove the experimental notices | Update both guides with the verified runner requirements, change `@wireguard-vpn-action` to `@v2` everywhere, and add the README section for the actions. | No | |
| 8 | Wiki pages | Add pages for the two actions in English, Danish and Bahasa Malaysia, following the wiki rules of [MIL-005], and register them in the wiki index. | No | [MIL-005] |
| 9 | Record the decision and the objective | Write an ADR: a VPN is delivered as a composite action, not as a reusable workflow (a called workflow runs as a separate job), and not through the marketplace action (written for GitHub). Objective O6, its scope item and success criterion 6 are already in [BC-001], and the schedule row, Gantt bar, scope entry, dependency and risk are in [PP-001] (added at acceptance, [RC-080], [RC-081]); this task writes the ADR only. | No | [ADR-0006] |
| 10 | Release 2.2.0 | Add the `Added` entry to the changelog, run the Release workflow (dry run first), and check that `v2` points at `v2.2.0` and `v1` is unchanged. | No | [ADR-0006] |
| 11 | Pin the framework templates | In `TirSystem/sqa-qc-framework`, change `@wireguard-vpn-action` to `@v2` in the two templates and the branch guide, remove the "experimental" notes that the release resolves, and open the pull request after release 2.2.0. This is a change in another repository, in its own pull request. | No | |
| 12 | Review | Record `RC-*` for this milestone, the actions with their tests, and the guides, each with a verdict. | No | |
## Verification results
Task 1 was run on 2026-10-19 with the throwaway branch `verify-subfolder-action`, which calls both actions in the short form (`TirSystem/github-action/actions/<name>@<ref>`) and with the full address (`https://git.tirsystem.com/TirSystem/github-action/actions/<name>@<ref>`), with every input empty, so that a resolved action fails with its own `[configuration]` message.
| Call | Result | Evidence |
| --- | --- | --- |
| Either form at `@wireguard-vpn-action` | Fails before the step: `Unable to resolve wireguard-vpn-action: reference not found`. The branch had been deleted when its pull request merged, so the ref did not exist; this is not a defect of the form. | run 417 (`2424`) |
| Short form at `@main`, both actions | Resolves and runs: the log shows `Run Pre ...@main`, the action's `mask` step and its main step, which fails with `[configuration] set config, or all of private-key, ... (WG_PRIVATE_KEY is empty)` and `[configuration] give authkey or oauth-secret` | run 418 (`2425`), jobs `wg-short`, `ts-short` |
| Full address at `@main`, both actions | The same result as the short form | run 418, jobs `wg-full`, `ts-full` |
Both forms resolve. The short form is the one the guides show; the full address stays in the guides as the fallback. `gitea.action_path` is set (the script path in the log is under `/var/run/act/actions/<id>/actions/<name>/`). A `uses:` of a branch that no longer exists fails with "reference not found", so a caller of an experimental ref breaks when its branch is deleted.
Tasks 6 and 9 were done on 2026-10-19 on the branch `mil-012-review-fixes-and-docs`:
- **Lint (task 6):** `tools/lint-action-scripts.sh` runs shellcheck and shfmt on `actions/**/*.sh`; `validate-workflows.yml` runs it with pinned tools (`shellcheck-py==0.10.0.1`, `shfmt-py==3.11.0.2`, from `tests/requirements.txt`), and `.editorconfig` sets the shell format (two spaces, indented case labels, as the shell conventions say). `tests/test_lint_action_scripts.py` seeds a shellcheck problem and a badly formatted script and shows both fail, and shows the scripts of the actions pass.
- **Decision (task 9):** [ADR-0011] (Proposed). Objective O6 and the Project Plan entries were added at the acceptance of this milestone ([RC-080], [RC-081]).
The review of pull request 194 (ten findings) is answered in the same branch: `tailscaled` is waited for before `tailscale up`; the `wireguard-go` choice survives `sudo`; `sudo` is used only when the job is not root; a `tailscaled` that already runs on the runner is not taken over and `down` leaves a daemon it did not start; the Tailscale install needs an exact `version`, with an optional `sha256`; a configuration field with a line break is refused; only the key values are masked; the key file is removed when `wg-quick` fails; and the guides call `@main` until the release. These are offline-tested with stubs; tasks 3 and 4 still have to show them on a runner.
Tasks 2, 3, 4, 5, 7, 8, 10, 11 and 12 are open: they need the trial runner (task 2), which is the operator's, or follow the verified guides (tasks 7 and 8) or the release (tasks 10 and 11).
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[MIL-001]: ./mil-001-stabilise-metadata-sync.md
[MIL-002]: ./mil-002-onboarding-and-operations.md
[MIL-004]: ./mil-004-versioned-release.md
[MIL-005]: ./mil-005-wiki-and-translations.md
[MIL-011]: ./mil-011-code-metrics-default-tools-ref.md
[MIL-012]: ./mil-012-vpn-actions-for-deployment.md
[RC-080]: ../sqa/reviews/rc-080-mil-012-plan.md
[RC-081]: ../sqa/reviews/rc-081-bc-001-o6.md
[RC-082]: ../sqa/reviews/rc-082-mil-012-results.md
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
[ADR-0011]: ../adr/adr-0011-vpn-composite-actions.md
[PP-001]: ../project-plan.md
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
@@ -0,0 +1,135 @@
# MIL-013: Source code comments are in English
## Metadata
| Key | Value |
| --- | --- |
| ID | MIL-013 |
| CrossReference | [BC-001], [US-001], [MIL-004], [MIL-010], [MIL-011], [ADR-0005], [ADR-0006], [ADR-0010], [ADR-0012] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version; reviewed (RC-083), accepted by the author in chat as stand-in for S01 with target date 2026-11-20 | [e35b1c5] |
| 2026-10-21 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 1 to 8, 10 and 13 and the three defects found in the trial; reviewed (RC-085), accepted by the author in chat as stand-in for S01 | [88f852a] |
---
## Purpose
Decide whether a pull request can be stopped automatically when a comment in its source code is not written in English, and the author told why.
TirSystem code is read by people who speak Danish, Bahasa Malaysia and English, so the comments of source code are in English only (the shared language of the repositories, and the register the project's coding conventions already ask for). Today nothing enforces it: a Danish or Malay comment is found, if at all, by a human reviewer. This milestone adds a check that finds them on the pull request, fails the check run so the pull request is blocked, and writes a message to the author with the file, the line and the reason.
The hard part is the detection, not the workflow. A comment is short, full of identifiers, and often a few words; Danish and Malay words are often close to English ones or are names. The first task therefore decides the method on a measured corpus and records it in an ADR; the rest follows from it.
## Deliverable
Release `2.3.0` of this repository ([ADR-0006]: a new capability is a minor release) with a reusable workflow `english-comments.yml` that other repositories call with `uses:`, its script files under `tools/` (comment extraction, detection, reporting), a guide, a README section, wiki pages in English, Danish and Bahasa Malaysia, a changelog entry under `Added`, and offline tests. An ADR for the detection method and its error rate. The workflow runs on the pull requests of this repository too. A review record. In the other repositories, in their own pull requests: the rule in the `coding-conventions` skill of `TirSystem/sqa-qc-framework`, and a proposed criterion for the checklist repository.
## Go / No-Go Criteria
| # | Criterion (objectively checkable) | Go | No-Go |
| --- | --- | --- | --- |
| 1 | The detection method is chosen on a corpus recorded in the repository: at least 40 comments each in Danish and in Bahasa Malaysia, at least 40 in other languages (German, Swedish, Norwegian, French), and at least 200 English comments, among them comments with identifiers, paths and abbreviations. The ADR gives recall and false positives per language | Recall at least 90 % for Danish and Malay, and at most 1 false positive per 200 English comments | A lower rate, or no corpus |
| 2 | The check reads comments and docstrings, not code or string values, in every language of [ADR-0010], and a comment line holding only identifiers, URLs, numbers or code is not judged | Fixtures for each language pass | A string value or a code-only comment is reported |
| 3 | The workflow checks the files a pull request changes (all tracked files when asked), reports each finding as `file:line`, the language found and the text, and fails the run when there is one | A pull request with one Danish comment fails; one without passes | The run passes with a finding, or fails without one |
| 4 | An author can mark an exception on the line (a quotation, a name) and list allowed words in a file of the repository; both are logged | The marked line is not reported | No way to mark an exception |
| 5 | On failure the workflow writes one comment on the pull request that mentions the author and lists the findings; a re-run updates that comment instead of adding another, and a passing run marks it resolved | Verified on a throwaway pull request, with the Gitea mention reaching the author | Two comments, no mention, or a comment on a passing run |
| 6 | The comment is written with a token that holds the smallest scope that works; it is a CI secret and appears in no log, and no source text is sent to a service outside the instance | A search of the run logs for the token finds none; the detector needs no network | A token in a log, or an external call |
| 7 | The failing check blocks the merge: the workflow's check is listed as required, and a pull request with a finding cannot be merged. Setting branch protection is the repository owner's action; this milestone documents it and verifies it with the owner | Verified with S01 or S03 on a throwaway branch | The pull request can still be merged |
| 8 | This repository calls the workflow on its own pull requests, and passes | The caller run is green on `main` | Not called, or red |
| 9 | The guide, README and wiki pages (English, Danish, Bahasa Malaysia) describe the workflow at `@v2`, and the changelog has an `Added` entry; the release is a minor (`2.3.0`), `v2` moves to it and `v1` is not touched | Released | Another level, or `v1` moved |
| 10 | The framework rule and the checklist criterion are open as pull requests of their own repositories after release 2.3.0 | Both open | Missing |
| 11 | Reviewed | A review record (`RC-*`) for this milestone, the workflow, the scripts, the tests and the guides, each with a verdict of Go | An instance without a record |
## Dependencies
| Depends on | Reason |
| --- | --- |
| [MIL-010] | The code counter that already tells comment lines from code lines for the languages of [ADR-0010]; the extractor builds on its scanner |
| [MIL-004] | The release process and the Release workflow that publish `2.3.0` |
| [MIL-011] | `MAJOR_TAG` and the `tools-ref` default that a new reusable workflow must follow |
| [ADR-0005] | The contract of a shared check: inputs with working defaults, pinned tools, a script file, a failure that names the cause |
| An organization secret with a Gitea token that may comment on pull requests (S03) | Criterion 5; created by the operator, never recorded in this repository |
| Branch protection of the repositories that adopt the check (S01 or S03) | Criterion 7; changing it is the owner's decision, not made by the workflow |
| `TirSystem/sqa-qc-framework` and `TirSystem/SQA-QC-Checklists` | Task 12 changes them in their own pull requests, after the release |
## Traceability
| Business Case objective / KPI / user story | Reference |
| --- | --- |
| Business Case objective O7: a shared workflow that checks that the comments in source code are written in English, blocks the pull request when they are not, and tells the author | [BC-001] |
| User stories | [US-001] (no new story: the tasks below are technical; the check is used by the authors of pull requests) |
## Ownership
| Role | Stakeholder ID (SA) |
| --- | --- |
| Owner | S05 |
| Approving reviewer | S01 |
## Target Date
2026-11-20 (Friday), proposed: two weeks from 2026-11-09, after [MIL-012] (2026-11-06) so that the two milestones do not compete for the one runner and the one reviewer. The Business Case sets no deadline. S01 confirmed the date when accepting this milestone, in chat on 2026-10-19.
## Tasks
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
| --- | --- | --- | --- | --- |
| 1 | Build the corpus | Collect and label the comments of criterion 1 in `tests/fixtures/` (real comments from the TirSystem repositories where they exist, written ones otherwise), with the language of each. English comments come from this repository and others that are known to be English. Record the source of each part. | No | |
| 2 | Choose the detection method (ADR) | Compare at least two methods on the corpus: a rule-based one (letters outside English, word lists for Danish and Malay) and a language-identification library restricted to the candidate languages, run offline. Measure recall and false positives per language, the install size and the time on a repository of 1 000 files. Write the ADR with the numbers and the choice. | No | [ADR-0005] |
| 3 | Extract comments | Add a script that returns the text, file and line of every comment and docstring for the languages of [ADR-0010], reusing the scanner of `tools/count_loc.py` (it classifies lines today but does not return the text). Skip string values, code and comment lines that hold only identifiers, URLs, numbers or code. | No | [MIL-010] |
| 4 | Detect the language | Implement the method of task 2 as a script with a fixed interface (comment in, language and confidence out), the exception marker on the line and the allowed-words file of criterion 4. | No | |
| 5 | Report and tell the author | A script that prints each finding as `::error file=...,line=...` and `file:line`, fails the run, and writes or updates one pull request comment that mentions the author, through the Gitea API with the smallest token scope; a passing run marks the comment resolved. A fake API in the tests. | No | |
| 6 | Write the reusable workflow | `english-comments.yml` with `workflow_call`, inputs with working defaults (changed files only, languages, allowed-words file, exclude), a pinned `tools-ref` that follows `MAJOR_TAG`, the token as a secret, and explicit job permissions. The `run:` lines only call the script files. | No | [ADR-0005] |
| 7 | Offline tests | Tests for the extractor, the detector against the corpus (the rates of criterion 1 as a test that fails below them), the reporting against the fake API, and the workflow definition; the coverage limit of `tools/` holds. | No | |
| 8 | Trial on the instance | On a throwaway pull request with one Danish comment, one Malay comment and one marked exception: record the failing run, the comment with the mention, the update on a re-run and the green run after the fix. Search the logs for the token. | No | |
| 9 | Block the merge | With S01 or S03, mark the check as required for `main` of this repository and show that a pull request with a finding cannot be merged. Document the setting for other repositories. This task changes branch protection, so it is done by the owner, not by the AI. | No | |
| 10 | Guides, README and wiki | Guide, README section, and wiki pages in English, Danish and Bahasa Malaysia, following [MIL-005]; the sidebar. | No | [MIL-005] |
| 11 | Call it in this repository, then release | A caller workflow for the pull requests of this repository; the changelog entry; the Release workflow (dry run first); check that `v2` points at `v2.3.0` and `v1` is unchanged. | No | [ADR-0006] |
| 12 | The rule in the framework and the checklists | In `TirSystem/sqa-qc-framework`, add the English-comments rule to the `coding-conventions` skill and the call of the workflow to its CI template; in `TirSystem/SQA-QC-Checklists`, propose a criterion. Each in its own pull request, after release 2.3.0. | No | |
| 13 | Add MIL-013 to the Business Case and the Project Plan | Objective O7, the scope item and a success criterion in [BC-001]; the schedule row, Gantt bar, scope entry, dependency and a plan risk (false positives on short comments) in [PP-001]; the traceability rows. | No | |
| 14 | Review | Record `RC-*` for this milestone, the workflow, the scripts, the tests and the guides, each with a verdict. | No | |
## Verification results
**Tasks 1 to 7, 10 and 13** were done on 2026-10-19 to 2026-10-21 and merged in pull request 211 (and 196 for task 13): the corpus (300 English, 45 Danish, 45 Malay, 48 other), [ADR-0012] (`Proposed`, not yet reviewed), the extractor and detector, the report script, the reusable workflow `english-comments.yml`, 673 offline tests with 98 % coverage of `tools/`, and the guide, README and wiki in three languages. The detector flags 0 of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments and all 48 of the others; the Danish and Malay comments were written for the test and not checked by a native reader.
**Task 8 (trial on the instance)** was run on 2026-10-21 with the throwaway branch `verify-english-comments` and its pull request 213 (closed, not merged). The sample file holds an English comment, a Danish comment directly under it, a Malay comment, and a Danish comment marked `english-comments: ignore`.
| Run | What was called | Result |
| --- | --- | --- |
| 433 (id 2478), first call at `@main` | the workflow of pull request 211 | Fetched the shared scripts, installed `langid` and `numpy` on the runner, found only the Malay comment (line 9, `id` 0.66); the Danish line next to the English one was **missed** (defect 1). The step "Tell the author" was **skipped**, so no comment was written (defect 2). The run failed. |
| 435 and 437 (ids 2480, 2482), a debug workflow | the contexts of a called workflow | `gitea.event_name` is `workflow_call`; `gitea.event.pull_request.number` (213), the author login and `gitea.base_ref` (`main`) are set. The conditions `always()`, `always() && ...pull_request.number != ''`, `... && ...pull_request.number`, `... && ...pull_request` and `... && gitea.base_ref != ''` ran the step; `gitea.event_name == 'pull_request'` and `github.event_name == 'pull_request'` did not. |
| 440 (id 2485), the fix branch | `english-comments.yml@mil-013-judge-each-line` | Both the Danish line (6, `da` 1.0) and the Malay line (9) were found, the marked line was not reported, the run failed, and the Actions bot (`gitea-actions`) wrote **one** comment (id 8715) that mentions `@Tirsvad` and lists the two findings. |
| 443 (id 2488) | the same, with another Danish comment | "Updated the comment of the check": still one comment (id 8715), with the new text. |
| 445 (id 2490) | both comments rewritten in English | The run passed, the failing step was skipped, and the comment was marked resolved (a "passes" text, no mention). |
| 447 (id 2492) | a Malay comment again | The comment updated; its link `.../actions/runs/2492` equals the `html_url` of the run (defect 3 fixed: the first version linked the run number 440). |
The three defects are fixed in pull request 212 (open at the time of writing): (1) a block of several lines is judged line by line first (at least four words, probability 0.7), then as a whole; (2) the author step asks for `gitea.event.pull_request.number != ''` instead of the event name; (3) the comment links the run by its id. Each has a test.
**Secrets:** the four `english-comments` jobs of runs 440, 443, 445 and 447 were searched for `token <20 or more characters>`, `basic <base64>`, `bearer` and for the value of the token used from this machine; nothing was found. `REPORT_TOKEN` is printed as `***` and the clone header as `AUTHORIZATION: basic ***`. The detector made no network call; the only calls were to the Gitea API of the instance.
**Not shown by the trial:** a call at `@main` of the corrected workflow (it follows the merge of pull request 212); a pull request from a fork (read-only token); a custom `REPORT_TOKEN` secret; the check as a required status that blocks the merge (task 9); and the call of the workflow by this repository (task 11). Tasks 9, 11, 12 and 14 are open.
---
[BC-001]: ../business-case.md
[US-001]: ../user-stories.md
[MIL-004]: ./mil-004-versioned-release.md
[MIL-005]: ./mil-005-wiki-and-translations.md
[MIL-010]: ./mil-010-code-metrics-badges.md
[MIL-011]: ./mil-011-code-metrics-default-tools-ref.md
[MIL-012]: ./mil-012-vpn-actions-for-deployment.md
[MIL-013]: ./mil-013-english-comments-check.md
[ADR-0005]: ../adr/adr-0005-quality-check-contract.md
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
[PP-001]: ../project-plan.md
[RC-083]: ../sqa/reviews/rc-083-mil-013-plan.md
[RC-084]: ../sqa/reviews/rc-084-bc-001-o7.md
[RC-085]: ../sqa/reviews/rc-085-mil-013-results.md
[ADR-0012]: ../adr/adr-0012-english-comment-detection.md
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
+22 -8
View File
@@ -4,25 +4,27 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | PP-001 | | ID | PP-001 |
| CrossReference | [BC-001], [SA-001], [ADR-0002], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011] | | CrossReference | [BC-001], [SA-001], [ADR-0002], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-010 (code metrics and coverage badges); reviewed (RC-068) | [5002450] | | 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Added MIL-012 (VPN actions for deployment); reviewed (RC-080), accepted by the author in chat as stand-in for S01 | [c5d5941] |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added MIL-011 (fix the defaults of the code metrics workflow); reviewed (RC-076), accepted by the author in chat as stand-in for S01 | [02e3794] | | 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Added MIL-013 (source code comments are in English); reviewed (RC-083), accepted by the author in chat as stand-in for S01 | [e35b1c5] |
--- ---
## Purpose ## Purpose
This plan schedules eleven milestones that move the repository from one This plan schedules thirteen milestones that move the repository from one
workflow with known defects to a documented, versioned collection of shared workflow with known defects to a documented, versioned collection of shared
workflows. The Business Case sets no deadline, so the dates are the plan's own; they were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002). workflows. The Business Case sets no deadline, so the dates are the plan's own; they were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
## Planning Assumptions ## Planning Assumptions
- Week 1 starts 2026-10-05; phases are two weeks long; the last decision is 2026-12-26; MIL-007 follows in January 2027 (2027-01-04 to 2027-01-16, confirmed on 2026-10-11 by S01). MIL-008 amends the shared wiki publishing of MIL-006 and runs in parallel with the October and November windows: its work started on 2026-10-13 and its decision date is its own Target Date, 2026-11-15. MIL-009 follows MIL-008 and runs from 2026-11-16 to its proposed Target Date, 2026-11-30, confirmed by S01 in chat on 2026-10-14. MIL-010 (code metrics and coverage badges) follows MIL-007 in January 2027, from 2027-01-18 to its Target Date, 2027-01-30, accepted by the user in chat on 2026-10-14. - Week 1 starts 2026-10-05; phases are two weeks long; the last decision is 2026-12-26; MIL-007 follows in January 2027 (2027-01-04 to 2027-01-16, confirmed on 2026-10-11 by S01). MIL-008 amends the shared wiki publishing of MIL-006 and runs in parallel with the October and November windows: its work started on 2026-10-13 and its decision date is its own Target Date, 2026-11-15. MIL-009 follows MIL-008 and runs from 2026-11-16 to its proposed Target Date, 2026-11-30, confirmed by S01 in chat on 2026-10-14. MIL-010 (code metrics and coverage badges) follows MIL-007 in January 2027, from 2027-01-18 to its Target Date, 2027-01-30, accepted by the user in chat on 2026-10-14.
- MIL-012 (VPN actions for deployment) runs from 2026-10-26 to its Target Date, 2026-11-06, proposed by the author and accepted by S01 in chat on 2026-10-19; it overlaps MIL-002 and MIL-003, which do not touch the runners' network.
- MIL-013 (source code comments are in English) runs from 2026-11-09 to its Target Date, 2026-11-20, proposed by the author and accepted by S01 in chat on 2026-10-19; it overlaps MIL-009, whose work is on wiki callers and does not touch the runners or the reviewer of the check.
- Work on a milestone starts only after its `MIL-*` document is `Accepted`. - Work on a milestone starts only after its `MIL-*` document is `Accepted`.
- Dates were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002). - Dates were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
@@ -41,6 +43,8 @@ workflows. The Business Case sets no deadline, so the dates are the plan's own;
| Migrate the wiki callers and remove the scoped workflow | [MIL-009] | 2026-11-16 to 2026-11-30 | 2026-11-30 | S05 | None yet | Seven repositories publish through a caller, scoped workflow removed, ADR-0004 superseded, released as 2.0.0 | Not created yet (the sync creates it when MIL-009 is Accepted) | | Migrate the wiki callers and remove the scoped workflow | [MIL-009] | 2026-11-16 to 2026-11-30 | 2026-11-30 | S05 | None yet | Seven repositories publish through a caller, scoped workflow removed, ADR-0004 superseded, released as 2.0.0 | Not created yet (the sync creates it when MIL-009 is Accepted) |
| Code metrics and coverage badges | [MIL-010] | 2027-01-18 to 2027-01-30 | 2027-01-30 | S05 | None yet (task 1 writes them) | Reusable workflow that counts code files and lines of code, reads coverage and commits SVG badges, released | Not created yet (the sync creates it when MIL-010 is Accepted) | | Code metrics and coverage badges | [MIL-010] | 2027-01-18 to 2027-01-30 | 2027-01-30 | S05 | None yet (task 1 writes them) | Reusable workflow that counts code files and lines of code, reads coverage and commits SVG badges, released | Not created yet (the sync creates it when MIL-010 is Accepted) |
| Fix the defaults of the code metrics workflow | [MIL-011] | 2026-10-14 to 2026-10-23 | 2026-10-23 | S05 | None (technical tasks) | Patch release 2.1.1: the default `tools-ref` of every reusable workflow names a tag that holds its scripts, a test for it, guides and wiki at `@v2`, the framework Badges page without the workaround | Not created yet (the sync creates it when MIL-011 is Accepted) | | Fix the defaults of the code metrics workflow | [MIL-011] | 2026-10-14 to 2026-10-23 | 2026-10-23 | S05 | None (technical tasks) | Patch release 2.1.1: the default `tools-ref` of every reusable workflow names a tag that holds its scripts, a test for it, guides and wiki at `@v2`, the framework Badges page without the workaround | Not created yet (the sync creates it when MIL-011 is Accepted) |
| VPN actions for deployment | [MIL-012] | 2026-10-26 to 2026-11-06 | 2026-11-06 | S05 | None (technical tasks) | Release 2.2.0: composite actions `connect-wireguard` and `connect-tailscale` verified on a runner of the instance, with guides, wiki in three languages and the framework templates pinned to the release | Not created yet (the sync creates it when MIL-012 is Accepted) |
| Source code comments are in English | [MIL-013] | 2026-11-09 to 2026-11-20 | 2026-11-20 | S05 | None (technical tasks) | Release 2.3.0: reusable workflow `english-comments.yml` that fails a pull request with a non-English comment and tells the author, with its ADR, tests, guides and wiki, and the rule proposed in the framework | Not created yet (the sync creates it when MIL-013 is Accepted) |
```plantuml ```plantuml
@startgantt @startgantt
@@ -67,6 +71,10 @@ Project starts 2026-10-05
[MIL-010 Go/No-Go] happens 2027-01-30 [MIL-010 Go/No-Go] happens 2027-01-30
[MIL-011 Defaults fix] starts 2026-10-14 and ends 2026-10-23 [MIL-011 Defaults fix] starts 2026-10-14 and ends 2026-10-23
[MIL-011 Go/No-Go] happens 2026-10-23 [MIL-011 Go/No-Go] happens 2026-10-23
[MIL-012 VPN actions] starts 2026-10-26 and ends 2026-11-06
[MIL-012 Go/No-Go] happens 2026-11-06
[MIL-013 English comments] starts 2026-11-09 and ends 2026-11-20
[MIL-013 Go/No-Go] happens 2026-11-20
@endgantt @endgantt
``` ```
@@ -84,6 +92,8 @@ Project starts 2026-10-05
| The wiki publisher as a workflow that repositories call, from docs/wiki | [MIL-008] | | The wiki publisher as a workflow that repositories call, from docs/wiki | [MIL-008] |
| Moving every publishing repository to the called workflow and removing the scoped one | [MIL-009] | | Moving every publishing repository to the called workflow and removing the scoped one | [MIL-009] |
| A shared workflow for code metrics and coverage badges | [MIL-010], [MIL-011] | | A shared workflow for code metrics and coverage badges | [MIL-010], [MIL-011] |
| Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale | [MIL-012] |
| A shared workflow that checks that comments in source code are in English and tells the author | [MIL-013] |
## Dependencies ## Dependencies
@@ -93,6 +103,8 @@ MIL-001 → MIL-002 → MIL-003 → MIL-004
MIL-003 + MIL-004 → MIL-007 MIL-003 + MIL-004 → MIL-007
MIL-006 → MIL-008 → MIL-009 MIL-006 → MIL-008 → MIL-009
MIL-003 + MIL-004 → MIL-010 → MIL-011 MIL-003 + MIL-004 → MIL-010 → MIL-011
MIL-004 + MIL-011 → MIL-012
MIL-004 + MIL-010 + MIL-011 → MIL-013
``` ```
A No-Go moves every later window by the rework time of the failed milestone. A No-Go moves every later window by the rework time of the failed milestone.
@@ -106,6 +118,8 @@ A No-Go moves every later window by the rework time of the failed milestone.
| Eight repositories still publish through the scoped workflow that MIL-008 deprecates (MIL-008 task 5) | Removing it would stop their wikis | It is deprecated and kept; a follow-up milestone migrates them and removes it in a major release (RC-061). | | Eight repositories still publish through the scoped workflow that MIL-008 deprecates (MIL-008 task 5) | Removing it would stop their wikis | It is deprecated and kept; a follow-up milestone migrates them and removes it in a major release (RC-061). |
| An organisation has no `TOKEN_FOR_GITEA_WIKI` secret, or a repository fails its first run (MIL-009 tasks 3 and 4) | The wiki of that repository stops updating | MIL-009 task 5 reads each wiki back after the first run; the scoped workflow is removed only after every repository passes. | | An organisation has no `TOKEN_FOR_GITEA_WIKI` secret, or a repository fails its first run (MIL-009 tasks 3 and 4) | The wiki of that repository stops updating | MIL-009 task 5 reads each wiki back after the first run; the scoped workflow is removed only after every repository passes. |
| The token of a run cannot push to a `badges` branch, or the SVG does not render on the mirror (MIL-010) | The delivery of the badges changes shape | MIL-010 task 3 verifies it first; fall back to a badge file in a path of the default branch or to a run artifact. | | The token of a run cannot push to a `badges` branch, or the SVG does not render on the mirror (MIL-010) | The delivery of the badges changes shape | MIL-010 task 3 verifies it first; fall back to a badge file in a path of the default branch or to a run artifact. |
| The runner cannot give a job `NET_ADMIN` and `/dev/net/tun`, or `tailscaled` does not survive to the next step (MIL-012) | The actions need another shape (a different start of the daemon, or userspace mode only) | MIL-012 tasks 2 to 4 verify it first on a runner of the instance; the release waits for criteria 2 and 3. |
| The check calls a correct comment non-English, or misses a Danish or Malay one (MIL-013) | Authors lose trust in the check, or it is turned off | MIL-013 tasks 1 and 2 measure the method on a corpus before anything is built, and the exception marker and the allowed-words file (criterion 4) take care of the rest. |
| Security review by S05 is a bottleneck | Milestone decisions slip | Keep milestones small and plan review time before each decision date. | | Security review by S05 is a bottleneck | Milestone decisions slip | Keep milestones small and plan review time before each decision date. |
## Open Issues ## Open Issues
@@ -282,6 +296,8 @@ no tests, no release process.
[MIL-009]: ./milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md [MIL-009]: ./milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
[MIL-010]: ./milestones/mil-010-code-metrics-badges.md [MIL-010]: ./milestones/mil-010-code-metrics-badges.md
[MIL-011]: ./milestones/mil-011-code-metrics-default-tools-ref.md [MIL-011]: ./milestones/mil-011-code-metrics-default-tools-ref.md
[MIL-012]: ./milestones/mil-012-vpn-actions-for-deployment.md
[MIL-013]: ./milestones/mil-013-english-comments-check.md
[Milestone MIL-001]: https://git.tirsystem.com/TirSystem/github-action/milestone/18 [Milestone MIL-001]: https://git.tirsystem.com/TirSystem/github-action/milestone/18
[Milestone MIL-002]: https://git.tirsystem.com/TirSystem/github-action/milestone/19 [Milestone MIL-002]: https://git.tirsystem.com/TirSystem/github-action/milestone/19
[Milestone MIL-003]: https://git.tirsystem.com/TirSystem/github-action/milestone/20 [Milestone MIL-003]: https://git.tirsystem.com/TirSystem/github-action/milestone/20
@@ -290,7 +306,5 @@ no tests, no release process.
[Milestone MIL-006]: https://git.tirsystem.com/TirSystem/github-action/milestone/94 [Milestone MIL-006]: https://git.tirsystem.com/TirSystem/github-action/milestone/94
[Milestone MIL-007]: https://git.tirsystem.com/TirSystem/github-action/milestone/95 [Milestone MIL-007]: https://git.tirsystem.com/TirSystem/github-action/milestone/95
[Milestone MIL-008]: https://git.tirsystem.com/TirSystem/github-action/milestone/152 [Milestone MIL-008]: https://git.tirsystem.com/TirSystem/github-action/milestone/152
[b74ee0e]: https://git.tirsystem.com/TirSystem/github-action/commit/b74ee0e5cfd04f25ba5f2c1addc80699147c479a [c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
[2f267df]: https://git.tirsystem.com/TirSystem/github-action/commit/2f267df2a270087cb7dc482af4ce4f4341e97b03 [e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
[5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e
[02e3794]: https://git.tirsystem.com/TirSystem/github-action/commit/02e3794d7b05799af2544ca303c99271da788e82
@@ -0,0 +1,50 @@
# Review Record: MIL-011 outcome (release 2.1.1)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-079 |
| CrossReference | [MIL-011], [RC-078] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; Go; accepted by the AI as stand-in for S01 in autonomous mode, not independent | [4aa3fd3] |
---
## Artifact Under Review
- Instance reviewed: the outcome of [MIL-011]: the release `v2.1.1` of this repository (tag on `3023461`, the release page, the moving major tag `v2`) and the framework `Badges` page (`TirSystem/sqa-qc-framework` pull request 57)
- Checklist used: the eight Go/No-Go criteria of [MIL-011]; criteria 1 to 5 were scored in [RC-078] and are not repeated
- Scope: re-score of criteria 6, 7 and 8, after the release
- Language and domain: n/a (technical type)
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 to 5 | See [RC-078] | Pass | Reproduced (run 1499), every default checked, the default derived from `MAJOR_TAG` with a failing seeded test, no `@v1` for `code-metrics.yml`, a run on the instance (run 1509). |
| 6 | The release is a patch (2.1.1); `v2` moves to it, `v1` is untouched | Pass | The Release workflow ran in dry-run mode (run 1524: "Checks passed for v2.1.1 on 3023461 ... would create the release page v2.1.1 and point v2 to it") and then for real (run 1525). `git ls-remote`: `v2.1.1` and `v2` (peeled) are `3023461`; `v1` is `1.2.2` (`9bf0a2b`), unchanged. The release page has the changelog entry as its text. After the release a call at `@v2` with the defaults ran on the instance (run 1526, job 1670): `tools-ref: v2`, the scripts fetched at `3023461`, badges published to `badges-verify`. |
| 7 | The framework `Badges` page no longer tells a caller to set `tools-ref` | Pass | `TirSystem/sqa-qc-framework` pull request 57 is merged and the wiki is published: the three pages have no `tools-ref: v2` line and no bullet about it, call the workflow at `@v2`, and say to use 2.1.1 or later, not 2.1.0. |
| 8 | Reviewed | Pass | [RC-076], [RC-077], [RC-078] and this record, each with a verdict of Go; [MIL-011] and its plan row are `Accepted`. |
## Overall Verdict
Go - all eight criteria pass. The defect is fixed in release 2.1.1: the default `tools-ref` of the five reusable workflows is the current major (`MAJOR_TAG`), a test keeps it so, and a call at `@v2` with the defaults runs. Two things stay as they are and are reported, not decided here: release `2.1.0` still has the defect (a release tag is never moved, ADR-0006) and callers are told to use 2.1.1 or later; and the other four workflows now fetch their scripts from the current major instead of `1.2.2`, which changes no behaviour for a caller at `@v1`. The AI accepts the record as stand-in for S01 in autonomous mode; there is no independent reader and the decision stays open to challenge in the run summary.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Delete the throwaway branch `verify-code-metrics-defaults` when the evidence is no longer needed (a branch deletion is left to a person) | S05 | 2026-10-23 |
| Decide whether the required status check `Sync GitHub mirror metadata / sync-metadata (push)` should stay required on pull requests, where it is only `skipped` and blocks a merge through the API | S05 | 2026-10-30 |
---
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
[RC-078]: ./rc-078-mil-011-fix.md
[RC-076]: ./rc-076-mil-011-plan.md
[RC-077]: ./rc-077-mil-011-task-1-results.md
[4aa3fd3]: https://git.tirsystem.com/TirSystem/github-action/commit/4aa3fd38f97c26a5d52abc566e958759583ccb85
+73
View File
@@ -0,0 +1,73 @@
# Review Record: MIL-012 and PP-001 (MIL-012)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-080 |
| CrossReference | [MIL-012], [PP-001], [BC-001], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01, after three corrections found in the first read | [c5d5941] |
---
## Artifact Under Review
- Instance reviewed: [MIL-012]; delta of [PP-001] for the added milestone
- Checklist used: [QC-MIL-001] for the milestone; for the plan, the criteria of `PP.md` (no QC checklist exists for PP-001)
- Scope: full review of MIL-012; delta re-review of PP-001, earlier record [RC-076]
- Language and domain: n/a (technical type)
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | `## Deliverable` names release 2.2.0 with the two composite actions, their guides, README section, wiki pages in three languages, changelog entry and test; a record of one successful job per action; an ADR; and the framework templates pinned to the release tag. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Ten criteria, each with a checkable Go and No-Go: the `uses:` form that resolves, a recorded run per action with the cleanup shown, no secret in any log (a search with a count), verified runner requirements, linting with a seeded error, a search that finds no branch reference, the release level and tags, the framework pull request, the review record. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | [MIL-004] (the Release workflow) and [MIL-011] (`MAJOR_TAG`), [ADR-0006], and the two outside dependencies on S03 (runner, test peers) and on the branch of `TirSystem/sqa-qc-framework`; in the plan's dependency diagram (`MIL-004 + MIL-011 -> MIL-012`). The first read found the operator named S05; the Business Case and the Stakeholder Analysis give runners and secrets to S03, now corrected. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objective O6 and success criterion 6 of [BC-001], added in the same change and reviewed in [RC-081]. The first read found the milestone calling O6 "proposed in task 9", which would have left it untraceable until later; now the objective exists and task 9 writes the ADR only. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S05, approving reviewer S01; both are in [SA-001]. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-06, confirmed by S01 in chat on 2026-10-19; the Business Case sets no deadline. The window (2026-10-26 to 2026-11-06) overlaps [MIL-002] and [MIL-003], which are documentation and quality checks and do not use the runner's network; the plan says so. |
## Plan delta (PP-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Every milestone has a `MIL-*` document and the plan's dates equal that document's target date | Pass | MIL-001 to MIL-012 exist; the MIL-012 decision date (2026-11-06) equals its Target Date. |
| 2 | Every Business Case scope item is covered by a gateway | Pass | The new scope item "Shared composite actions that connect a deployment job to a private network" is covered by [MIL-012] in the scope table. |
| 3 | The schedule respects the Business Case constraints | Pass | No deadline in the Business Case; the window is two weeks. |
| 4 | Dependencies between milestones are stated | Pass | `MIL-004 + MIL-011 -> MIL-012`. |
| 5 | Every gateway has an owner who is a stakeholder ID | Pass | S05. |
| 6 | Plan content is internally consistent | Pass | The first read found a broken link target in the milestone (a wrong ADR-0006 file name and two milestone file names), now checked by resolving every link of the file. The Purpose says "twelve", and the Gantt, the schedule, the scope table and the dependencies agree. |
| 7 | Open Issues are current | Pass | A plan risk is added: the runner cannot give `NET_ADMIN` and `/dev/net/tun`, or `tailscaled` does not survive to the next step, with the mitigation in tasks 2 to 4. |
## Overall Verdict
Go - all criteria pass after three corrections found in the first read (the operator S05 where the Business Case says S03, the objective O6 left as a proposal, and three wrong link targets). The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
| Task 2: ask the operator for the `vpn` runner and the test peers before task 3 starts; nothing in this milestone creates them | S03 | 2026-10-30 |
| The code of both actions, their tests and guides already exist on the unreleased branch `wireguard-vpn-action`; commit it with `Task: MIL-012#N` trailers (tasks 3, 4, 6 and 7 own it) only after the milestone is published as issues | S05 | Before the first commit on that branch |
---
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
[PP-001]: ../../project-plan.md
[BC-001]: ../../business-case.md
[SA-001]: ../../stakeholder-analysis.md
[MIL-002]: ../../milestones/mil-002-onboarding-and-operations.md
[MIL-003]: ../../milestones/mil-003-quality-checks.md
[MIL-004]: ../../milestones/mil-004-versioned-release.md
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
[ADR-0006]: ../../adr/adr-0006-release-process-and-version-pinning.md
[RC-076]: ./rc-076-mil-011-plan.md
[RC-081]: ./rc-081-bc-001-o6.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
+53
View File
@@ -0,0 +1,53 @@
# Review Record: BC-001 (objective O6, VPN actions for deployment)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-081 |
| CrossReference | [BC-001], [QC-BC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [c5d5941] |
---
## Artifact Under Review
- Instance reviewed: [BC-001]
- Checklist used: [QC-BC-001]
- Scope: delta re-review of criteria 2, 3, 4 and 7 (the change adds objective O6, an In Scope bullet, an assumption, a constraint and success criterion 6); criteria 1, 5, 6 and 8 are re-read and unchanged. Earlier record: [RC-067]
- Language and domain: n/a for this delta (the action item of [RC-067] about the Language and Domain rows is still open)
- Language reviewer: none
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | Unchanged; the Cost-Benefit Assessment labels its qualitative form. |
| 2 | Risks are identified with documented impact and mitigation | Pass | The delivery risk of the VPN actions (the runner cannot give the privileges, or the daemon does not survive between steps) is a plan risk in PP-001 with a mitigation; the Business Case risks about secrets already cover a leaked key. |
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | Criterion 6: "2 (WireGuard and Tailscale) by MIL-012", measured by the recorded runs of tasks 3 to 5, with no secret in the run log. |
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | "Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale (MIL-012)" is a new In Scope bullet. Out of Scope is unchanged and consistent: operating runners stays out, and the new assumption says S03 provides the privileged runner. |
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | Unchanged; the new assumption names S03. |
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Unchanged. |
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | The assumption (S03 provides a runner with `NET_ADMIN` and `/dev/net/tun`) and the constraint (a VPN key or token never appears in a log, an artifact or a command line) are in their own sections. |
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Unchanged: Proceed. |
## Overall Verdict
Go - all eight criteria pass for the revision that adds objective O6, its scope item, assumption, constraint and success criterion 6. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; there is no independent reader and the decision stays open to challenge.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| The Metadata table of BC-001 has no `Language` and `Domain` rows (QC-LANG-001 criterion 1); this predates this change and is carried over from [RC-067]. Add the rows or record in an ADR why they are not needed | S01 | Before the next material change to BC-001 |
---
[BC-001]: ../../business-case.md
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
[RC-067]: ./rc-067-bc-001-o5.md
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
@@ -0,0 +1,65 @@
# Review Record: MIL-012 results of tasks 1, 6 and the review fixes (MIL-012)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-082 |
| CrossReference | [MIL-012], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [3325bea] |
---
## Artifact Under Review
- Instance reviewed: [MIL-012]
- Checklist used: [QC-MIL-001]
- Scope: delta re-review of the milestone after [RC-080]: the new `## Verification results` section (task 1, task 6, the fixes made after the review of pull request 194), the added `[ADR-0011]` in the CrossReference, and the claims about open tasks. Criteria 1 to 6 are re-read; none of the milestone's own text for them changed. [ADR-0011] is **not** reviewed here: it stays `Proposed` and needs a review against its own checklist
- Language and domain: n/a (technical type)
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Unchanged: release 2.2.0 with the two actions, guides, wiki pages in three languages, ADR, framework templates pinned. The results section does not claim any of it is delivered; it lists tasks 2 to 5, 7, 8 and 10 to 12 as open. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Unchanged, ten criteria. The results bear on two of them. Criterion 1 (a `uses:` of a subfolder action resolves, the form recorded): runs 417 and 418 show both forms resolve at `@main`, and the failure at the deleted branch is explained by the log text "reference not found". Criterion 6 (scripts linted in CI, a seeded error fails): met by `tools/lint-action-scripts.sh` and its tests on the working branch; the pull-request run of the new step is the final evidence. The other eight criteria are not claimed. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged; [ADR-0011] is added to the CrossReference and exists. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Unchanged: objective O6 and success criterion 6 of [BC-001], reviewed in [RC-081]. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: owner S05, approving reviewer S01. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2026-11-06. The open tasks 2 to 5 need the operator's runner (S03); the milestone states the dependency, and the date is at risk until the runner exists. |
## Results check
| # | Claim in the results | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Task 1: both forms resolve at `@main` | Pass | Run 418 (`.../actions/runs/2425`): jobs `wg-short`, `wg-full`, `ts-short`, `ts-full` each executed the action's `mask` step and main step and failed with the action's own `[configuration]` error. Read from the job logs through the API. |
| 2 | Run 417 failed because the branch was deleted | Pass | `git ls-remote` showed no `wireguard-vpn-action` head after pull request 194 merged; the log says `Unable to resolve wireguard-vpn-action: reference not found`. |
| 3 | Task 6: lint script, workflow step, pinned tools, seeded-error test | Pass | `tools/lint-action-scripts.sh`; the test file seeds a shellcheck problem and a badly formatted script, shows both fail, and shows the two action scripts pass; the full suite (590 tests) passes locally. Not yet run by the instance's CI. |
| 4 | The ten findings of the review of pull request 194 are answered | Pass | Each has a code change and an offline test (the daemon wait with a delayed stub, `sudo` that resets the environment, a host daemon not taken over, a line break refused, the key file removed on failure, `latest` refused). The wording of the results says they are offline-tested with stubs and that tasks 3 and 4 must show them on a runner. |
| 5 | Open tasks are listed honestly | Pass | Tasks 2, 3, 4, 5, 7, 8, 10, 11 and 12 are named as open, with the reason. |
## Overall Verdict
Go - the results recorded are accurate and claim no more than the evidence shows. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Review [ADR-0011] against its checklist and accept or change it; it is `Proposed` | S01 | Before task 10 (release 2.2.0) |
| Provide the `vpn` runner, the WireGuard peer and the Tailscale tag with an OAuth client for tasks 3 and 4; the target date 2026-11-06 depends on it | S03 | 2026-10-30 |
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
---
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
[BC-001]: ../../business-case.md
[ADR-0011]: ../../adr/adr-0011-vpn-composite-actions.md
[RC-080]: ./rc-080-mil-012-plan.md
[RC-081]: ./rc-081-bc-001-o6.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
+74
View File
@@ -0,0 +1,74 @@
# Review Record: MIL-013 and PP-001 (MIL-013)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-083 |
| CrossReference | [MIL-013], [PP-001], [BC-001], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01, after one correction found in the first read | [e35b1c5] |
---
## Artifact Under Review
- Instance reviewed: [MIL-013]; delta of [PP-001] for the added milestone
- Checklist used: [QC-MIL-001] for the milestone; for the plan, the criteria of `PP.md` (no QC checklist exists for PP-001)
- Scope: full review of MIL-013; delta re-review of PP-001, earlier record [RC-080]
- Language and domain: n/a (technical type)
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | `## Deliverable` names release 2.3.0 with the reusable workflow, its scripts under `tools/`, guide, README section, wiki pages in three languages, changelog entry, offline tests, an ADR for the detection method and a review record, and the two pull requests in the other repositories. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Eleven criteria, each with a checkable Go and No-Go. The first is measurable (a corpus of named size, recall of at least 90 % for Danish and Malay, at most one false positive per 200 English comments); the others are runs, searches or a verified setting. Criterion 7 (the merge is blocked) depends on a setting that only the repository owner makes, and says so. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | [MIL-010] (the comment scanner), [MIL-004] (the Release workflow), [MIL-011] (`MAJOR_TAG`), [ADR-0005]; the outside dependencies (a Gitea token from S03, branch protection by S01 or S03, the framework and checklist repositories) are named. The first read noted that the planned window of [MIL-004] (2026-11-16 to 2026-11-28) starts after this milestone does; [MIL-004] is already released (`v1`, `v2`), so the dependency is met and the plan says nothing wrong. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objective O7 and success criterion 7 of [BC-001], added in the same change and reviewed in [RC-084]. The first read found the traceability row still saying O7 was "added at acceptance of this milestone", which is now false; it states the objective plainly. |
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S05, approving reviewer S01; both are in [SA-001]. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-20, confirmed by S01 in chat on 2026-10-19; the Business Case sets no deadline. The window (2026-11-09 to 2026-11-20) follows [MIL-012] (2026-11-06) and overlaps [MIL-009], which is wiki callers and does not use the check's token or reviewer. |
## Plan delta (PP-001)
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | Every milestone has a `MIL-*` document and the plan's dates equal that document's target date | Pass | MIL-001 to MIL-013 exist; the MIL-013 decision date (2026-11-20) equals its Target Date. |
| 2 | Every Business Case scope item is covered by a gateway | Pass | The new scope item "A shared workflow that checks that comments in source code are in English and tells the author" is covered by [MIL-013] in the scope table. |
| 3 | The schedule respects the Business Case constraints | Pass | No deadline in the Business Case; the window is two weeks. |
| 4 | Dependencies between milestones are stated | Pass | `MIL-004 + MIL-010 + MIL-011 -> MIL-013`. |
| 5 | Every gateway has an owner who is a stakeholder ID | Pass | S05. |
| 6 | Plan content is internally consistent | Pass | The Purpose says "thirteen"; the Gantt, the schedule, the scope table, the dependencies and the planning assumption agree. Unused commit link definitions left by the history roll were removed. |
| 7 | Open Issues are current | Pass | A plan risk is added: the check calls a correct comment non-English or misses a Danish or Malay one, with the mitigation in tasks 1 and 2 and the exception marker. |
## Overall Verdict
Go - all criteria pass after one correction found in the first read (the stale "added at acceptance" wording). The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
| Task 9: decide which repositories mark the check as required and set it; the workflow cannot, and the AI does not change branch protection | S01 | Before task 8 ends |
| Ask the operator for a Gitea token that may comment on pull requests, stored as an organization secret, before task 8 | S03 | 2026-11-13 |
---
[MIL-013]: ../../milestones/mil-013-english-comments-check.md
[PP-001]: ../../project-plan.md
[BC-001]: ../../business-case.md
[SA-001]: ../../stakeholder-analysis.md
[MIL-004]: ../../milestones/mil-004-versioned-release.md
[MIL-009]: ../../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
[MIL-010]: ../../milestones/mil-010-code-metrics-badges.md
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
[ADR-0005]: ../../adr/adr-0005-quality-check-contract.md
[RC-080]: ./rc-080-mil-012-plan.md
[RC-084]: ./rc-084-bc-001-o7.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
+54
View File
@@ -0,0 +1,54 @@
# Review Record: BC-001 (objective O7, English comments check)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-084 |
| CrossReference | [BC-001], [QC-BC-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [e35b1c5] |
---
## Artifact Under Review
- Instance reviewed: [BC-001]
- Checklist used: [QC-BC-001]
- Scope: delta re-review of criteria 2, 3, 4 and 7 (the change adds objective O7, an In Scope bullet, an assumption, a constraint and success criterion 7); criteria 1, 5, 6 and 8 are re-read and unchanged. Earlier record: [RC-081]
- Language and domain: n/a for this delta (the action item of [RC-067] about the Language and Domain rows is still open)
- Language reviewer: none
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | Unchanged; the Cost-Benefit Assessment labels its qualitative form. |
| 2 | Risks are identified with documented impact and mitigation | Pass | The delivery risk (the check wrongly flags a correct comment, or misses a Danish or Malay one) is a plan risk in PP-001 with a mitigation: a measured corpus before the build, an exception marker and an allowed-words file. |
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | Criterion 7: "0 after MIL-013" non-English comments reaching the default branch of a repository that calls the check, measured by the check's run on the default branch and a throwaway pull request that cannot be merged. |
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | "A shared workflow that checks that comments in source code are in English and tells the author (MIL-013)" is a new In Scope bullet. Out of Scope is unchanged and does not contradict it: "application code of consuming repositories" stays out because the check only reads comments and reports them, and changes nothing in the consuming repository; "operating the Gitea instance or runners" stays out because branch protection is set by S01 or S03, as the new assumption says. |
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | Unchanged; the new assumption names S01 and S03. |
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Unchanged. |
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | The assumption (S03 provides a token that may comment on pull requests; S01 or S03 mark the check required) and the constraint (no source text leaves the instance, the token never appears in a log) are in their own sections. |
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Unchanged: Proceed. |
## Overall Verdict
Go - all eight criteria pass for the revision that adds objective O7, its scope item, assumption, constraint and success criterion 7. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; there is no independent reader and the decision stays open to challenge.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| The Metadata table of BC-001 has no `Language` and `Domain` rows (QC-LANG-001 criterion 1); this predates this change and is carried over from [RC-067]. Add the rows or record in an ADR why they are not needed | S01 | Before the next material change to BC-001 |
---
[BC-001]: ../../business-case.md
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
[RC-067]: ./rc-067-bc-001-o5.md
[RC-081]: ./rc-081-bc-001-o6.md
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
@@ -0,0 +1,67 @@
# Review Record: MIL-013 results of tasks 1 to 8, 10 and 13 (MIL-013)
## Metadata
| Key | Value |
| --- | --- |
| ID | RC-085 |
| CrossReference | [MIL-013], [QC-MIL-001] |
## Version History
| Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- |
| 2026-10-21 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [88f852a] |
---
## Artifact Under Review
- Instance reviewed: [MIL-013]
- Checklist used: [QC-MIL-001]
- Scope: delta re-review of the milestone after [RC-083]: the new `## Verification results` section (the results of the merged work, the trial on the instance and its three defects, the search of the logs), and the added `[ADR-0012]` in the CrossReference. Criteria 1 to 6 are re-read; none of the milestone's own text for them changed. [ADR-0012] is **not** reviewed here: it stays `Proposed` and needs a review against its own checklist
- Language and domain: n/a (technical type)
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-21 for the acceptance and stands in for S01; the AI prepared the evidence)
## Checklist Results
| # | Criterion | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | A concrete deliverable is defined for every gate | Pass | Unchanged: release 2.3.0 with the workflow, scripts, guide, wiki, tests, ADR and the two pull requests in the other repositories. The results do not claim any of it is released. |
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Unchanged, eleven criteria. The results bear on criterion 1 (corpus rates, met on the corpus, with the limits of an AI-written corpus said), 3 (a pull request with a Danish comment fails, one without passes: met in runs 440 and 445), 4 (an exception can be marked: met, the marked line was not reported), 5 (one comment that mentions the author, updated, resolved: met when the fix branch is called) and 6 (no token in a log: met for the four jobs searched; the detector made no network call). Criteria 7 to 11 are not claimed. |
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged; [ADR-0012] is added to the CrossReference and exists. |
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Unchanged: objective O7 and success criterion 7 of [BC-001] ([RC-084]). |
| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: owner S05, approving reviewer S01. |
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2026-11-20. Open tasks 9, 11, 12 and 14 and the merge of pull request 212 are before it; task 9 needs the owner. |
## Results check
| # | Claim in the results | Status | Evidence/Notes |
| --- | --- | --- | --- |
| 1 | The first call at `@main` found only the Malay comment and skipped the author step | Pass | Read from the log of job 2761 of run 433 (id 2478): one finding at `trial/sample.py:9`, and the step list shows "Tell the author" skipped. |
| 2 | In a called workflow `gitea.event_name` is `workflow_call` | Pass | Printed by the debug workflow in runs 435 and 437: `event_name=workflow_call number=213 author=Tirsvad base_ref=main`; the steps with the event-name conditions are listed as skipped, the others as success. |
| 3 | The fix branch finds both comments, writes one comment, updates it, resolves it | Pass | Runs 440, 443 and 445 read from the API: the comment list of pull request 213 held one comment (id 8715) after each run; its text after run 445 was the "passes" text without a mention. |
| 4 | The run link equals the run's address | Pass | The link in the comment after run 447 is `https://git.tirsystem.com/TirSystem/github-action/actions/runs/2492`, the `html_url` of run 447. |
| 5 | No credential in the logs | Pass | A search of four job logs; the only lines matching were `REPORT_TOKEN: ***`. The search patterns cannot find a short or oddly formed secret, which the result says by naming the patterns. |
| 6 | The open items are listed honestly | Pass | The result names what the trial did not show: a corrected call at `@main`, a fork pull request, a custom token, the required check, and the call by this repository. |
## Overall Verdict
Go - the results recorded are accurate and claim no more than the evidence shows. The author confirmed the acceptance in chat on 2026-10-21 as stand-in for S01; this is not an independent review.
## Action Items
| Action | Owner | Due |
| --- | --- | --- |
| Review [ADR-0012] against its checklist and accept or change it; it is `Proposed` | S01 | Before task 11 (release 2.3.0) |
| Review and merge pull request 212 (the three defects); call the corrected workflow at `@main` once more | S01 | 2026-11-06 |
| Task 9: decide which repositories mark the check as required and set it | S01 | 2026-11-13 |
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-09 |
---
[MIL-013]: ../../milestones/mil-013-english-comments-check.md
[BC-001]: ../../business-case.md
[ADR-0012]: ../../adr/adr-0012-english-comment-detection.md
[RC-083]: ./rc-083-mil-013-plan.md
[RC-084]: ./rc-084-bc-001-o7.md
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
+30 -7
View File
@@ -4,13 +4,13 @@
| Key | Value | | Key | Value |
| --- | --- | | --- | --- |
| ID | TM-001 | | ID | TM-001 |
| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [RC-074], [RC-075], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] | | CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013], [ADR-0011], [ADR-0012], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [RC-074], [RC-075], [RC-076], [RC-077], [RC-078], [RC-079], [RC-080], [RC-081], [RC-082], [RC-083], [RC-084], [RC-085], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] |
## Version History ## Version History
| Date | Status | Author | Reviewer | Change | Commit | | Date | Status | Author | Reviewer | Change | Commit |
| --- | --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- | --- |
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-011 and RC-076 (fix the defaults of the code metrics workflow) | [3d7a9df] | | 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-013 (English comments check), RC-083 and RC-084 (its plan and objective O7) | [e35b1c5] |
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-077 and RC-078 (MIL-011 task 1 results, and the fix of the defaults) | [a6892c3] | | 2026-10-21 | Proposed | Jens Tirsvad Nielsen | S01 | Added ADR-0012 and RC-085 (MIL-013 results of the trial) | [88f852a] |
--- ---
@@ -25,13 +25,15 @@ updated whenever an artifact instance is created or reviewed.
| Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) | | Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| [SA-001] | SA | - | [BC-001], [UCD-001] | [RC-004] | | [SA-001] | SA | - | [BC-001], [UCD-001] | [RC-004] |
| [BC-001] | BC | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006] | [RC-001], [RC-008], [RC-067] | | [BC-001] | BC | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-012], [MIL-013] | [RC-001], [RC-008], [RC-067], [RC-081], [RC-084] |
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [US-001] | [RC-002], [RC-009], [RC-015], [RC-017], [RC-025], [RC-032], [RC-053], [RC-062], [RC-064], [RC-068] | | [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013], [US-001] | [RC-002], [RC-009], [RC-015], [RC-017], [RC-025], [RC-032], [RC-053], [RC-062], [RC-064], [RC-068], [RC-080], [RC-083] |
| [MIL-001] | MIL | [BC-001], [PP-001] | [MIL-002] | [RC-003], [RC-026] | | [MIL-001] | MIL | [BC-001], [PP-001] | [MIL-002] | [RC-003], [RC-026] |
| [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] | | [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] |
| [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] | | [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] |
| [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] | | [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] |
| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072], [RC-073] | | [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072], [RC-073] |
| [ADR-0011] | ADR | [MIL-012], [ADR-0005], [ADR-0006] | - | - |
| [ADR-0012] | ADR | [MIL-013], [ADR-0005], [ADR-0010] | - | - |
| [RC-001] | RC | [BC-001] | - | - | | [RC-001] | RC | [BC-001] | - | - |
| [RC-002] | RC | [PP-001] | - | - | | [RC-002] | RC | [PP-001] | - | - |
| [RC-003] | RC | [MIL-001] | - | - | | [RC-003] | RC | [MIL-001] | - | - |
@@ -44,7 +46,9 @@ updated whenever an artifact instance is created or reviewed.
| [MIL-008] | MIL | [BC-001], [PP-001], [MIL-006], [ADR-0004] | [MIL-009] | [RC-060], [RC-061] | | [MIL-008] | MIL | [BC-001], [PP-001], [MIL-006], [ADR-0004] | [MIL-009] | [RC-060], [RC-061] |
| [MIL-009] | MIL | [BC-001], [PP-001], [MIL-008], [ADR-0004], [ADR-0006], [ADR-0009] | - | [RC-064], [RC-066] | | [MIL-009] | MIL | [BC-001], [PP-001], [MIL-008], [ADR-0004], [ADR-0006], [ADR-0009] | - | [RC-064], [RC-066] |
| [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | [MIL-011] | [RC-068], [RC-074], [RC-075] | | [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | [MIL-011] | [RC-068], [RC-074], [RC-075] |
| [MIL-011] | MIL | [BC-001], [PP-001], [MIL-010], [ADR-0006], [ADR-0010] | - | [RC-076], [RC-077], [RC-078] | | [MIL-011] | MIL | [BC-001], [PP-001], [MIL-010], [ADR-0006], [ADR-0010] | [MIL-012] | [RC-076], [RC-077], [RC-078], [RC-079] |
| [MIL-012] | MIL | [BC-001], [PP-001], [MIL-004], [MIL-011], [ADR-0006], [ADR-0011] | - | [RC-080], [RC-082] |
| [MIL-013] | MIL | [BC-001], [PP-001], [MIL-004], [MIL-010], [MIL-011], [ADR-0005], [ADR-0006], [ADR-0010] | - | [RC-083], [RC-085] |
| [RC-007] | RC | [MIL-004] | - | - | | [RC-007] | RC | [MIL-004] | - | - |
| [RC-008] | RC | [BC-001] | - | - | | [RC-008] | RC | [BC-001] | - | - |
| [RC-009] | RC | [PP-001] | - | - | | [RC-009] | RC | [PP-001] | - | - |
@@ -136,6 +140,13 @@ updated whenever an artifact instance is created or reviewed.
| [RC-076] | RC | [MIL-011], [PP-001] | - | - | | [RC-076] | RC | [MIL-011], [PP-001] | - | - |
| [RC-077] | RC | [MIL-011] | - | - | | [RC-077] | RC | [MIL-011] | - | - |
| [RC-078] | RC | [MIL-011] | - | - | | [RC-078] | RC | [MIL-011] | - | - |
| [RC-079] | RC | [MIL-011] | - | - |
| [RC-080] | RC | [MIL-012], [PP-001] | - | - |
| [RC-081] | RC | [BC-001] | - | - |
| [RC-082] | RC | [MIL-012] | - | - |
| [RC-083] | RC | [MIL-013], [PP-001] | - | - |
| [RC-084] | RC | [BC-001] | - | - |
| [RC-085] | RC | [MIL-013] | - | - |
## Coverage Notes ## Coverage Notes
@@ -159,8 +170,12 @@ updated whenever an artifact instance is created or reviewed.
[MIL-008]: ../milestones/mil-008-wiki-reusable-workflow.md [MIL-008]: ../milestones/mil-008-wiki-reusable-workflow.md
[MIL-010]: ../milestones/mil-010-code-metrics-badges.md [MIL-010]: ../milestones/mil-010-code-metrics-badges.md
[MIL-011]: ../milestones/mil-011-code-metrics-default-tools-ref.md [MIL-011]: ../milestones/mil-011-code-metrics-default-tools-ref.md
[MIL-012]: ../milestones/mil-012-vpn-actions-for-deployment.md
[MIL-013]: ../milestones/mil-013-english-comments-check.md
[MIL-009]: ../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md [MIL-009]: ../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md [ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
[ADR-0011]: ../adr/adr-0011-vpn-composite-actions.md
[ADR-0012]: ../adr/adr-0012-english-comment-detection.md
[RC-001]: ./reviews/rc-001-business-case.md [RC-001]: ./reviews/rc-001-business-case.md
[RC-002]: ./reviews/rc-002-project-plan.md [RC-002]: ./reviews/rc-002-project-plan.md
[RC-003]: ./reviews/rc-003-mil-001.md [RC-003]: ./reviews/rc-003-mil-001.md
@@ -249,6 +264,13 @@ updated whenever an artifact instance is created or reviewed.
[RC-076]: ./reviews/rc-076-mil-011-plan.md [RC-076]: ./reviews/rc-076-mil-011-plan.md
[RC-077]: ./reviews/rc-077-mil-011-task-1-results.md [RC-077]: ./reviews/rc-077-mil-011-task-1-results.md
[RC-078]: ./reviews/rc-078-mil-011-fix.md [RC-078]: ./reviews/rc-078-mil-011-fix.md
[RC-079]: ./reviews/rc-079-mil-011-outcome.md
[RC-080]: ./reviews/rc-080-mil-012-plan.md
[RC-081]: ./reviews/rc-081-bc-001-o6.md
[RC-082]: ./reviews/rc-082-mil-012-results.md
[RC-083]: ./reviews/rc-083-mil-013-plan.md
[RC-084]: ./reviews/rc-084-bc-001-o7.md
[RC-085]: ./reviews/rc-085-mil-013-results.md
[ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md [ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md
[ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md [ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md
[ADR-0007]: ../adr/adr-0007-release-publish-destination.md [ADR-0007]: ../adr/adr-0007-release-publish-destination.md
@@ -259,4 +281,5 @@ updated whenever an artifact instance is created or reviewed.
[UC-005]: ../uc-005/uc.md [UC-005]: ../uc-005/uc.md
[ADR-0005]: ../adr/adr-0005-quality-check-contract.md [ADR-0005]: ../adr/adr-0005-quality-check-contract.md
[3d7a9df]: https://git.tirsystem.com/TirSystem/github-action/commit/3d7a9df3a3a4712dcc5214a71b0569d90122bf4e [3d7a9df]: https://git.tirsystem.com/TirSystem/github-action/commit/3d7a9df3a3a4712dcc5214a71b0569d90122bf4e
[a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb [e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
+2 -1
View File
@@ -1,4 +1,4 @@
<!-- translation of Capabilities.md, source sha256 3e3bcd9498f7 --> <!-- translation of Capabilities.md, source sha256 a8c14f722a77 -->
> Dette er en oversættelse af [`Capabilities`](Capabilities). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres. > Dette er en oversættelse af [`Capabilities`](Capabilities). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres.
@@ -17,4 +17,5 @@ Rækker, der siger Implementeret eller Vejledninger er skrevet, findes i dag. Re
| Fælles kvalitetskontrol (Python og shell) | Implementeret som det genbrugelige workflow `.gitea/workflows/quality-checks.yml`, med offline-tests og et forsøg i en anden organisation; taget i brug i RepoFoundry ([vejledning](QualityChecksPythonShell.da)) | | Fælles kvalitetskontrol (Python og shell) | Implementeret som det genbrugelige workflow `.gitea/workflows/quality-checks.yml`, med offline-tests og et forsøg i en anden organisation; taget i brug i RepoFoundry ([vejledning](QualityChecksPythonShell.da)) |
| Fælles kvalitetskontrol (C og C++, C#) | Implementeret som de genbrugelige workflows `.gitea/workflows/quality-checks-cpp.yml` og `.gitea/workflows/quality-checks-csharp.yml` med præcise værktøjsversioner og offline-tests; endnu ikke brugt i et andet repository (MIL-007) ([vejledning til C og C++](QualityChecksCpp.da), [vejledning til C#](QualityChecksCSharp.da)) | | Fælles kvalitetskontrol (C og C++, C#) | Implementeret som de genbrugelige workflows `.gitea/workflows/quality-checks-cpp.yml` og `.gitea/workflows/quality-checks-csharp.yml` med præcise værktøjsversioner og offline-tests; endnu ikke brugt i et andet repository (MIL-007) ([vejledning til C og C++](QualityChecksCpp.da), [vejledning til C#](QualityChecksCSharp.da)) |
| Kodemålinger og dækningsbadges | Implementeret og udgivet i 2.1.0 som det genbrugelige workflow `.gitea/workflows/code-metrics.yml` med en tæller, en Cobertura-læser, en SVG-renderer og offline-tests; afprøvet i et kasserepository, endnu ikke brugt i et projektrepository (MIL-010) ([vejledning](CodeMetrics.da)) | | Kodemålinger og dækningsbadges | Implementeret og udgivet i 2.1.0 som det genbrugelige workflow `.gitea/workflows/code-metrics.yml` med en tæller, en Cobertura-læser, en SVG-renderer og offline-tests; afprøvet i et kasserepository, endnu ikke brugt i et projektrepository (MIL-010) ([vejledning](CodeMetrics.da)) |
| Engelske kommentarer | Implementeret som det genbrugelige workflow `.gitea/workflows/english-comments.yml` med en kommentarudtrækker, en sprogdetektor målt på et korpus (ADR-0012), en kommentar på pull requesten og offline-tests; endnu ikke kørt på instansen eller krævet af noget repository (MIL-013) ([vejledning](EnglishComments.da)) |
| Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; udgivelsesworkflowet findes, og `v1.0.0` er udgivet (MIL-004) ([vejledning](Upgrading.da)) | | Versioneret udgivelse og publicering | Udgivelsesproces, changelog og opgraderingsvejledning er defineret; udgivelsesworkflowet findes, og `v1.0.0` er udgivet (MIL-004) ([vejledning](Upgrading.da)) |
+1
View File
@@ -17,4 +17,5 @@ Rows that say Implemented or Guides written exist today. The rest is planned and
| Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](QualityChecksPythonShell)) | | Shared quality checks (Python and shell) | Implemented as the reusable workflow `.gitea/workflows/quality-checks.yml`, with offline tests and a trial in a second organization; adopted in RepoFoundry ([guide](QualityChecksPythonShell)) |
| Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](QualityChecksCpp), [C# guide](QualityChecksCSharp)) | | Shared quality checks (C and C++, C#) | Implemented as the reusable workflows `.gitea/workflows/quality-checks-cpp.yml` and `.gitea/workflows/quality-checks-csharp.yml` with exact tool versions and offline tests; not yet used in another repository (MIL-007) ([C and C++ guide](QualityChecksCpp), [C# guide](QualityChecksCSharp)) |
| Code metrics and coverage badges | Implemented and released in 2.1.0 as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](CodeMetrics)) | | Code metrics and coverage badges | Implemented and released in 2.1.0 as the reusable workflow `.gitea/workflows/code-metrics.yml` with a counter, a Cobertura reader, an SVG renderer and offline tests; tried in a throwaway repository, not yet used in a project repository (MIL-010) ([guide](CodeMetrics)) |
| English comments | Implemented as the reusable workflow `.gitea/workflows/english-comments.yml` with a comment extractor, a language detector measured on a corpus (ADR-0012), a pull request comment and offline tests; not yet run on the instance or required by any repository (MIL-013) ([guide](EnglishComments)) |
| Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](Upgrading)) | | Versioned release and publishing | Release process, changelog and upgrade guide defined; the release workflow exists and `v1.0.0` is released (MIL-004) ([guide](Upgrading)) |
+2 -1
View File
@@ -1,4 +1,4 @@
<!-- translation of Capabilities.md, source sha256 3e3bcd9498f7 --> <!-- translation of Capabilities.md, source sha256 a8c14f722a77 -->
> Ini ialah terjemahan [`Capabilities`](Capabilities). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya. > Ini ialah terjemahan [`Capabilities`](Capabilities). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya.
@@ -17,4 +17,5 @@ Baris yang menyatakan Dilaksanakan atau Panduan telah ditulis wujud hari ini. Se
| Semakan kualiti bersama (Python dan shell) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks.yml`, dengan ujian luar talian dan percubaan dalam organisasi kedua; digunakan dalam RepoFoundry ([panduan](QualityChecksPythonShell.ms)) | | Semakan kualiti bersama (Python dan shell) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks.yml`, dengan ujian luar talian dan percubaan dalam organisasi kedua; digunakan dalam RepoFoundry ([panduan](QualityChecksPythonShell.ms)) |
| Semakan kualiti bersama (C dan C++, C#) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks-cpp.yml` dan `.gitea/workflows/quality-checks-csharp.yml` dengan versi alat yang tepat dan ujian luar talian; belum digunakan dalam repositori lain (MIL-007) ([panduan C dan C++](QualityChecksCpp.ms), [panduan C#](QualityChecksCSharp.ms)) | | Semakan kualiti bersama (C dan C++, C#) | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/quality-checks-cpp.yml` dan `.gitea/workflows/quality-checks-csharp.yml` dengan versi alat yang tepat dan ujian luar talian; belum digunakan dalam repositori lain (MIL-007) ([panduan C dan C++](QualityChecksCpp.ms), [panduan C#](QualityChecksCSharp.ms)) |
| Metrik kod dan lencana liputan | Dilaksanakan dan diterbitkan dalam 2.1.0 sebagai aliran kerja boleh guna semula `.gitea/workflows/code-metrics.yml` dengan pengira, pembaca Cobertura, pemaparan SVG dan ujian luar talian; dicuba dalam repositori sekali pakai, belum digunakan dalam repositori projek (MIL-010) ([panduan](CodeMetrics.ms)) | | Metrik kod dan lencana liputan | Dilaksanakan dan diterbitkan dalam 2.1.0 sebagai aliran kerja boleh guna semula `.gitea/workflows/code-metrics.yml` dengan pengira, pembaca Cobertura, pemaparan SVG dan ujian luar talian; dicuba dalam repositori sekali pakai, belum digunakan dalam repositori projek (MIL-010) ([panduan](CodeMetrics.ms)) |
| Komen dalam bahasa Inggeris | Dilaksanakan sebagai aliran kerja boleh guna semula `.gitea/workflows/english-comments.yml` dengan pengekstrak komen, pengesan bahasa yang diukur pada korpus (ADR-0012), komen pada pull request dan ujian luar talian; belum dijalankan pada instans atau diwajibkan oleh mana-mana repositori (MIL-013) ([panduan](EnglishComments.ms)) |
| Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; aliran kerja keluaran sudah ada dan `v1.0.0` telah dikeluarkan (MIL-004) ([panduan](Upgrading.ms)) | | Keluaran berversi dan penerbitan | Proses keluaran, changelog dan panduan naik taraf telah ditakrifkan; aliran kerja keluaran sudah ada dan `v1.0.0` telah dikeluarkan (MIL-004) ([panduan](Upgrading.ms)) |
+108
View File
@@ -0,0 +1,108 @@
<!-- translation of EnglishComments.md, source sha256 9dafcabbc807 -->
> Dette er en oversættelse af [`EnglishComments`](EnglishComments). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres.
[English](EnglishComments) | Dansk | [Bahasa Melayu](EnglishComments.ms)
# Kommentarer i kildekoden på engelsk
Sådan kalder en softwareudvikler det fælles workflow, der kontrollerer, at
kommentarerne og docstrings i en pull request er skrevet på engelsk. Workflowet
lader kørslen fejle, når én ikke er det, og skriver én kommentar på pull
requesten, der nævner forfatteren. Det ændrer ingen fil og sender ingen
kildetekst ud af instansen. Beslutningen bag er ADR-0012 i dette repository.
## Kald af workflowet
```yaml
name: English comments
on:
pull_request:
jobs:
english-comments:
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
with:
exclude: "docs/generated/**"
allowed-words-file: .english-comments-allowed-words
```
| Input | Standard | Betydning |
| --- | --- | --- |
| `scope` | `changed` | `changed` kontrollerer kommentarerne på de linjer, pull requesten tilføjer; `all` kontrollerer hver sporet fil |
| `exclude` | | Kommasepareret liste af fil- eller stimønstre, der ikke kontrolleres (`*` og `**` er jokertegn) |
| `allowed-words-file` | | En fil i repositoryet med ord, der aldrig bedømmes, ét pr. linje; `#` starter en kommentar |
| `threshold` | `0.6` | Sandsynligheden, over 0 og højst 1, hvorfra en kommentar regnes for ikke at være engelsk |
| `min-words` | `3` | En kommentar med færre ord bedømmes ikke |
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | De fastlåste pakker, detektoren bruger |
| `tools-ref` | den nuværende hovedversion | Det tag i dette repository, hvis scripts køres; fastlås det sammen med workflowet |
Den valgfri secret `REPORT_TOKEN` er et Gitea-token, der må kommentere på pull
requests. Uden den bruger workflowet kørslens eget token, så kommentaren
skrives af Actions-botten. En pull request fra en fork har et token med kun
læseadgang: kontrollen får stadig kørslen til at fejle, men der skrives ingen
kommentar.
## Hvad der kontrolleres
Kommentarerne og docstrings i filer på de sprog, kodetælleren kender
([kodemålinger](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
JavaScript, TypeScript, Go, Rust og SQL. Strenge og kode læses ikke. Kommentarlinjer
efter hinanden danner en blok. Hver linje i en blok bedømmes for sig (mindst fire ord og en
sandsynlighed på mindst 0,7), så en engelsk linje ikke kan skjule en dansk ved siden af; når ingen linje
markeres, bedømmes blokken som helhed (mindst `min-words` ord og `threshold`), så en sætning delt over korte
linjer ikke overses.
Før en blok bedømmes, fjernes alt, der ikke er et ord: stier, URL'er, tilvalg
(`--output`), filnavne, identifikatorer (`snake_case`, `camelCase`), tal,
akronymer og konstanter (`JSON`), en indledende etiket som `Usage:` og ordene
i filen med tilladte ord. En blok med færre end `min-words` ord tilbage
bedømmes ikke, så en kort kommentar slipper igennem. Kommentarer, der
konfigurerer et værktøj (`noqa`, `type:`, `pragma`, `shellcheck`), læses ikke.
## Når kontrollen fejler
Kørslen fejler, hvert fund vises som en annotation (`::error file=...,line=...`)
og i loggen som `file:line: looks Danish (0.93): "the comment"`, og én kommentar
på pull requesten opregner dem og nævner forfatteren. En ny kørsel opdaterer den
kommentar; når pull requesten består, markeres kommentaren som løst og nævner
ikke længere nogen.
Ret det ved at skrive kommentaren om på engelsk. For et citat eller et navn, der
ikke er engelsk, kan du enten skrive `english-comments: ignore` i kommentaren
(det springer hele den kommentarblok over, den står i), eller tilføje ordet til
filen med tilladte ord.
## Blokering af sammenfletningen
En fejlende kørsel blokerer ikke i sig selv en sammenfletning. Markér kontrollen
som påkrævet i grenbeskyttelsen af målgrenen: kør workflowet én gang på en pull
request og vælg dets post på listen over statuskontroller (det præcise navn
dannes af det kaldende workflow, dets job og hændelsen). At indstille
grenbeskyttelse er repositoryejerens handling; workflowet ændrer den ikke.
## Hvor præcis den er
Detektoren er `langid`, begrænset til engelsk, dansk, norsk, svensk, tysk,
fransk, malajisk, indonesisk og hollandsk. På repositoryets korpus markerer den
ingen af 300 engelske kommentarer, 44 af 45 danske og 41 af 45 malajiske
kommentarer og alle 48 tyske, svenske, norske og franske (ADR-0012). Dens
grænser:
- en kommentar på ét eller to ord, eller en der mest består af navne, bedømmes ikke;
- en kommentar på dårligt engelsk slipper igennem, og en kommentar, der blander
engelske fagudtryk med få danske eller malajiske ord, kan slippe igennem;
- korpussets danske og malajiske kommentarer er skrevet til testen og ikke samlet
fra rigtig kode, så rigtig kode vil vise andre fejl. Meld en misset kommentar
eller et falsk fund med kommentaren, så kommer den med i korpussen.
## Fejlfinding
| Besked | Årsag |
| --- | --- |
| `[configuration] give --base` | Workflowet blev startet af en hændelse, der ikke er en pull request, med `scope: changed`; kald det på `pull_request` eller brug `scope: all` |
| `[configuration] git diff failed` | Basisgrenen findes ikke i checkout; workflowet henter hele historikken, så tjek at pull requesten har en målgren |
| `[configuration] langid is not installed` | Installationstrinnet fejlede; tjek de fastlåste versioner |
| `[permissions] ... may not comment on pull requests` | Tokenet må ikke kommentere; giv `REPORT_TOKEN` skriveadgang til issues og pull requests |
| Et fund, der ikke er et fund | Tilføj ordene til filen med tilladte ord, eller `english-comments: ignore` til kommentaren |
+106
View File
@@ -0,0 +1,106 @@
<!-- Generated from guides/english-comments.md by tools/build_wiki.py. Edit the source, not this page. -->
> This page is generated from [`guides/english-comments.md`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/english-comments.md). Changes made here are overwritten the next time the wiki is published.
English | [Dansk](EnglishComments.da) | [Bahasa Melayu](EnglishComments.ms)
# Keeping source code comments in English
How a software engineer calls the shared workflow that checks that the comments
and docstrings of a pull request are written in English. The workflow fails the
run when one is not, and writes one comment on the pull request that mentions
the author. It changes no file and sends no source text outside the instance.
Its decision is ADR-0012 in this repository.
## Calling the workflow
```yaml
name: English comments
on:
pull_request:
jobs:
english-comments:
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
with:
exclude: "docs/generated/**"
allowed-words-file: .english-comments-allowed-words
```
| Input | Default | Meaning |
| --- | --- | --- |
| `scope` | `changed` | `changed` checks the comments on the lines the pull request adds; `all` checks every tracked file |
| `exclude` | | Comma list of file name or path patterns that are not checked (`*` and `**` are wildcards) |
| `allowed-words-file` | | A file of the repository with words that are never judged, one per line, `#` starts a comment |
| `threshold` | `0.6` | The probability, above 0 and at most 1, from which a comment counts as not English |
| `min-words` | `3` | A comment with fewer words is not judged |
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | The pinned packages the detector needs |
| `tools-ref` | the current major | The tag of this repository whose scripts are run; pin it together with the workflow |
The optional secret `REPORT_TOKEN` is a Gitea token that may comment on pull
requests. Without it the workflow uses the token of the run, so the comment is
written by the Actions bot. A pull request from a fork has a read-only token:
the check still fails the run, but no comment is written.
## What is checked
The comments and docstrings of the files of the languages in the code counter
([code metrics](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
JavaScript, TypeScript, Go, Rust and SQL. Strings and code are not read.
Consecutive comment lines form a block. Each line of a block is judged on its own
(at least four words and a probability of at least 0.7), so an English line cannot
hide a Danish one next to it; when no line is flagged, the block is judged as a whole
(at least `min-words` words and `threshold`), so a sentence cut over short lines is not missed.
Before a block is judged, everything that is not a word is removed: paths, URLs,
options (`--output`), file names, identifiers (`snake_case`, `camelCase`),
numbers, acronyms and constants (`JSON`), a leading label such as `Usage:`, and
the words of the allowed-words file. A block with fewer than `min-words` words
left is not judged, so a short comment passes. Comments that configure a tool
(`noqa`, `type:`, `pragma`, `shellcheck`) are not read.
## When the check fails
The run fails, each finding is shown as an annotation (`::error file=...,line=...`)
and in the log as `file:line: looks Danish (0.93): "the comment"`, and one comment
on the pull request lists them and mentions the author. A new run updates that
comment; when the pull request passes, the comment is marked resolved and no
longer mentions anyone.
Fix it by rewriting the comment in English. For a quotation or a name that is
not English, either put `english-comments: ignore` in the comment (it skips the
whole comment block it is in), or add the word to the allowed-words file.
## Blocking the merge
A failing run does not block a merge by itself. In the branch protection of the
target branch, mark the check as required: run the workflow once on a pull
request and choose its entry in the list of status checks (the exact name is
made from the calling workflow, its job and the event). Setting branch
protection is the repository owner's action; the workflow does not change it.
## How accurate it is
The detector is `langid`, limited to English, Danish, Norwegian, Swedish,
German, French, Malay, Indonesian and Dutch. On the corpus of the repository it
flags none of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments
and all of the 48 German, Swedish, Norwegian and French ones (ADR-0012). Its
limits:
- a comment of one or two words, or one that is mostly names, is not judged;
- a comment in poor English passes, and a comment that mixes English technical
terms with few Danish or Malay words can pass;
- the Danish and Malay comments of the corpus were written for the test, not
collected from real code, so real code will show other misses. Report a miss
or a false positive with the comment, and it joins the corpus.
## Troubleshooting
| Message | Cause |
| --- | --- |
| `[configuration] give --base` | The workflow was started by an event that is not a pull request with `scope: changed`; call it on `pull_request` or use `scope: all` |
| `[configuration] git diff failed` | The base branch is not in the checkout; the workflow fetches the full history, so check that the pull request has a target branch |
| `[configuration] langid is not installed` | The install step failed; check the pinned versions |
| `[permissions] ... may not comment on pull requests` | The token cannot comment; give `REPORT_TOKEN` write access to issues and pull requests |
| A finding that is not a finding | Add the words to the allowed-words file, or `english-comments: ignore` to the comment |
+110
View File
@@ -0,0 +1,110 @@
<!-- translation of EnglishComments.md, source sha256 9dafcabbc807 -->
> Ini ialah terjemahan [`EnglishComments`](EnglishComments). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya.
[English](EnglishComments) | [Dansk](EnglishComments.da) | Bahasa Melayu
# Memastikan komen kod sumber dalam bahasa Inggeris
Cara seorang jurutera perisian memanggil aliran kerja bersama yang menyemak bahawa
komen dan docstring sesuatu pull request ditulis dalam bahasa Inggeris. Aliran
kerja menggagalkan larian apabila ada yang bukan bahasa Inggeris, dan menulis satu
komen pada pull request yang menyebut penulisnya. Ia tidak mengubah sebarang fail
dan tidak menghantar teks sumber keluar daripada instans. Keputusan di
belakangnya ialah ADR-0012 dalam repositori ini.
## Memanggil aliran kerja
```yaml
name: English comments
on:
pull_request:
jobs:
english-comments:
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
with:
exclude: "docs/generated/**"
allowed-words-file: .english-comments-allowed-words
```
| Input | Lalai | Maksud |
| --- | --- | --- |
| `scope` | `changed` | `changed` menyemak komen pada baris yang ditambah oleh pull request; `all` menyemak setiap fail yang dijejaki |
| `exclude` | | Senarai corak nama fail atau laluan yang dipisahkan koma dan tidak disemak (`*` dan `**` ialah kad liar) |
| `allowed-words-file` | | Fail dalam repositori dengan perkataan yang tidak pernah dinilai, satu pada setiap baris; `#` memulakan komen |
| `threshold` | `0.6` | Kebarangkalian, melebihi 0 dan paling banyak 1, yang mulai daripadanya sesuatu komen dianggap bukan bahasa Inggeris |
| `min-words` | `3` | Komen dengan perkataan yang lebih sedikit tidak dinilai |
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | Pakej tetap yang diperlukan oleh pengesan |
| `tools-ref` | major semasa | Tag repositori ini yang skripnya dijalankan; tetapkan bersama aliran kerja |
Secret pilihan `REPORT_TOKEN` ialah token Gitea yang boleh memberi komen pada pull
request. Tanpanya, aliran kerja menggunakan token larian itu sendiri, jadi komen
ditulis oleh bot Actions. Pull request daripada fork mempunyai token baca sahaja:
semakan masih menggagalkan larian, tetapi tiada komen ditulis.
## Apa yang disemak
Komen dan docstring dalam fail bahasa yang dikenali oleh pengira kod
([metrik kod](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
JavaScript, TypeScript, Go, Rust dan SQL. String dan kod tidak dibaca. Baris komen
yang berturutan membentuk satu blok. Setiap baris dalam blok dinilai secara berasingan (sekurang-kurangnya
empat perkataan dan kebarangkalian sekurang-kurangnya 0.7), jadi baris bahasa Inggeris tidak dapat menyembunyikan
baris bahasa Denmark di sebelahnya; apabila tiada baris ditandakan, blok dinilai secara keseluruhan
(sekurang-kurangnya `min-words` perkataan dan `threshold`), supaya ayat yang dipecahkan kepada baris pendek tidak terlepas.
Sebelum sesuatu blok dinilai, segala yang bukan perkataan dibuang: laluan, URL,
pilihan (`--output`), nama fail, pengecam (`snake_case`, `camelCase`), nombor,
akronim dan pemalar (`JSON`), label di permulaan seperti `Usage:`, dan perkataan
dalam fail perkataan yang dibenarkan. Blok yang tinggal kurang daripada
`min-words` perkataan tidak dinilai, jadi komen pendek lulus. Komen yang
mengkonfigurasi alat (`noqa`, `type:`, `pragma`, `shellcheck`) tidak dibaca.
## Apabila semakan gagal
Larian gagal, setiap penemuan dipaparkan sebagai anotasi (`::error file=...,line=...`)
dan dalam log sebagai `file:line: looks Danish (0.93): "the comment"`, dan satu
komen pada pull request menyenaraikannya serta menyebut penulisnya. Larian baharu
mengemas kini komen itu; apabila pull request lulus, komen ditandakan selesai dan
tidak lagi menyebut sesiapa.
Betulkannya dengan menulis semula komen dalam bahasa Inggeris. Untuk petikan atau
nama yang bukan bahasa Inggeris, sama ada letakkan `english-comments: ignore`
dalam komen (ia melangkau seluruh blok komen tempat ia berada), atau tambahkan
perkataan itu ke dalam fail perkataan yang dibenarkan.
## Menyekat penggabungan
Larian yang gagal tidak menyekat penggabungan dengan sendirinya. Dalam
perlindungan cabang bagi cabang sasaran, tandakan semakan sebagai wajib: jalankan
aliran kerja sekali pada sebuah pull request dan pilih entrinya dalam senarai
semakan status (nama tepatnya dibentuk daripada aliran kerja pemanggil, kerjanya
dan peristiwa). Menetapkan perlindungan cabang ialah tindakan pemilik repositori;
aliran kerja tidak mengubahnya.
## Sejauh mana ia tepat
Pengesan ialah `langid`, terhad kepada bahasa Inggeris, Denmark, Norway, Sweden,
Jerman, Perancis, Melayu, Indonesia dan Belanda. Pada korpus repositori, ia
menandakan tiada satu pun daripada 300 komen bahasa Inggeris, 44 daripada 45
komen bahasa Denmark dan 41 daripada 45 komen bahasa Melayu, serta kesemua 48
komen bahasa Jerman, Sweden, Norway dan Perancis (ADR-0012). Batasannya:
- komen satu atau dua perkataan, atau yang kebanyakannya nama, tidak dinilai;
- komen dalam bahasa Inggeris yang lemah lulus, dan komen yang mencampurkan istilah
teknikal bahasa Inggeris dengan beberapa perkataan Denmark atau Melayu boleh lulus;
- komen bahasa Denmark dan Melayu dalam korpus ditulis untuk ujian, bukan dikumpulkan
daripada kod sebenar, jadi kod sebenar akan menunjukkan kesilapan lain. Laporkan
komen yang terlepas atau penemuan palsu bersama komennya, dan ia akan masuk ke
dalam korpus.
## Diagnosis
| Mesej | Punca |
| --- | --- |
| `[configuration] give --base` | Aliran kerja dimulakan oleh peristiwa yang bukan pull request dengan `scope: changed`; panggil pada `pull_request` atau gunakan `scope: all` |
| `[configuration] git diff failed` | Cabang asas tiada dalam checkout; aliran kerja mengambil seluruh sejarah, jadi semak bahawa pull request mempunyai cabang sasaran |
| `[configuration] langid is not installed` | Langkah pemasangan gagal; semak versi yang ditetapkan |
| `[permissions] ... may not comment on pull requests` | Token tidak boleh memberi komen; berikan `REPORT_TOKEN` akses tulis kepada isu dan pull request |
| Penemuan yang bukan penemuan | Tambahkan perkataan ke dalam fail perkataan yang dibenarkan, atau `english-comments: ignore` pada komen |
+44 -3
View File
@@ -1,4 +1,4 @@
<!-- translation of Home.md, source sha256 9b967f9e1aff --> <!-- translation of Home.md, source sha256 9b84b5cf7534 -->
> 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,12 +13,12 @@ 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 ti workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki (kaldt af hvert repository), fælles kvalitetskontrol for Python og shell, for C og C++ og for C#, kodemålinger og dækningsbadges, et udgivelsesworkflow, publicering af accepterede milepæle og repositoryets egen validering. De er listet med formål og diagram under [Workflows](#workflows). Det indeholder elleve workflows: synkronisering af metadata til GitHub-spejlet, publicering af wiki (kaldt af hvert repository), fælles kvalitetskontrol for Python og shell, for C og C++ og for C#, kodemålinger og dækningsbadges, en kontrol af, at kommentarer er på engelsk, 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 ti 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 elleve 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 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
@@ -28,6 +28,7 @@ Der er ti workflows i dette repository. Et **scoped** workflow leveres af instan
| 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-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 | | 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 |
| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Tæller kodefilerne og kodelinjerne (uden kommentar- og tomme linjer), læser en Cobertura-dækningsrapport og committer tre SVG-badges til en `badges`-gren | | Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository | Tæller kodefilerne og kodelinjerne (uden kommentar- og tomme linjer), læser en Cobertura-dækningsrapport og committer tre SVG-badges til en `badges`-gren |
| English comments | [`english-comments.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/english-comments.yml) | Genbrugeligt (`workflow_call`) | et kald fra et andet repository, på en pull request | Bedømmer sproget i de kommentarer og docstrings, en pull request tilføjer, lader kørslen fejle, når én ikke er engelsk, og skriver én kommentar på pull requesten, der nævner forfatteren |
| Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Kun dette repository | push til `main`, der ændrer `docs/wiki/**`, manuelt | Kalder det genbrugelige workflow Publish wiki pages for dette repositorys wiki | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Kun dette repository | push til `main`, der ændrer `docs/wiki/**`, manuelt | Kalder det genbrugelige workflow Publish wiki pages for dette repositorys wiki |
| 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 |
@@ -279,6 +280,46 @@ deactivate Workflow
@enduml @enduml
``` ```
### Engelske kommentarer
Et genbrugeligt workflow, der kontrollerer, at de kommentarer og docstrings, en pull request tilføjer, er skrevet på engelsk, lader kørslen fejle, når én ikke er det, og skriver én kommentar på pull requesten, der nævner forfatteren. Det ændrer ingen fil og sender ingen kildetekst ud af instansen; at markere kontrollen som påkrævet i grenbeskyttelsen er det, der blokerer sammenfletningen ([vejledning](EnglishComments.da), ADR-0012).
#### Workflow
```plantuml
@startuml
title English comments check (reusable workflow)
autonumber
actor "Pull request
uses: ...english-comments.yml@v2" as Caller
participant "Gitea Actions
english-comments" as Workflow
participant "Gitea REST API" as Gitea
Caller -> Workflow: workflow_call with scope, exclude,
allowed-words-file, threshold and min-words
activate Workflow
Workflow -> Workflow: Check out the full history and the shared scripts
Workflow -> Workflow: Install the pinned language detector
Workflow -> Workflow: Read the comments on the added lines
Workflow -> Workflow: Remove paths, options, identifiers and allowed words
Workflow -> Workflow: Judge the language of each comment block
alt a block is not English
Workflow -> Gitea: Create or update one comment that mentions the author
Workflow --> Caller: run failed (the required check blocks the merge)
else all blocks are English
Workflow -> Gitea: Mark an earlier comment resolved
Workflow --> Caller: run passed
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)).
+43 -2
View File
@@ -13,12 +13,12 @@ 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 ten workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, 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 eleven workflows: metadata sync to the GitHub mirror, wiki publishing (called by each repository), shared quality checks for Python and shell, for C and C++ and for C#, code metrics and coverage badges, a check that comments are in English, 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
Ten 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. Eleven 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 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
@@ -28,6 +28,7 @@ Ten workflows live in this repository. A **scoped** workflow is delivered by the
| 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-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 | | 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 |
| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch | | Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Reusable (`workflow_call`) | a call from another repository | Counts the code files and the lines of code (without comment and blank lines), reads a Cobertura coverage report and commits three SVG badges to a `badges` branch |
| English comments | [`english-comments.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/english-comments.yml) | Reusable (`workflow_call`) | a call from another repository, on a pull request | Judges the language of the comments and docstrings a pull request adds, fails the run when one is not English and writes one comment on the pull request that mentions the author |
| Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | This repository only | push to `main` that changes `docs/wiki/**`, manual | Calls the reusable Publish wiki pages workflow for the wiki of this repository |
| 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 |
@@ -279,6 +280,46 @@ deactivate Workflow
@enduml @enduml
``` ```
### English comments
A reusable workflow that checks that the comments and docstrings a pull request adds are written in English, fails the run when one is not, and writes one comment on the pull request that mentions the author. It changes no file and sends no source text outside the instance; marking the check as required in the branch protection is what blocks the merge ([guide](EnglishComments), ADR-0012).
#### Workflow
```plantuml
@startuml
title English comments check (reusable workflow)
autonumber
actor "Pull request
uses: ...english-comments.yml@v2" as Caller
participant "Gitea Actions
english-comments" as Workflow
participant "Gitea REST API" as Gitea
Caller -> Workflow: workflow_call with scope, exclude,
allowed-words-file, threshold and min-words
activate Workflow
Workflow -> Workflow: Check out the full history and the shared scripts
Workflow -> Workflow: Install the pinned language detector
Workflow -> Workflow: Read the comments on the added lines
Workflow -> Workflow: Remove paths, options, identifiers and allowed words
Workflow -> Workflow: Judge the language of each comment block
alt a block is not English
Workflow -> Gitea: Create or update one comment that mentions the author
Workflow --> Caller: run failed (the required check blocks the merge)
else all blocks are English
Workflow -> Gitea: Mark an earlier comment resolved
Workflow --> Caller: run passed
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)).
+44 -3
View File
@@ -1,4 +1,4 @@
<!-- translation of Home.md, source sha256 9b967f9e1aff --> <!-- translation of Home.md, source sha256 9b84b5cf7534 -->
> 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,12 +13,12 @@ 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 sepuluh aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki (dipanggil oleh setiap repositori), semakan kualiti bersama untuk Python dan shell, untuk C dan C++ serta untuk C#, metrik kod dan lencana liputan, 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 sebelas aliran kerja: penyegerakan metadata ke cermin GitHub, penerbitan wiki (dipanggil oleh setiap repositori), semakan kualiti bersama untuk Python dan shell, untuk C dan C++ serta untuk C#, metrik kod dan lencana liputan, semakan bahawa komen ditulis dalam bahasa Inggeris, 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 sepuluh 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 sebelas 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 |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
@@ -28,6 +28,7 @@ Terdapat sepuluh aliran kerja dalam repositori ini. Aliran kerja **scoped** diha
| 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-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 | | 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 |
| Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Mengira fail kod dan baris kod (tanpa baris komen dan baris kosong), membaca laporan liputan Cobertura dan melakukan commit tiga lencana SVG ke cabang `badges` | | Code metrics | [`code-metrics.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/code-metrics.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain | Mengira fail kod dan baris kod (tanpa baris komen dan baris kosong), membaca laporan liputan Cobertura dan melakukan commit tiga lencana SVG ke cabang `badges` |
| English comments | [`english-comments.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/english-comments.yml) | Boleh guna semula (`workflow_call`) | panggilan daripada repositori lain, pada pull request | Menilai bahasa komen dan docstring yang ditambah oleh pull request, menggagalkan larian apabila ada yang bukan bahasa Inggeris dan menulis satu komen pada pull request yang menyebut penulisnya |
| Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Repositori ini sahaja | push ke `main` yang mengubah `docs/wiki/**`, manual | Memanggil aliran kerja boleh guna semula Publish wiki pages untuk wiki repositori ini | | Publish own wiki | [`publish-own-wiki.yml`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/.gitea/workflows/publish-own-wiki.yml) | Repositori ini sahaja | push ke `main` yang mengubah `docs/wiki/**`, manual | Memanggil aliran kerja boleh guna semula Publish wiki pages untuk wiki repositori ini |
| 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 |
@@ -279,6 +280,46 @@ deactivate Workflow
@enduml @enduml
``` ```
### Komen dalam bahasa Inggeris
Aliran kerja boleh guna semula yang menyemak bahawa komen dan docstring yang ditambah oleh pull request ditulis dalam bahasa Inggeris, menggagalkan larian apabila ada yang tidak, dan menulis satu komen pada pull request yang menyebut penulisnya. Ia tidak mengubah sebarang fail dan tidak menghantar teks sumber keluar daripada instans; menandakan semakan sebagai wajib dalam perlindungan cabang ialah yang menyekat penggabungan ([panduan](EnglishComments.ms), ADR-0012).
#### Aliran kerja
```plantuml
@startuml
title English comments check (reusable workflow)
autonumber
actor "Pull request
uses: ...english-comments.yml@v2" as Caller
participant "Gitea Actions
english-comments" as Workflow
participant "Gitea REST API" as Gitea
Caller -> Workflow: workflow_call with scope, exclude,
allowed-words-file, threshold and min-words
activate Workflow
Workflow -> Workflow: Check out the full history and the shared scripts
Workflow -> Workflow: Install the pinned language detector
Workflow -> Workflow: Read the comments on the added lines
Workflow -> Workflow: Remove paths, options, identifiers and allowed words
Workflow -> Workflow: Judge the language of each comment block
alt a block is not English
Workflow -> Gitea: Create or update one comment that mentions the author
Workflow --> Caller: run failed (the required check blocks the merge)
else all blocks are English
Workflow -> Gitea: Mark an earlier comment resolved
Workflow --> Caller: run passed
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)).
+3
View File
@@ -12,6 +12,7 @@
- [Quality checks (C and C++)](QualityChecksCpp) - [Quality checks (C and C++)](QualityChecksCpp)
- [Quality checks (C#)](QualityChecksCSharp) - [Quality checks (C#)](QualityChecksCSharp)
- [Code metrics](CodeMetrics) - [Code metrics](CodeMetrics)
- [English comments](EnglishComments)
- [Upgrading](Upgrading) - [Upgrading](Upgrading)
- [Capabilities](Capabilities) - [Capabilities](Capabilities)
@@ -25,6 +26,7 @@
- [Kvalitetskontrol (C og C++)](QualityChecksCpp.da) - [Kvalitetskontrol (C og C++)](QualityChecksCpp.da)
- [Kvalitetskontrol (C#)](QualityChecksCSharp.da) - [Kvalitetskontrol (C#)](QualityChecksCSharp.da)
- [Kodemålinger](CodeMetrics.da) - [Kodemålinger](CodeMetrics.da)
- [Engelske kommentarer](EnglishComments.da)
- [Opgradering](Upgrading.da) - [Opgradering](Upgrading.da)
- [Kapabiliteter](Capabilities.da) - [Kapabiliteter](Capabilities.da)
@@ -38,5 +40,6 @@
- [Semakan kualiti (C dan C++)](QualityChecksCpp.ms) - [Semakan kualiti (C dan C++)](QualityChecksCpp.ms)
- [Semakan kualiti (C#)](QualityChecksCSharp.ms) - [Semakan kualiti (C#)](QualityChecksCSharp.ms)
- [Metrik kod](CodeMetrics.ms) - [Metrik kod](CodeMetrics.ms)
- [Komen dalam bahasa Inggeris](EnglishComments.ms)
- [Menaik taraf](Upgrading.ms) - [Menaik taraf](Upgrading.ms)
- [Keupayaan](Capabilities.ms) - [Keupayaan](Capabilities.ms)
+23
View File
@@ -0,0 +1,23 @@
# When a repository uses a workflow
Each workflow of this repository has one rule here that says when a repository
of the organisation, in its SQA and QC, should use it. The rule is the opt-in
test: a repository that meets the "Use when" condition calls the workflow, and a
review of that repository (SQA) checks that it does. A repository that does not
meet the condition does not call it.
A new workflow is not finished until its row is in the table below
(`AGENTS.md`, rule "Workflow adoption rules").
| Workflow | Use when | Do not use when | What it gives SQA and QC |
| --- | --- | --- | --- |
| Sync GitHub mirror metadata | Scoped: nothing to do. Every repository of the organisation gets it, so a repository has a GitHub push mirror only if it needs one. | The repository has no GitHub mirror. | The mirror shows the same description and topics as the source of truth. |
| Quality checks | The repository tracks Python (`*.py`) or shell (`*.sh`) files. | It has neither. | Evidence for the code checklists that formatting, typing and shell lint are clean before a merge. Mark the check required in the branch protection. |
| Quality checks C++ | The repository tracks C or C++ files. | It has none. | The same evidence for C and C++, at exact tool versions. |
| Quality checks C# | The repository tracks C# files and can restore packages from the network. | It has none, or cannot reach the package source. | The same evidence for C#, on an exact .NET SDK. |
| Code metrics | The repository has a test suite that writes a Cobertura report and its README or wiki shows size and coverage. | It has no tests, or no report is produced. | Coverage and size as badges, so a reviewer sees them without running anything. |
| English comments | The repository's code comments must be English (the project's language rule) and it takes pull requests. | The project's language rule allows other languages in comments. | Enforces the language rule on every pull request, with the author named. Mark it required to block the merge. |
| Publish wiki pages | The repository has a wiki and its pages are written as Markdown under `docs/wiki/` (or `wiki_dir`). | It has no wiki, or the wiki is edited by hand. | The wiki equals the reviewed files; a page change goes through a pull request and its review. |
| Release | Opt-in is not by call: it is for repositories that publish versioned releases pinned by others, and it is run only in this repository. | Not applicable to other repositories. | A tagged, checked release other repositories can pin. |
| Publish accepted milestones | Internal to this repository (it reads this repository's milestones and reviews). A project that keeps `docs/milestones/` and `docs/sqa/reviews/` uses the framework's `templates/ci/` copy. | The repository has no `MIL-*` documents. | Only a milestone that is Accepted with a Go review becomes an issue (the plan-first gate). |
| Validate workflows | Internal: it gates this repository's own pull requests. | Not applicable to other repositories. | Proof that every workflow parses and passes the offline tests. |
+100
View File
@@ -0,0 +1,100 @@
# Keeping source code comments in English
How a software engineer calls the shared workflow that checks that the comments
and docstrings of a pull request are written in English. The workflow fails the
run when one is not, and writes one comment on the pull request that mentions
the author. It changes no file and sends no source text outside the instance.
Its decision is ADR-0012 in this repository.
## Calling the workflow
```yaml
name: English comments
on:
pull_request:
jobs:
english-comments:
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
with:
exclude: "docs/generated/**"
allowed-words-file: .english-comments-allowed-words
```
| Input | Default | Meaning |
| --- | --- | --- |
| `scope` | `changed` | `changed` checks the comments on the lines the pull request adds; `all` checks every tracked file |
| `exclude` | | Comma list of file name or path patterns that are not checked (`*` and `**` are wildcards) |
| `allowed-words-file` | | A file of the repository with words that are never judged, one per line, `#` starts a comment |
| `threshold` | `0.6` | The probability, above 0 and at most 1, from which a comment counts as not English |
| `min-words` | `3` | A comment with fewer words is not judged |
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | The pinned packages the detector needs |
| `tools-ref` | the current major | The tag of this repository whose scripts are run; pin it together with the workflow |
The optional secret `REPORT_TOKEN` is a Gitea token that may comment on pull
requests. Without it the workflow uses the token of the run, so the comment is
written by the Actions bot. A pull request from a fork has a read-only token:
the check still fails the run, but no comment is written.
## What is checked
The comments and docstrings of the files of the languages in the code counter
([code metrics](code-metrics.md)): Python, shell, PowerShell, C, C++, C#, Java,
JavaScript, TypeScript, Go, Rust and SQL. Strings and code are not read.
Consecutive comment lines form a block. Each line of a block is judged on its own
(at least four words and a probability of at least 0.7), so an English line cannot
hide a Danish one next to it; when no line is flagged, the block is judged as a whole
(at least `min-words` words and `threshold`), so a sentence cut over short lines is not missed.
Before a block is judged, everything that is not a word is removed: paths, URLs,
options (`--output`), file names, identifiers (`snake_case`, `camelCase`),
numbers, acronyms and constants (`JSON`), a leading label such as `Usage:`, and
the words of the allowed-words file. A block with fewer than `min-words` words
left is not judged, so a short comment passes. Comments that configure a tool
(`noqa`, `type:`, `pragma`, `shellcheck`) are not read.
## When the check fails
The run fails, each finding is shown as an annotation (`::error file=...,line=...`)
and in the log as `file:line: looks Danish (0.93): "the comment"`, and one comment
on the pull request lists them and mentions the author. A new run updates that
comment; when the pull request passes, the comment is marked resolved and no
longer mentions anyone.
Fix it by rewriting the comment in English. For a quotation or a name that is
not English, either put `english-comments: ignore` in the comment (it skips the
whole comment block it is in), or add the word to the allowed-words file.
## Blocking the merge
A failing run does not block a merge by itself. In the branch protection of the
target branch, mark the check as required: run the workflow once on a pull
request and choose its entry in the list of status checks (the exact name is
made from the calling workflow, its job and the event). Setting branch
protection is the repository owner's action; the workflow does not change it.
## How accurate it is
The detector is `langid`, limited to English, Danish, Norwegian, Swedish,
German, French, Malay, Indonesian and Dutch. On the corpus of the repository it
flags none of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments
and all of the 48 German, Swedish, Norwegian and French ones (ADR-0012). Its
limits:
- a comment of one or two words, or one that is mostly names, is not judged;
- a comment in poor English passes, and a comment that mixes English technical
terms with few Danish or Malay words can pass;
- the Danish and Malay comments of the corpus were written for the test, not
collected from real code, so real code will show other misses. Report a miss
or a false positive with the comment, and it joins the corpus.
## Troubleshooting
| Message | Cause |
| --- | --- |
| `[configuration] give --base` | The workflow was started by an event that is not a pull request with `scope: changed`; call it on `pull_request` or use `scope: all` |
| `[configuration] git diff failed` | The base branch is not in the checkout; the workflow fetches the full history, so check that the pull request has a target branch |
| `[configuration] langid is not installed` | The install step failed; check the pinned versions |
| `[permissions] ... may not comment on pull requests` | The token cannot comment; give `REPORT_TOKEN` write access to issues and pull requests |
| A finding that is not a finding | Add the words to the allowed-words file, or `english-comments: ignore` to the comment |
+132
View File
@@ -0,0 +1,132 @@
# Reaching a tailnet with Tailscale
The composite action `actions/connect-tailscale` joins a Tailscale network from
a Gitea Actions runner, so the next steps of the same job (a deployment over
SSH, a migration) can reach hosts on the tailnet. It does what the GitHub
marketplace action `tailscale/github-action` does, written for Gitea Actions:
that action is built for GitHub and has not been verified on Gitea. It is
**experimental**: it is released in 2.2.0, and it has not yet run on a
runner with a real tailnet (MIL-012). For a plain tunnel without a third-party service, see
[WireGuard](vpn-wireguard.md).
A node belongs to one job: every job that deploys joins on its own and logs out
in a last step. It cannot be a reusable workflow, because a called workflow runs
as a separate job.
## Use it
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Connect Tailscale
uses: TirSystem/github-action/actions/connect-tailscale@v2
with:
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
tags: tag:ci
version: 1.80.2
health-check-host: deploy-host
- name: Deploy
run: ssh deploy@deploy-host ./deploy.sh
- name: Disconnect Tailscale
if: always()
uses: TirSystem/github-action/actions/connect-tailscale@v2
with:
action: down
```
If the short form does not resolve on your instance (Gitea looks for the action
on the host set in `DEFAULT_ACTIONS_URL`), use the full address:
`https://git.tirsystem.com/TirSystem/github-action/actions/connect-tailscale@v2`.
## Inputs
| Input | Default | Meaning |
| --- | --- | --- |
| `action` | `up` | `up` or `down` |
| `authkey` | | A Tailscale auth key, as a secret. Give this or `oauth-secret` |
| `oauth-secret` | | The secret of an OAuth client with the scope `auth_keys`, as a secret. Needs `tags` |
| `tags` | | ACL tags of the node, `tag:ci` or `tag:ci,tag:deploy`. Required with `oauth-secret` |
| `hostname` | runner name | Name of the node in the tailnet |
| `version` | | The exact version to install when the runner has no `tailscale`, for example `1.80.2`. Empty uses the installed one and fails when there is none |
| `sha256` | | The SHA-256 of the downloaded archive `tailscale_<version>_<arch>.tgz`; the install fails when it differs. Recommended |
| `userspace-networking` | `false` | `true` runs without a TUN device |
| `args` | | Extra flags for `tailscale up`, for example `--accept-routes`. Never a secret |
| `timeout` | `60` | Seconds to wait for the node |
| `health-check-host` | | A tailnet host that must answer a Tailscale ping |
## Set it up
1. In the Tailscale admin console define a tag for CI (`tag:ci`) in the policy
file, with the owners that may use it, and an access rule that lets
`tag:ci` reach only the hosts and ports the deployment needs.
2. Create either an **OAuth client** with the scope `auth_keys` (write) for that
tag, or an **auth key** that is reusable only when needed, ephemeral,
pre-authorized and tagged. The OAuth client is preferred: it does not expire
the way a key does, and each run gets a fresh ephemeral key.
3. Store the secret as a secret of the organization, with a name that does not
start with `GITEA_` or `GITHUB_`.
4. Rotate the secret when a person with access leaves.
The state of the node is kept in memory. With an OAuth client the key is
ephemeral, so the node disappears from the tailnet when the job ends, even if
the last step does not run. With an auth key that is not ephemeral, a cancelled
job can leave the node in the tailnet until `down` runs or it is removed in the
admin console: create the key as ephemeral.
## The runner
- The job runs as root, or `sudo` is available (the action uses `sudo` only
when the job is not root). The action downloads the Tailscale binaries from
`pkgs.tailscale.com` when `tailscale` is not on the runner; `version` is then
required, and `sha256` makes the install check the archive. A runner image
that already contains Tailscale skips the download.
- The action starts its own `tailscaled`. It refuses to run when a `tailscaled`
already answers on the runner, because that daemon belongs to the host and
`down` would log it out: use the action on job containers, not on a runner
that runs jobs directly on a host with its own Tailscale.
- **TUN mode** (the default) needs the capability `NET_ADMIN` and the device
`/dev/net/tun` in the runner container: in the `act_runner` configuration set
under `container:` `options: "--cap-add=NET_ADMIN --device=/dev/net/tun"`.
- **Userspace mode** (`userspace-networking: true`) needs neither, but the
tailnet is then reached only through a SOCKS5 proxy on `localhost:1055`:
```bash
ssh -o ProxyCommand="nc -X 5 -x localhost:1055 %h %p" deploy@deploy-host
```
Tools that cannot use a proxy do not work in this mode.
- Use a dedicated runner with a label of its own (for example `vpn`) for jobs
that join a tailnet.
- The daemon is started in the background of the "Run the Tailscale step". That
it stays alive for the following steps of the job has to be confirmed on the
runner in use; the first trial must do so.
## What the action does
- masks the secrets in the log first;
- passes the auth key to `tailscale up` in a file of mode 600 (`--auth-key=file:`),
never on a command line, and deletes the file at once;
- prints errors as `::error::[category] message`, with the categories
`configuration`, `environment` and `connection`;
- waits until `tailscaled` answers before `tailscale up` (up to 30 seconds);
- on `down` logs the node out, stops `tailscaled` and deletes its work folder,
and only when this action started that daemon.
The `file:` form of `--auth-key` needs a recent Tailscale; with an old pinned
version `tailscale up` reports an invalid key.
## Troubleshooting
| Message | Cause |
| --- | --- |
| `[configuration] give authkey or oauth-secret` | Neither was given, or both |
| `[configuration] tags is required` | `oauth-secret` without `tags` |
| `[environment] could not download` | The runner cannot reach `pkgs.tailscale.com`, or the version does not exist |
| `[connection] tailscale up failed` | A wrong or expired key, a tag the client may not use, or a missing TUN device (try `userspace-networking`); the last lines of `tailscaled` follow |
| `[connection] health-check-host ... does not answer` | The access rule does not let `tag:ci` reach the host |
+108
View File
@@ -0,0 +1,108 @@
# Reaching a private network with WireGuard
The composite action `actions/connect-wireguard` brings a WireGuard tunnel up on
a Gitea Actions runner, so the next steps of the same job (a deployment over
SSH, a database migration) can reach hosts that are not on the internet. It is
the WireGuard counterpart of the Tailscale action. It is **experimental**: it
is released in 2.2.0, and it has not yet run on a runner with a real tunnel
(MIL-012).
A tunnel belongs to one job: every job that deploys brings its own tunnel up
and takes it down. It cannot be a reusable workflow, because a called workflow
runs as a separate job.
## Use it
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4.2.2
- name: Connect the VPN
uses: TirSystem/github-action/actions/connect-wireguard@v2
with:
config: ${{ secrets.WG_CONFIG }}
health-check-host: 10.8.0.1
- name: Deploy
run: ssh deploy@10.8.0.10 ./deploy.sh
- name: Disconnect the VPN
if: always()
uses: TirSystem/github-action/actions/connect-wireguard@v2
with:
action: down
```
If the short form does not resolve on your instance (Gitea looks for the action
on the host set in `DEFAULT_ACTIONS_URL`), use the full address:
`https://git.tirsystem.com/TirSystem/github-action/actions/connect-wireguard@v2`.
## Inputs
| Input | Default | Meaning |
| --- | --- | --- |
| `action` | `up` | `up` or `down` |
| `config` | | A complete wg-quick file, as a secret. When set, the fields below are ignored |
| `private-key`, `address`, `peer-public-key`, `endpoint`, `allowed-ips` | | The fields of a configuration, all five needed without `config` |
| `preshared-key`, `dns` | | Optional |
| `persistent-keepalive` | `25` | Seconds |
| `interface` | `wg0` | Interface name |
| `health-check-host` | | A host behind the tunnel; the step fails when it does not answer a ping |
## Set it up
1. On the WireGuard server add one peer for CI, with its own key pair and an
address inside the tunnel (`10.8.0.2/32`). Let it reach only the hosts the
deployment needs.
2. Store the configuration (or the private key) as a secret of the
organization, never in the repository. Give the secret a name that does not
start with `GITEA_` or `GITHUB_`.
3. Set `allowed-ips` to the private network only. `0.0.0.0/0` sends all of the
job's traffic, including the checkout, through the tunnel.
4. Rotate the key when a person with access to the secret leaves.
## The runner must allow it
A tunnel needs a network interface, so the runner needs more than a plain job
container:
- the job runs as root, or `sudo` is available (the action uses `sudo` only
when the job is not root, and installs `wireguard-tools` with `apt` when it is
missing, so a Debian or Ubuntu image);
- the container has the capability `NET_ADMIN` and the device `/dev/net/tun`.
In the `act_runner` configuration set under `container:`
`options: "--cap-add=NET_ADMIN --device=/dev/net/tun"`;
- without the kernel module the action uses `wireguard-go` (user space), which
needs the same capability and device.
Use a dedicated runner with a label of its own (for example `vpn`) and put
`runs-on: vpn` on the jobs that need it, so the other jobs do not get the extra
privilege.
## What the action does
- masks the keys in the log before anything else (the values of `PrivateKey`
and `PresharedKey` of a full configuration, and the key inputs);
- refuses an input that holds a line break, because an extra line in the file
could add a `PostUp` command that runs as root;
- writes `/etc/wireguard/<interface>.conf` with mode 600 and brings it up with
`wg-quick` (through `env`, so the `wireguard-go` choice survives `sudo`); the
file is removed again when the interface does not come up;
- prints errors as `::error::[category] message`, with the categories
`configuration`, `environment` and `connection`;
- on `down` takes the interface down and deletes the file.
It never prints a key. Do not turn on shell tracing (`set -x`) in the steps
that follow.
## Troubleshooting
| Message | Cause |
| --- | --- |
| `[configuration] ... is empty` | Neither `config` nor all five fields were given |
| `[environment] ... NET_ADMIN and /dev/net/tun` | The runner container lacks the capability or the device |
| `[connection] wg-quick could not bring wg0 up` | A wrong key, endpoint or a firewall; run `wg show` on the server |
| `[connection] health-check-host ... does not answer` | The tunnel is up but the host is not in `allowed-ips`, or is blocked |
+120
View File
@@ -0,0 +1,120 @@
"""MIL-013 task 2 (ADR-0012): compare detection methods on the corpus.
Needs langdetect, langid and lingua-language-detector installed (not part of the
test requirements). Usage: python compare_detectors.py [-v]
"""
import csv
import re
import sys
import time
from collections import defaultdict
from pathlib import Path
CORPUS = Path(__file__).resolve().parent
rows = []
for name in ("english.tsv", "foreign.tsv"):
with (CORPUS / name).open(encoding="utf-8", newline="") as handle:
rows += [(r["lang"], r["text"]) for r in csv.DictReader(handle, delimiter="\t", quoting=csv.QUOTE_NONE)]
# ---------- A: rule-based
NON_ENGLISH_LETTERS = re.compile(r"[æøåäöüßéèêàâçñôîûœ]", re.I)
STOP = {
"en": "the a an of to and is in it that this for on with as are be by or if not from at which can will we you "
"when then use used only each all any its into has have was were been do does done so but also than how what".split(),
"da": "og er det en at i jeg til på for med af ikke den der som vi skal kan har de om men et fra hvis her kun "
"efter før bliver være dette denne mappen filen listen brugeren serveren tjek hent gem slet læs opret".split(),
"ms": "dan yang untuk dengan ini itu di dalam pada akan atau tidak dari ke kita jika boleh adalah mesti sama "
"semak fail pelayan pengguna senarai kira baris guna supaya apabila sebelum selepas daripada".split(),
"other": "der die das und ist nicht ein eine mit von zu den dem auf für wir och att som en är inte för med "
"på av vi le la les des est une pour pas dans que qui nous sur om det jeg å ikke vi må".split(),
}
def rule_based(text: str) -> bool:
if NON_ENGLISH_LETTERS.search(text):
return True
words = re.findall(r"[a-zA-ZæøåÆØÅ']+", text.lower())
score = {k: sum(w in set(v) for w in words) for k, v in STOP.items()}
foreign = max(score["da"], score["ms"], score["other"])
return foreign >= 2 and foreign > score["en"]
# ---------- B: langdetect
def make_langdetect():
from langdetect import DetectorFactory, detect_langs
DetectorFactory.seed = 0
def run(text: str, threshold: float = 0.8) -> bool:
try:
best = detect_langs(text)[0]
except Exception:
return False
return best.lang != "en" and best.prob >= threshold
return run
# ---------- C: langid
def make_langid():
import langid
langid.set_languages(["en", "da", "no", "sv", "de", "fr", "ms", "id", "nl"])
def run(text: str, threshold: float = 0.0) -> bool:
lang, _score = langid.classify(text)
return lang != "en"
return run
# ---------- D: lingua
def make_lingua():
from lingua import Language, LanguageDetectorBuilder
names = ["ENGLISH", "DANISH", "BOKMAL", "NYNORSK", "SWEDISH", "GERMAN", "FRENCH", "MALAY", "INDONESIAN", "DUTCH"]
languages = [getattr(Language, n) for n in names if hasattr(Language, n)]
print("lingua languages:", [l.name for l in languages], file=sys.stderr)
detector = LanguageDetectorBuilder.from_languages(*languages).build()
def run(text: str, threshold: float = 0.5) -> bool:
values = detector.compute_language_confidence_values(text)
if not values:
return False
top = values[0]
return top.language != Language.ENGLISH and top.value >= threshold
return run
METHODS = {"rule": rule_based}
for name, factory in (("langdetect", make_langdetect), ("langid", make_langid), ("lingua", make_lingua)):
try:
METHODS[name] = factory()
except Exception as error: # a method that cannot be built is reported, not hidden
print("cannot build", name, error, file=sys.stderr)
def report(name: str, predict) -> None:
started = time.perf_counter()
flagged = defaultdict(lambda: [0, 0]) # lang -> [flagged, total]
wrong = []
for lang, text in rows:
is_flagged = predict(text)
flagged[lang][1] += 1
if is_flagged:
flagged[lang][0] += 1
if (lang == "en") == is_flagged:
wrong.append((lang, text))
elapsed = time.perf_counter() - started
foreign = [k for k in flagged if k != "en"]
line = f"{name:12s} " + " ".join(f"{k}={flagged[k][0]}/{flagged[k][1]}" for k in sorted(foreign))
print(f"{line} FALSE-POSITIVES en={flagged['en'][0]}/{flagged['en'][1]} {elapsed:.2f}s/{len(rows)}")
if "-v" in sys.argv:
for lang, text in wrong[:25]:
print(" ", lang, "|", text[:80])
for name, predict in METHODS.items():
report(name, predict)
+301
View File
@@ -0,0 +1,301 @@
lang text origin
en "string", "char" (a literal of one character) or "none" tools/count_loc.py
en (ADR-0008). The workflow checks out the calling repository, fetches this file tools/quality-checks-cpp.sh
en (MIL-004 task 6). The repository is public, so no token is needed. .gitea/scoped_workflows/sync-github-metadata.yml
en (compile-commands-dir) are optional. .gitea/workflows/quality-checks-cpp.yml
en (tools/publish_wiki.py), fetched at tools-ref. .gitea/workflows/publish-wiki-pages.yml
en ----------------------------------------------------------------- helpers tests/test_quality_workflows_cpp_csharp.py
en --------------------------------------------------------------------- C# tests/test_quality_workflows_cpp_csharp.py
en --------------------------------------------------------------------- C++ tests/test_quality_workflows_cpp_csharp.py
en .c files are C; everything else (including headers) is checked as C++. tools/quality-checks-cpp.sh
en .gitmodules uses ssh:// URLs and the runner has no SSH key, so the .gitea/workflows/sync-milestones.yml
en A directory with a `python3` that runs the interpreter of the tests. tests/test_code_metrics.py
en A failure with a category. tools/publish_wiki.py
en A runner must be registered for the repository (or its organisation). .gitea/workflows/sync-milestones.yml
en A script module, readable and writable by item and by attribute. tests/workflow_source.py
en A script run: the finished process and the tool calls the stubs logged. tests/test_quality_workflows_cpp_csharp.py
en A source file or section needed for a page is missing. tools/build_wiki.py
en A translation has the same code blocks, table rows and heading levels. tests/test_build_wiki.py
en A wiki that already has pages named with spaces, as Gitea stores them. tests/test_publish_wiki.py
en ADR-0009, MIL-009: the scoped publish-wiki.yml and the marker are removed. tests/test_publish_wiki.py
en An exact stable version of the .NET SDK, 10 or later (no channel, no range). tools/quality-checks-csharp.sh
en An input is invalid; the message is printed as a `[configuration]` failure. tools/badges.py
en Answers like the Gitea repository and wiki endpoints. tests/test_publish_wiki.py
en Answers the calls of the release script and records the writes. tests/test_release.py
en Black or white, whichever has the higher contrast on the background. tools/badges.py
en Build the English wiki pages in ``docs/wiki/`` from ``README.md`` and ``guides/``. tools/build_wiki.py
en CLANG_TIDY_VERSION, QC_VENV (and QC_BIN for tests). tools/quality-checks-cpp.sh
en COVERAGE_WARN, COVERAGE_GOOD, BADGES_BRANCH, BADGES_OUT (the directory of the badge files; a temporary tools/code-metrics.sh
en Copy the badge files into the worktree and stage them; return 0 when tools/code-metrics.sh
en Copy the description and topics of a Gitea repository to its GitHub push mirror. tools/sync_github_metadata.py
en Count the code files and the lines of code of a directory (ADR-0010). tools/count_loc.py
en Count the code files under directory; see the module docstring for the result. tools/count_loc.py
en Create a release of this repository: version tag, release page, major tag. tools/release.py
en Create and update the pages; delete the others only when prune is set. tools/publish_wiki.py
en Create the release page (and with it the tag), then move the major tag. tools/release.py
en Describe the shape of a token without revealing any of its characters. tools/publish_wiki.py
en Empty on a push (a real run); "true" or "false" on a manual run. .gitea/workflows/sync-milestones.yml
en Env: SHELLCHECK, SHFMT the commands to run (default: from the PATH) tools/lint-action-scripts.sh
en Every failure is printed as ::error::[category] message. tools/code-metrics.sh
en Every failure is printed as ::error::[category] message. A key is never printed. actions/connect-wireguard/connect-wireguard.sh
en Exit: 0 every script is clean (or there is none); 1 at least one finding or tools/lint-action-scripts.sh
en Gitea keeps a hyphen in a title literal (Home-da.-.md, address Home%2Dda). tests/test_publish_wiki.py
en Gitea shows subpages only through _Sidebar, so it must reach every page. tests/test_build_wiki.py
en H2 titles to keep; None keeps the file tools/build_wiki.py
en Helpers that read workflow files and the Python embedded in them. tests/workflow_source.py
en INSTALLING THIS FILE IS THE STANDING AUTHORIZATION TO PUBLISH. This project .gitea/workflows/sync-milestones.yml
en Import a script file without running its `main` (its name is not `__main__`). tests/workflow_source.py
en Import tools/changelog.py, which sits next to this file. tools/release.py
en Import tools/publish_wiki.py. tests/workflow_source.py
en Import tools/sync_github_metadata.py. tests/workflow_source.py
en It checks the C and C++ files of the repository with clang-format and .gitea/workflows/quality-checks-cpp.yml
en It checks the C# solution or project of the repository with `dotnet format` .gitea/workflows/quality-checks-csharp.yml
en It checks the Python and shell files of the repository with pinned tools, .gitea/workflows/quality-checks.yml
en It counts the code files and the lines of code of the repository (comment .gitea/workflows/code-metrics.yml
en It needs read and write on issues and milestones, and read on the framework .gitea/workflows/sync-milestones.yml
en It runs no test, needs no secret and writes only the badges branch. .gitea/workflows/code-metrics.yml
en LANGUAGES, WORKING_DIRECTORY, RUFF_VERSION, MYPY_VERSION, tools/quality-checks.sh
en LF on every platform tests/test_lint_action_scripts.py
en Leading emoji and symbols in README headings; removed so anchors stay stable. tools/build_wiki.py
en Link text of the sidebar: English, then one list per translation code. tools/build_wiki.py
en Lint the shell scripts of the composite actions (MIL-012 task 6): shellcheck, tools/lint-action-scripts.sh
en MIL-004 Go/No-Go criterion 5: every script is a file under tools/. tests/test_workflows.py
en Markdown pages of wiki_dir to the wiki of the calling repository, creating .gitea/workflows/publish-wiki-pages.yml
en Needs: bash, find, shellcheck, shfmt tools/lint-action-scripts.sh
en No translation follows an older version of its English page. tests/test_build_wiki.py
en Offline tests for the composite action actions/connect-tailscale. tests/test_connect_tailscale.py
en Offline tests for the composite action actions/connect-wireguard. tests/test_connect_wireguard.py
en Offline tests for the parts of tools/ that the flow tests do not reach. tests/test_tool_entrypoints.py
en Offline tests for the reusable C++ and C# quality-check workflows (ADR-0008). tests/test_quality_workflows_cpp_csharp.py
en Offline tests for the reusable code-metrics workflow and tools/code-metrics.sh (ADR-0010). tests/test_code_metrics.py
en Offline tests for the reusable quality-check workflow (ADR-0005). tests/test_quality_workflow.py
en Offline tests for tools/badges.py (ADR-0010): the coverage reader and the SVG renderer. tests/test_badges.py
en Offline tests for tools/build_wiki.py. tests/test_build_wiki.py
en Offline tests for tools/changelog.py and the CHANGELOG.md of this repository. tests/test_changelog.py
en Offline tests for tools/count_loc.py (ADR-0010): hand-counted fixtures per language. tests/test_count_loc.py
en Offline tests for tools/lint-action-scripts.sh (MIL-012 task 6). tests/test_lint_action_scripts.py
en Offline tests for tools/publish_wiki.py and the publish-wiki workflows. tests/test_publish_wiki.py
en Offline tests for tools/release.py and .gitea/workflows/release.yml. tests/test_release.py
en Offline tests for tools/sync_github_metadata.py. tests/test_sync_github_metadata.py
en On a push to main that changes docs/milestones/ or docs/sqa/reviews/, this .gitea/workflows/sync-milestones.yml
en One `## [x]` section of the changelog. tools/changelog.py
en One generated wiki page. tools/build_wiki.py
en Publish accepted milestones to the git host. .gitea/workflows/sync-milestones.yml
en Publish the pages of a folder to the Gitea wiki of the repository. tools/publish_wiki.py
en Publishes the wiki of this repository from docs/wiki/ by calling the .gitea/workflows/publish-own-wiki.yml
en Publishing an empty directory would delete the whole wiki. tools/publish_wiki.py
en PyYAML reads `on` as True tests/test_code_metrics.py
en QC_DOTNET_INSTALL_SCRIPT, QC_DOTNET for tests). tools/quality-checks-csharp.sh
en Read a Cobertura coverage report and write the code metrics badges (ADR-0010). tools/badges.py
en Read, check and cut release notes from CHANGELOG.md (ADR-0006). tools/changelog.py
en Records calls and answers like the Gitea and GitHub REST APIs. tests/test_sync_github_metadata.py
en Releases this repository (ADR-0006, ADR-0007, MIL-004 task 5). It is started .gitea/workflows/release.yml
en Remove the leading emoji from a heading line. tools/build_wiki.py
en Replace urllib.request.urlopen; set `.reply` to a body or an exception. tests/test_tool_entrypoints.py
en Return (code, comment, blank) line counts of the text of a file. tools/count_loc.py
en Return (exact tag, major tag, whether the major tag moves). tools/release.py
en Return ``{file name: content}`` for every English page and the sidebar. tools/build_wiki.py
en Return a flat two-part SVG badge; the width follows the number of characters. tools/badges.py
en Return a message for every page that is missing or out of date. tools/build_wiki.py
en Return a message for every translation that is missing or stale. tools/build_wiki.py
en Return every tag name of the repository. tools/release.py
en Return every workflow file in the known workflow directories. tests/workflow_source.py
en Return one message per breach of the convention. tools/changelog.py
en Return red below warn, yellow from warn to below good, green from good. tools/badges.py
en Return the Markdown of the release page for ``version``. tools/changelog.py
en Return the boolean of the WIKI_PRUNE input: true or false, nothing else. tools/publish_wiki.py
en Return the full Markdown of one wiki page. tools/build_wiki.py
en Return the identity of a page: Gitea stores a space as a hyphen. tools/publish_wiki.py
en Return the index after the string that opens at start; the end of line if unclosed. tools/count_loc.py
en Return the keys of the pages that exist in the wiki. tools/publish_wiki.py
en Return the language of a file name by its extension, or None. tools/count_loc.py
en Return the line coverage of a Cobertura XML report as a whole percent, half up. tools/badges.py
en Return the line that links a page to its other languages. tools/build_wiki.py
en Return the lines before the first H2 and the lines of each H2 section. tools/build_wiki.py
en Return the navigation shown beside every wiki page. tools/build_wiki.py
en Return the problems of one workflow's default `tools-ref`, or []. tests/test_tools_ref.py
en Return the release page text for a version, or fail as a validation error. tools/release.py
en Return the releases in file order; text before the first one is ignored. tools/changelog.py
en Return the short content hash a translation records for its source. tools/build_wiki.py
en Return the source of each `python3 - <<'PY'` heredoc in a workflow. tests/workflow_source.py
en Return the title to send to Gitea for a page key. tools/publish_wiki.py
en Return the version as a tuple, or raise a configuration error. tools/release.py
en Return the wiki form of a link target found in ``source``. tools/build_wiki.py
en Return the workflow files of this repository that a caller can use. tests/test_tools_ref.py
en Return {page key: content} for every Markdown file in wiki_dir. tools/publish_wiki.py
en Reusable workflow (ADR-0005). A repository calls it with .gitea/workflows/quality-checks.yml
en Reusable workflow (ADR-0008). A repository calls it with .gitea/workflows/quality-checks-cpp.yml
en Reusable workflow (ADR-0009, MIL-008). A repository calls it with .gitea/workflows/publish-wiki-pages.yml
en Reusable workflow (ADR-0010). A repository calls it with .gitea/workflows/code-metrics.yml
en Run a tool as `python tools/<name>` with only the given environment. tests/test_tool_entrypoints.py
en Run it once by hand from the Actions page with dry_run left on: it prints .gitea/workflows/sync-milestones.yml
en Run the generator; return the process exit code. tools/build_wiki.py
en Run the script with a stub wg-quick that records its arguments. tests/test_connect_wireguard.py
en SHELLCHECK_PY_VERSION, SHFMT_PY_VERSION, QC_VENV (and QC_BIN for tests). tools/quality-checks.sh
en Scan one line; return (has_code, has_comment) and update the state. tools/count_loc.py
en Scoped workflow: it runs in every repository of an organization. The script .gitea/scoped_workflows/sync-github-metadata.yml
en Secret: TOKEN_FOR_GITEA, the bare Gitea token of the organization (no JSON). .gitea/workflows/sync-milestones.yml
en Settings come from the environment, never from interpolated expressions: tools/code-metrics.sh
en Static validation of every workflow file in this repository. tests/test_workflows.py
en Steps of the composite action actions/connect-tailscale/action.yml. actions/connect-tailscale/connect-tailscale.sh
en Steps of the composite action actions/connect-wireguard/action.yml. actions/connect-wireguard/connect-wireguard.sh
en Steps of the reusable workflow .gitea/workflows/code-metrics.yml (ADR-0010). tools/code-metrics.sh
en Steps of the reusable workflow .gitea/workflows/quality-checks-cpp.yml tools/quality-checks-cpp.sh
en Steps of the reusable workflow .gitea/workflows/quality-checks-csharp.yml tools/quality-checks-csharp.sh
en Steps of the reusable workflow .gitea/workflows/quality-checks.yml (ADR-0005). tools/quality-checks.sh
en Strip heading emoji and rewrite links, leaving fenced code untouched. tools/build_wiki.py
en Succeeds when one of the named files exists in the working directory or above tools/quality-checks-cpp.sh
en TOKEN_FOR_GITEA_WIKI: ${{ secrets.TOKEN_FOR_GITEA_WIKI }} .gitea/workflows/publish-wiki-pages.yml
en TS_ARGS is split on spaces on purpose: it holds flags, never a secret actions/connect-tailscale/connect-tailscale.sh
en TS_AUTHKEY, TS_OAUTH_SECRET, TS_TAGS, TS_HOSTNAME, TS_VERSION, TS_USERSPACE, actions/connect-tailscale/connect-tailscale.sh
en TS_SHA256, TS_ARGS, TS_TIMEOUT, TS_HEALTH_CHECK_HOST. TS_TAILSCALE, actions/connect-tailscale/connect-tailscale.sh
en TS_TAILSCALED, TS_SUDO, TS_STATE_DIR, TS_START_WAIT (tries) and actions/connect-tailscale/connect-tailscale.sh
en TS_WAIT_INTERVAL (seconds) name the commands, the work folder and the waits, actions/connect-tailscale/connect-tailscale.sh
en The Home page names each workflow file of the repository and draws it. tests/test_build_wiki.py
en The `if __name__ == "__main__"` block: exit code 1 and ERROR: on stderr. tests/test_tool_entrypoints.py
en The auth key is passed to tailscale in a file of mode 600, never on a command actions/connect-tailscale/connect-tailscale.sh
en The caller file is the opt-in; there is no marker file. It publishes the .gitea/workflows/publish-wiki-pages.yml
en The comment and string rules of one language. tools/count_loc.py
en The commit is the tip of the branch and its validation run is green. tools/release.py
en The default `tools-ref` of every reusable workflow names the current major. tests/test_tools_ref.py
en The exclude patterns of the caller; a pattern without `/` matches a name anywhere. tools/count_loc.py
en The page a link to a source file should point to. The README is the home page. tools/build_wiki.py
en The pages in wiki/ match what README.md and guides/ produce. tests/test_build_wiki.py
en The reusable workflow sets WIKI_PRUNE: the caller is the opt-in (MIL-008). tests/test_publish_wiki.py
en The wiki of this repository equals docs/wiki/ (ADR-0009, MIL-008 task 4). tests/test_publish_wiki.py
en The workflow checks out the calling repository, fetches this file into tools/code-metrics.sh
en Translate an exclude pattern: `*` stops at `/`, `**` crosses it, `?` is one character. tools/count_loc.py
en Translations (ADR-0001): `<Page>.<code>` written by hand in docs/wiki/. tools/build_wiki.py
en True when the tag holds the file; True when the tag is not in this checkout. tests/test_tools_ref.py
en Two merges in a row queue instead of racing. .gitea/workflows/sync-milestones.yml
en Usage: lint-action-scripts.sh [<directory>] (default: actions) tools/lint-action-scripts.sh
en Use the user-space implementation when the kernel has no WireGuard module, actions/connect-wireguard/connect-wireguard.sh
en WG_ALLOWED_IPS, WG_PRESHARED_KEY, WG_DNS, WG_KEEPALIVE, WG_INTERFACE, actions/connect-wireguard/connect-wireguard.sh
en WG_CONFIG, WG_PRIVATE_KEY, WG_ADDRESS, WG_PEER_PUBLIC_KEY, WG_ENDPOINT, actions/connect-wireguard/connect-wireguard.sh
en WG_HEALTH_CHECK_HOST. WG_DIR is the configuration directory (/etc/wireguard actions/connect-wireguard/connect-wireguard.sh
en WORKING_DIRECTORY, EXCLUDE, COVERAGE_REPORT, COVERAGE_ARTIFACT, tools/code-metrics.sh
en WORKING_DIRECTORY, PROJECT, DOTNET_SDK_VERSION, DOTNET_ROOT (and tools/quality-checks-csharp.sh
en WORKING_DIRECTORY, STD, COMPILE_COMMANDS_DIR, CLANG_FORMAT_VERSION, tools/quality-checks-cpp.sh
en Windows has no POSIX file modes tests/test_connect_wireguard.py
en Without a .clang-tidy, clang-tidy enables no check and refuses to run, so tools/quality-checks-cpp.sh
en Write every page; return the names of the files that changed. tools/build_wiki.py
en Write the badge files and the metrics JSON; return the paths in a fixed order. tools/badges.py
en `install` creates the venv and installs `==` versions; checked with a fake pip. tests/test_quality_workflows_cpp_csharp.py
en `ip link add` fails in the test environment, so wireguard-go must exist tests/test_connect_wireguard.py
en a breaking entry needs a new major compared with the release before it tools/changelog.py
en a called workflow has its own runner tests/test_workflows.py
en a daemon that already runs belongs to the host; this action would log it out actions/connect-tailscale/connect-tailscale.sh
en a full configuration: mask the value of every PrivateKey and PresharedKey actions/connect-wireguard/connect-wireguard.sh
en a line break would add directives (PostUp runs as root) to the file actions/connect-wireguard/connect-wireguard.sh
en a missing tool tools/lint-action-scripts.sh
en a sudo that resets the environment, as the real one does tests/test_connect_wireguard.py
en a wrapped item: the lines that continue it belong to the same item tools/changelog.py
en a.py has one code line, run.sh one tests/test_code_metrics.py
en an entry for the version. dry_run defaults to true: it checks and prints and .gitea/workflows/release.yml
en and `dotnet build` (warnings as errors) on an exact .NET SDK 10 or later, .gitea/workflows/quality-checks-csharp.yml
en and blank lines are not counted), reads an optional Cobertura XML coverage .gitea/workflows/code-metrics.yml
en and nothing when none of them changed tools/code-metrics.sh
en and shfmt in the format .editorconfig sets (two spaces, indented case labels). tools/lint-action-scripts.sh
en and updating pages, and deleting pages that are not in the folder only when .gitea/workflows/publish-wiki-pages.yml
en branches: [ main ] .gitea/workflows/publish-wiki-pages.yml
en branches: [main] .gitea/workflows/code-metrics.yml
en by default); WG_QUICK, WG_SUDO and WG_PING name the commands, for the tests. actions/connect-wireguard/connect-wireguard.sh
en by hand with the version to release; it creates the version tag, the release .gitea/workflows/release.yml
en caller a project copies from framework/templates/ci/publish-wiki.yml, with .gitea/workflows/publish-own-wiki.yml
en changes no file and needs no secret. .gitea/workflows/quality-checks.yml
en changes no file and needs no secret. The package restore needs the network. .gitea/workflows/quality-checks-csharp.yml
en clang-tidy at exact versions, changes no file and needs no secret. The .gitea/workflows/quality-checks-cpp.yml
en clang-tidy exits 0 on a warning, so every finding is made an error: a tools/quality-checks-cpp.sh
en code-metrics.sh measure count the code, read the coverage report and tools/code-metrics.sh
en code-metrics.sh publish commit the badge files to the badges branch, tools/code-metrics.sh
en code-metrics.sh validate check the inputs, before anything is counted tools/code-metrics.sh
en command line. .gitea/workflows/sync-milestones.yml
en commit is the tip of main, its validation run is green and the changelog has .gitea/workflows/release.yml
en compile-commands-dir is relative to the repository root, not to the working directory tools/quality-checks-cpp.sh
en configuration the same way. tools/quality-checks-cpp.sh
en connect-tailscale.sh down log the node out and stop tailscaled actions/connect-tailscale/connect-tailscale.sh
en connect-tailscale.sh mask print ::add-mask:: for every secret input actions/connect-tailscale/connect-tailscale.sh
en connect-tailscale.sh up install tailscale if needed, start tailscaled and join the tailnet actions/connect-tailscale/connect-tailscale.sh
en connect-wireguard.sh down take the tunnel down and delete the configuration actions/connect-wireguard/connect-wireguard.sh
en connect-wireguard.sh mask print ::add-mask:: for every secret input actions/connect-wireguard/connect-wireguard.sh
en connect-wireguard.sh up write the configuration and bring the tunnel up actions/connect-wireguard/connect-wireguard.sh
en contents: write .gitea/workflows/code-metrics.yml
en deleted. Each run updates the issues of every accepted milestone (the sync is .gitea/workflows/sync-milestones.yml
en directory under RUNNER_TEMP by default). tools/code-metrics.sh
en env keeps the userspace variable through sudo; the file holds the key, so actions/connect-wireguard/connect-wireguard.sh
en finding must fail the check, as a failing tool does for the other languages. tools/quality-checks-cpp.sh
en for the tests. actions/connect-tailscale/connect-tailscale.sh
en idempotent, but the host records the edit). .gitea/workflows/sync-milestones.yml
en installed it on 2026-10-11 at the request of S01. .gitea/workflows/sync-milestones.yml
en into shared-scripts/ and runs one command per step: tools/quality-checks-cpp.sh
en is a file of TirSystem/github-action (tools/sync_github_metadata.py), fetched .gitea/scoped_workflows/sync-github-metadata.yml
en is created or updated as a Milestone with its tasks as Issues. A Proposed .gitea/workflows/sync-milestones.yml
en it is removed when the interface does not come up actions/connect-wireguard/connect-wireguard.sh
en it, up to the top of the repository: clang-format and clang-tidy find their tools/quality-checks-cpp.sh
en latest Version History row is Accepted and whose latest review record is a Go .gitea/workflows/sync-milestones.yml
en line, and nothing else, so the rest of the log stays readable actions/connect-wireguard/connect-wireguard.sh
en line. Every failure is printed as ::error::[category] message. actions/connect-tailscale/connect-tailscale.sh
en metrics: .gitea/workflows/code-metrics.yml
en milestone, or one without a Go review, is skipped. Nothing is closed or .gitea/workflows/sync-milestones.yml
en name -> (source, code, comment, blank), counted by hand. tests/test_count_loc.py
en names that start with GITEA_ or GITHUB_. .gitea/workflows/sync-milestones.yml
en no final newline tests/test_count_loc.py
en often has no sudo. TS_SUDO overrides it (empty for none). actions/connect-tailscale/connect-tailscale.sh
en often has no sudo. WG_SUDO overrides it (empty for none). actions/connect-wireguard/connect-wireguard.sh
en on failure the captured output is the ::error:: line, which must be shown actions/connect-wireguard/connect-wireguard.sh
en only a daemon this action started is logged out and stopped actions/connect-tailscale/connect-tailscale.sh
en only the key values are masked tests/test_connect_wireguard.py
en only the probe ran: no second daemon, no up tests/test_connect_tailscale.py
en outside this repository, so it gets a copy of the repository's file tests/test_lint_action_scripts.py
en page from the changelog entry and moves the major tag, and only when the .gitea/workflows/release.yml
en parsed YAML has no fixed shape tests/test_quality_workflows_cpp_csharp.py
en paths: [ "docs/wiki/**" ] .gitea/workflows/publish-wiki-pages.yml
en permissions: .gitea/workflows/code-metrics.yml
en pretend the kernel has WireGuard and the tools are present tests/test_connect_wireguard.py
en prune is true. The script is a file of this repository .gitea/workflows/publish-wiki-pages.yml
en quality-checks-cpp.sh install install the exact clang tool versions into a venv tools/quality-checks-cpp.sh
en quality-checks-cpp.sh run run clang-format and clang-tidy on the tracked files tools/quality-checks-cpp.sh
en quality-checks-cpp.sh validate check the inputs, before anything is installed tools/quality-checks-cpp.sh
en quality-checks-csharp.sh install install the exact .NET SDK version tools/quality-checks-csharp.sh
en quality-checks-csharp.sh run run dotnet format and dotnet build on the project tools/quality-checks-csharp.sh
en quality-checks-csharp.sh validate check the inputs, before anything is installed tools/quality-checks-csharp.sh
en quality-checks.sh install install the pinned tools into a virtual environment tools/quality-checks.sh
en quality-checks.sh run run every selected tool on the tracked files tools/quality-checks.sh
en quality-checks.sh validate check the inputs, before anything is installed tools/quality-checks.sh
en quality: .gitea/workflows/quality-checks-cpp.yml
en real/a.py only, not link.py tests/test_count_loc.py
en render_config: the wg-quick configuration on stdout actions/connect-wireguard/connect-wireguard.sh
en report (an artifact of an earlier job of the caller, or a file of the .gitea/workflows/code-metrics.yml
en repository needs a .clang-format; a .clang-tidy and a compile database .gitea/workflows/quality-checks-cpp.yml
en repository) and commits three SVG badges to a branch of the calling .gitea/workflows/code-metrics.yml
en repository, which is checked out as a submodule. Gitea does not accept secret .gitea/workflows/sync-milestones.yml
en repository. .gitea/workflows/code-metrics.yml
en reusable workflow of this repository (ADR-0009, MIL-008 task 4). It is the .gitea/workflows/publish-own-wiki.yml
en secrets: .gitea/workflows/publish-wiki-pages.yml
en shared-scripts/ and runs one command per step: tools/code-metrics.sh
en shellcheck disable=SC2206 actions/connect-tailscale/connect-tailscale.sh
en shfmt reads .editorconfig from the folder upwards; a temporary folder is tests/test_lint_action_scripts.py
en something differs from the branch. tools/code-metrics.sh
en stop at the top of the repository, or at the file system root tools/quality-checks-cpp.sh
en submodules (framework, and framework/qc inside it) are fetched over HTTPS .gitea/workflows/sync-milestones.yml
en sudo is used only when the job is not root: a job container runs as root and actions/connect-tailscale/connect-tailscale.sh
en the command is required tests/test_tool_entrypoints.py
en the plan and creates nothing. Turn dry_run off only for a real run. .gitea/workflows/sync-milestones.yml
en the project gets the compiler diagnostics and the static analyzer. tools/quality-checks-cpp.sh
en the stub records its arguments and, for `up`, the content of the key file tests/test_connect_tailscale.py
en the tools are in the virtual environment of the interpreter that runs the tests tests/test_lint_action_scripts.py
en tools-ref is main so that the script that is run is the one of this commit. .gitea/workflows/publish-own-wiki.yml
en two differences: the reusable workflow is called by its local path, and .gitea/workflows/publish-own-wiki.yml
en type: ignore[index] tools/changelog.py
en type: ignore[return-value] tools/changelog.py
en uses: TirSystem/github-action/.gitea/workflows/code-metrics.yml@v2 .gitea/workflows/code-metrics.yml
en uses: TirSystem/github-action/.gitea/workflows/publish-wiki-pages.yml@v1 .gitea/workflows/publish-wiki-pages.yml
en uses: TirSystem/github-action/.gitea/workflows/quality-checks-cpp.yml@v1 .gitea/workflows/quality-checks-cpp.yml
en uses: TirSystem/github-action/.gitea/workflows/quality-checks-csharp.yml@v1 .gitea/workflows/quality-checks-csharp.yml
en uses: TirSystem/github-action/.gitea/workflows/quality-checks.yml@v1 .gitea/workflows/quality-checks.yml
en validate-workflows.yml measures tools/ and fails below the .coveragerc limit. tests/test_workflows.py
en validate-workflows.yml runs it on every pull request and push to main. tools/lint-action-scripts.sh
en variables that must reach wg-quick through sudo, which resets the environment actions/connect-wireguard/connect-wireguard.sh
en wait_for_daemon: tailscaled needs a moment to create its socket actions/connect-tailscale/connect-tailscale.sh
en which is the usual case in a Gitea runner container. actions/connect-wireguard/connect-wireguard.sh
en with a checkout so that it is readable, lintable and tested as a file .gitea/scoped_workflows/sync-github-metadata.yml
Can't render this file because it contains an unexpected character in line 2 and column 11.
+139
View File
@@ -0,0 +1,139 @@
lang text origin
da hent brugeren fra databasen written
da Denne funktion beregner totalen for kurven written
da vi skal huske at lukke forbindelsen bagefter written
da TODO: ret fejlen når listen er tom written
da returnerer sandt hvis filen findes written
da Gem resultatet i en midlertidig mappe written
da her tjekker vi om brugeren har adgang written
da læs konfigurationen fra miljøvariablerne written
da hvis der ikke er noget svar, prøver vi igen written
da Opret en ny forbindelse til serveren written
da dette er en midlertidig løsning indtil videre written
da loop gennem alle filerne og tæl linjerne written
da Vi bruger en cache for at spare tid written
da fejlbesked skal vises til brugeren written
da sorter listen efter dato, nyeste først written
da ingen grund til at gentage kaldet written
da Kontroller at tokenet ikke er udløbet written
da slet de gamle filer før vi starter written
da Her håndterer vi alle undtagelser written
da afrund til nærmeste hele tal written
da venter på at serveren bliver klar written
da Parametrene er valgfrie, standardværdien er nul written
da kald funktionen to gange for at være sikker written
da skriv loggen til disken hver time written
da Det her virker kun på Linux written
da vi springer tomme linjer over written
da opdater tælleren når en ny post kommer ind written
da fjern dubletter fra resultatet written
da Brug en kø så arbejdet ikke blokerer written
da den øverste mappe i projektet written
da gennemløb træet fra roden og ned written
da Denne klasse holder styr på sessionerne written
da hvis navnet er tomt, brug standardnavnet written
da tjek om porten er optaget written
da Beregn gennemsnittet af de sidste ti målinger written
da vi skal have en bedre løsning her senere written
da Sæt tiden til midnat written
da læser hele filen ind i hukommelsen written
da Pas på, her kan der opstå en kapløbstilstand written
da flyt filen til arkivmappen written
da Dette er kun til test written
da slå op i tabellen efter navn written
da henter næste side af resultaterne written
da hvorfor gør vi det her? Fordi serveren kræver det written
da gem ændringerne og luk vinduet written
ms dapatkan pengguna daripada pangkalan data written
ms Fungsi ini mengira jumlah bagi troli written
ms kita perlu ingat untuk menutup sambungan selepas itu written
ms TODO: betulkan ralat apabila senarai kosong written
ms mengembalikan benar jika fail wujud written
ms Simpan hasil dalam folder sementara written
ms di sini kita semak sama ada pengguna mempunyai akses written
ms baca konfigurasi daripada pemboleh ubah persekitaran written
ms jika tiada jawapan, kita cuba lagi written
ms Buat sambungan baharu ke pelayan written
ms ini penyelesaian sementara buat masa ini written
ms ulang semua fail dan kira baris written
ms Kita guna cache untuk jimat masa written
ms mesej ralat mesti dipaparkan kepada pengguna written
ms susun senarai mengikut tarikh, yang terbaru dahulu written
ms tidak perlu mengulang panggilan written
ms Semak bahawa token belum tamat tempoh written
ms padam fail lama sebelum kita mulakan written
ms Di sini kita mengendalikan semua pengecualian written
ms bundarkan kepada nombor bulat yang terdekat written
ms menunggu pelayan sedia written
ms Parameter adalah pilihan, nilai lalai ialah sifar written
ms panggil fungsi dua kali untuk pastikan written
ms tulis log ke cakera setiap jam written
ms Ini hanya berfungsi pada Linux written
ms kita langkau baris kosong written
ms kemas kini pembilang apabila rekod baharu masuk written
ms buang pendua daripada hasil written
ms Guna giliran supaya kerja tidak menyekat written
ms folder teratas dalam projek written
ms lalui pokok dari akar ke bawah written
ms Kelas ini menjejaki sesi written
ms jika nama kosong, guna nama lalai written
ms semak sama ada port sedang digunakan written
ms Kira purata sepuluh ukuran terakhir written
ms kita perlukan penyelesaian yang lebih baik nanti written
ms Tetapkan masa kepada tengah malam written
ms membaca seluruh fail ke dalam memori written
ms Awas, di sini boleh berlaku keadaan perlumbaan written
ms alihkan fail ke folder arkib written
ms Ini hanya untuk ujian written
ms cari dalam jadual mengikut nama written
ms mendapatkan halaman seterusnya bagi hasil written
ms kenapa kita buat begini? Kerana pelayan memerlukannya written
ms simpan perubahan dan tutup tetingkap written
de Hole den Benutzer aus der Datenbank written
de Diese Funktion berechnet die Summe des Warenkorbs written
de wir müssen die Verbindung danach schließen written
de Gibt wahr zurück, wenn die Datei existiert written
de Speichere das Ergebnis in einem temporären Ordner written
de hier prüfen wir, ob der Benutzer Zugriff hat written
de Lies die Konfiguration aus den Umgebungsvariablen written
de wenn keine Antwort kommt, versuchen wir es erneut written
de Das ist nur eine vorläufige Lösung written
de Fehlermeldung muss dem Benutzer angezeigt werden written
de lösche die alten Dateien, bevor wir anfangen written
de Achtung, hier kann ein Wettlauf entstehen written
sv hämta användaren från databasen written
sv Den här funktionen räknar ut summan av varukorgen written
sv vi måste stänga anslutningen efteråt written
sv returnerar sant om filen finns written
sv Spara resultatet i en tillfällig mapp written
sv här kontrollerar vi om användaren har åtkomst written
sv läs konfigurationen från miljövariablerna written
sv om det inte kommer något svar försöker vi igen written
sv Det här är bara en tillfällig lösning written
sv felmeddelandet ska visas för användaren written
sv ta bort de gamla filerna innan vi börjar written
sv Se upp, här kan det uppstå en tävlingssituation written
no hent brukeren fra databasen written
no Denne funksjonen beregner summen av handlekurven written
no vi må huske å lukke forbindelsen etterpå written
no returnerer sant hvis filen finnes written
no Lagre resultatet i en midlertidig mappe written
no her sjekker vi om brukeren har tilgang written
no les konfigurasjonen fra miljøvariablene written
no hvis det ikke kommer noe svar, prøver vi igjen written
no Dette er bare en midlertidig løsning written
no feilmeldingen skal vises til brukeren written
no slett de gamle filene før vi begynner written
no Pass på, her kan det oppstå en kappløpstilstand written
fr récupère l'utilisateur depuis la base de données written
fr Cette fonction calcule le total du panier written
fr il faut penser à fermer la connexion ensuite written
fr renvoie vrai si le fichier existe written
fr Enregistre le résultat dans un dossier temporaire written
fr ici on vérifie si l'utilisateur a accès written
fr lit la configuration depuis les variables d'environnement written
fr s'il n'y a pas de réponse, on réessaie written
fr Ce n'est qu'une solution provisoire written
fr le message d'erreur doit être affiché à l'utilisateur written
fr supprime les anciens fichiers avant de commencer written
fr Attention, une condition de concurrence peut survenir ici written
1 lang text origin
2 da hent brugeren fra databasen written
3 da Denne funktion beregner totalen for kurven written
4 da vi skal huske at lukke forbindelsen bagefter written
5 da TODO: ret fejlen når listen er tom written
6 da returnerer sandt hvis filen findes written
7 da Gem resultatet i en midlertidig mappe written
8 da her tjekker vi om brugeren har adgang written
9 da læs konfigurationen fra miljøvariablerne written
10 da hvis der ikke er noget svar, prøver vi igen written
11 da Opret en ny forbindelse til serveren written
12 da dette er en midlertidig løsning indtil videre written
13 da loop gennem alle filerne og tæl linjerne written
14 da Vi bruger en cache for at spare tid written
15 da fejlbesked skal vises til brugeren written
16 da sorter listen efter dato, nyeste først written
17 da ingen grund til at gentage kaldet written
18 da Kontroller at tokenet ikke er udløbet written
19 da slet de gamle filer før vi starter written
20 da Her håndterer vi alle undtagelser written
21 da afrund til nærmeste hele tal written
22 da venter på at serveren bliver klar written
23 da Parametrene er valgfrie, standardværdien er nul written
24 da kald funktionen to gange for at være sikker written
25 da skriv loggen til disken hver time written
26 da Det her virker kun på Linux written
27 da vi springer tomme linjer over written
28 da opdater tælleren når en ny post kommer ind written
29 da fjern dubletter fra resultatet written
30 da Brug en kø så arbejdet ikke blokerer written
31 da den øverste mappe i projektet written
32 da gennemløb træet fra roden og ned written
33 da Denne klasse holder styr på sessionerne written
34 da hvis navnet er tomt, brug standardnavnet written
35 da tjek om porten er optaget written
36 da Beregn gennemsnittet af de sidste ti målinger written
37 da vi skal have en bedre løsning her senere written
38 da Sæt tiden til midnat written
39 da læser hele filen ind i hukommelsen written
40 da Pas på, her kan der opstå en kapløbstilstand written
41 da flyt filen til arkivmappen written
42 da Dette er kun til test written
43 da slå op i tabellen efter navn written
44 da henter næste side af resultaterne written
45 da hvorfor gør vi det her? Fordi serveren kræver det written
46 da gem ændringerne og luk vinduet written
47 ms dapatkan pengguna daripada pangkalan data written
48 ms Fungsi ini mengira jumlah bagi troli written
49 ms kita perlu ingat untuk menutup sambungan selepas itu written
50 ms TODO: betulkan ralat apabila senarai kosong written
51 ms mengembalikan benar jika fail wujud written
52 ms Simpan hasil dalam folder sementara written
53 ms di sini kita semak sama ada pengguna mempunyai akses written
54 ms baca konfigurasi daripada pemboleh ubah persekitaran written
55 ms jika tiada jawapan, kita cuba lagi written
56 ms Buat sambungan baharu ke pelayan written
57 ms ini penyelesaian sementara buat masa ini written
58 ms ulang semua fail dan kira baris written
59 ms Kita guna cache untuk jimat masa written
60 ms mesej ralat mesti dipaparkan kepada pengguna written
61 ms susun senarai mengikut tarikh, yang terbaru dahulu written
62 ms tidak perlu mengulang panggilan written
63 ms Semak bahawa token belum tamat tempoh written
64 ms padam fail lama sebelum kita mulakan written
65 ms Di sini kita mengendalikan semua pengecualian written
66 ms bundarkan kepada nombor bulat yang terdekat written
67 ms menunggu pelayan sedia written
68 ms Parameter adalah pilihan, nilai lalai ialah sifar written
69 ms panggil fungsi dua kali untuk pastikan written
70 ms tulis log ke cakera setiap jam written
71 ms Ini hanya berfungsi pada Linux written
72 ms kita langkau baris kosong written
73 ms kemas kini pembilang apabila rekod baharu masuk written
74 ms buang pendua daripada hasil written
75 ms Guna giliran supaya kerja tidak menyekat written
76 ms folder teratas dalam projek written
77 ms lalui pokok dari akar ke bawah written
78 ms Kelas ini menjejaki sesi written
79 ms jika nama kosong, guna nama lalai written
80 ms semak sama ada port sedang digunakan written
81 ms Kira purata sepuluh ukuran terakhir written
82 ms kita perlukan penyelesaian yang lebih baik nanti written
83 ms Tetapkan masa kepada tengah malam written
84 ms membaca seluruh fail ke dalam memori written
85 ms Awas, di sini boleh berlaku keadaan perlumbaan written
86 ms alihkan fail ke folder arkib written
87 ms Ini hanya untuk ujian written
88 ms cari dalam jadual mengikut nama written
89 ms mendapatkan halaman seterusnya bagi hasil written
90 ms kenapa kita buat begini? Kerana pelayan memerlukannya written
91 ms simpan perubahan dan tutup tetingkap written
92 de Hole den Benutzer aus der Datenbank written
93 de Diese Funktion berechnet die Summe des Warenkorbs written
94 de wir müssen die Verbindung danach schließen written
95 de Gibt wahr zurück, wenn die Datei existiert written
96 de Speichere das Ergebnis in einem temporären Ordner written
97 de hier prüfen wir, ob der Benutzer Zugriff hat written
98 de Lies die Konfiguration aus den Umgebungsvariablen written
99 de wenn keine Antwort kommt, versuchen wir es erneut written
100 de Das ist nur eine vorläufige Lösung written
101 de Fehlermeldung muss dem Benutzer angezeigt werden written
102 de lösche die alten Dateien, bevor wir anfangen written
103 de Achtung, hier kann ein Wettlauf entstehen written
104 sv hämta användaren från databasen written
105 sv Den här funktionen räknar ut summan av varukorgen written
106 sv vi måste stänga anslutningen efteråt written
107 sv returnerar sant om filen finns written
108 sv Spara resultatet i en tillfällig mapp written
109 sv här kontrollerar vi om användaren har åtkomst written
110 sv läs konfigurationen från miljövariablerna written
111 sv om det inte kommer något svar försöker vi igen written
112 sv Det här är bara en tillfällig lösning written
113 sv felmeddelandet ska visas för användaren written
114 sv ta bort de gamla filerna innan vi börjar written
115 sv Se upp, här kan det uppstå en tävlingssituation written
116 no hent brukeren fra databasen written
117 no Denne funksjonen beregner summen av handlekurven written
118 no vi må huske å lukke forbindelsen etterpå written
119 no returnerer sant hvis filen finnes written
120 no Lagre resultatet i en midlertidig mappe written
121 no her sjekker vi om brukeren har tilgang written
122 no les konfigurasjonen fra miljøvariablene written
123 no hvis det ikke kommer noe svar, prøver vi igjen written
124 no Dette er bare en midlertidig løsning written
125 no feilmeldingen skal vises til brukeren written
126 no slett de gamle filene før vi begynner written
127 no Pass på, her kan det oppstå en kappløpstilstand written
128 fr récupère l'utilisateur depuis la base de données written
129 fr Cette fonction calcule le total du panier written
130 fr il faut penser à fermer la connexion ensuite written
131 fr renvoie vrai si le fichier existe written
132 fr Enregistre le résultat dans un dossier temporaire written
133 fr ici on vérifie si l'utilisateur a accès written
134 fr lit la configuration depuis les variables d'environnement written
135 fr s'il n'y a pas de réponse, on réessaie written
136 fr Ce n'est qu'une solution provisoire written
137 fr le message d'erreur doit être affiché à l'utilisateur written
138 fr supprime les anciens fichiers avant de commencer written
139 fr Attention, une condition de concurrence peut survenir ici written
+4
View File
@@ -1,3 +1,7 @@
pytest>=8,<10 pytest>=8,<10
PyYAML>=6,<7 PyYAML>=6,<7
coverage>=7,<8 coverage>=7,<8
shellcheck-py==0.10.0.1
shfmt-py==3.11.0.2
langid==1.1.6
numpy==2.1.3
+1
View File
@@ -106,6 +106,7 @@ def _write_sources(root: Path) -> None:
"quality-checks-cpp", "quality-checks-cpp",
"quality-checks-csharp", "quality-checks-csharp",
"code-metrics", "code-metrics",
"english-comments",
"upgrading", "upgrading",
) )
for name in guide_names: for name in guide_names:
+212
View File
@@ -0,0 +1,212 @@
"""Offline tests for the composite action actions/connect-tailscale.
The shell script is run with stub tailscale and tailscaled commands (no sudo,
no network), so the arguments, the key file, the masking and the failure
messages are checked without touching the runner.
"""
import os
import shutil
import stat
import subprocess
from pathlib import Path
import pytest
import yaml
from workflow_source import REPO_ROOT
ACTION_DIR = REPO_ROOT / "actions" / "connect-tailscale"
ACTION = ACTION_DIR / "action.yml"
SCRIPT = ACTION_DIR / "connect-tailscale.sh"
BASH = shutil.which("bash")
pytestmark = pytest.mark.skipif(BASH is None, reason="bash is needed to run the action script")
AUTHKEY = "tskey-auth-SECRETSECRETSECRET"
OAUTH = "tskey-client-SECRETSECRETSECRET"
# the stub records its arguments and, for `up`, the content of the key file
TAILSCALE_STUB = """#!/usr/bin/env bash
echo "$*" >> "$STUB_LOG"
case "$1" in
up)
for a in "$@"; do
case "$a" in --auth-key=file:*) cat "${a#--auth-key=file:}" > "$STUB_KEY" ;; esac
done
exit "${UP_EXIT:-0}" ;;
ping) exit "${PING_EXIT:-0}" ;;
version)
# the daemon answers only after tailscaled was started, and after DAEMON_DELAY calls
[ -f "$STUB_DAEMON" ] || exit 1
n=$(( $(cat "$STUB_DAEMON") + 1 )); echo "$n" > "$STUB_DAEMON"
[ "$n" -gt "${DAEMON_DELAY:-0}" ] ;;
esac
"""
@pytest.fixture(scope="module")
def document() -> dict:
return yaml.safe_load(ACTION.read_text(encoding="utf-8"))
def make_stub(path: Path, body: str) -> None:
path.write_text(body, encoding="utf-8")
path.chmod(path.stat().st_mode | stat.S_IXUSR)
def run(tmp_path: Path, command: str, **env: str) -> subprocess.CompletedProcess:
make_stub(tmp_path / "tailscale", TAILSCALE_STUB)
make_stub(
tmp_path / "tailscaled",
'#!/usr/bin/env bash\necho "$*" >> "$STUB_LOG"\necho 0 > "$STUB_DAEMON"\n',
)
full = {
"PATH": f"{tmp_path}:{os.environ['PATH']}",
"TS_SUDO": "",
"TS_STATE_DIR": str(tmp_path / "state"),
"STUB_LOG": str(tmp_path / "log"),
"STUB_KEY": str(tmp_path / "key"),
"STUB_DAEMON": str(tmp_path / "daemon"),
"TS_WAIT_INTERVAL": "0.05",
"TS_START_WAIT": "100",
**env,
}
return subprocess.run(
[BASH, str(SCRIPT), command], env=full, capture_output=True, text=True, check=False
)
def log(tmp_path: Path) -> str:
return (tmp_path / "log").read_text(encoding="utf-8")
class TestDefinition:
def test_is_a_composite_action(self, document):
assert document["runs"]["using"] == "composite"
def test_inputs_are_the_contract(self, document):
assert set(document["inputs"]) == {
"action", "authkey", "oauth-secret", "tags", "hostname", "version",
"sha256", "userspace-networking", "args", "timeout", "health-check-host",
}
def test_uses_gitea_contexts_only(self):
text = ACTION.read_text(encoding="utf-8")
assert "gitea.action_path" in text
assert "github." not in text.replace("tailscale/github-action", "").replace("GitHub", "")
def test_no_input_is_interpolated_into_a_run_line(self, document):
for step in document["runs"]["steps"]:
assert "inputs." not in step["run"]
class TestUp:
def test_joins_with_an_auth_key_file_not_the_key_on_the_command_line(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_TAGS="tag:ci", TS_HOSTNAME="ci-1")
assert result.returncode == 0, result.stdout + result.stderr
calls = log(tmp_path)
assert AUTHKEY not in calls
assert AUTHKEY not in result.stdout + result.stderr
assert (tmp_path / "key").read_text(encoding="utf-8") == AUTHKEY
assert "--advertise-tags=tag:ci" in calls
assert "--hostname=ci-1" in calls
assert not (tmp_path / "state" / "authkey").exists()
def test_an_oauth_secret_becomes_an_ephemeral_preauthorized_key(self, tmp_path):
result = run(tmp_path, "up", TS_OAUTH_SECRET=OAUTH, TS_TAGS="tag:ci")
assert result.returncode == 0, result.stdout
assert (tmp_path / "key").read_text(encoding="utf-8") == f"{OAUTH}?ephemeral=true&preauthorized=true"
def test_userspace_networking_starts_the_daemon_without_a_tun_device(self, tmp_path):
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_USERSPACE="true")
assert "--tun=userspace-networking --socks5-server=localhost:1055" in log(tmp_path)
def test_extra_args_are_passed(self, tmp_path):
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_ARGS="--accept-routes --shields-up")
assert "--accept-routes --shields-up" in log(tmp_path)
@pytest.mark.parametrize(
("env", "message"),
[
({}, "give authkey or oauth-secret"),
({"TS_AUTHKEY": AUTHKEY, "TS_OAUTH_SECRET": OAUTH}, "not both"),
({"TS_OAUTH_SECRET": OAUTH}, "tags is required"),
({"TS_AUTHKEY": AUTHKEY, "TS_TAGS": "ci"}, "tags must look like"),
({"TS_AUTHKEY": AUTHKEY, "TS_TIMEOUT": "soon"}, "timeout"),
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "1.x"}, "version"),
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "latest"}, "version"),
({"TS_AUTHKEY": AUTHKEY, "TS_SHA256": "abc"}, "sha256"),
({"TS_AUTHKEY": AUTHKEY, "TS_USERSPACE": "yes"}, "userspace-networking"),
({"TS_AUTHKEY": AUTHKEY, "TS_HOSTNAME": "bad name"}, "hostname"),
],
)
def test_bad_inputs_fail_before_anything_starts(self, tmp_path, env, message):
result = run(tmp_path, "up", **env)
assert result.returncode == 1
assert "::error::[configuration]" in result.stdout
assert message in result.stdout
assert not (tmp_path / "log").exists()
def test_a_failing_up_fails_and_deletes_the_key_file(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, UP_EXIT="1")
assert result.returncode == 1
assert "::error::[connection]" in result.stdout
assert not (tmp_path / "state" / "authkey").exists()
assert AUTHKEY not in result.stdout
def test_a_failing_health_check_fails_the_step(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_HEALTH_CHECK_HOST="db", PING_EXIT="1")
assert result.returncode == 1
assert "health-check-host" in result.stdout
class TestDaemon:
def test_waits_until_the_daemon_answers(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="5")
assert result.returncode == 0, result.stdout
assert "up --auth-key" in log(tmp_path)
def test_a_daemon_that_never_answers_fails_before_up(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="1000", TS_START_WAIT="3")
assert result.returncode == 1
assert "::error::[connection] tailscaled did not answer" in result.stdout
assert "up --auth-key" not in log(tmp_path)
def test_a_daemon_of_the_host_is_not_taken_over(self, tmp_path):
(tmp_path / "daemon").write_text("0", encoding="utf-8")
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)
assert result.returncode == 1
assert "::error::[environment] a tailscaled already runs" in result.stdout
calls = log(tmp_path) # only the probe ran: no second daemon, no up
assert "--state=mem:" not in calls
assert "up --auth-key" not in calls
def test_down_leaves_a_daemon_it_did_not_start(self, tmp_path):
(tmp_path / "daemon").write_text("0", encoding="utf-8")
result = run(tmp_path, "down")
assert result.returncode == 0
assert "nothing to do" in result.stdout
assert not (tmp_path / "log").exists()
def test_without_tailscale_a_version_is_required(self, tmp_path):
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_TAILSCALE="no-such-tailscale")
assert result.returncode == 1
assert "::error::[configuration] tailscale is not on the runner; set version" in result.stdout
class TestDownAndMask:
def test_down_logs_out_and_removes_the_state(self, tmp_path):
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)
result = run(tmp_path, "down")
assert result.returncode == 0
assert "logout" in log(tmp_path)
assert not (tmp_path / "state").exists()
def test_mask_prints_every_secret(self, tmp_path):
result = run(tmp_path, "mask", TS_AUTHKEY=AUTHKEY, TS_OAUTH_SECRET=OAUTH)
assert f"::add-mask::{AUTHKEY}" in result.stdout
assert f"::add-mask::{OAUTH}" in result.stdout
def test_unknown_command_fails(self, tmp_path):
assert run(tmp_path, "sideways").returncode == 1
+182
View File
@@ -0,0 +1,182 @@
"""Offline tests for the composite action actions/connect-wireguard.
The shell script is run with stub commands (no sudo, no wg-quick, no network),
so the generated configuration, the masking and the failure messages are
checked without touching the runner.
"""
import os
import shutil
import stat
import subprocess
from pathlib import Path
import pytest
import yaml
from workflow_source import REPO_ROOT
ACTION_DIR = REPO_ROOT / "actions" / "connect-wireguard"
ACTION = ACTION_DIR / "action.yml"
SCRIPT = ACTION_DIR / "connect-wireguard.sh"
BASH = shutil.which("bash")
pytestmark = pytest.mark.skipif(BASH is None, reason="bash is needed to run the action script")
KEY = "PRIVATEKEYPRIVATEKEYPRIVATEKEYPRIVATEKEY00="
@pytest.fixture(scope="module")
def document() -> dict:
return yaml.safe_load(ACTION.read_text(encoding="utf-8"))
def run(tmp_path: Path, command: str, **env: str) -> subprocess.CompletedProcess:
"""Run the script with a stub wg-quick that records its arguments."""
stub = tmp_path / "wg-quick"
stub.write_text('#!/usr/bin/env bash\necho "$*" >> "$STUB_LOG"\n', encoding="utf-8")
stub.chmod(stub.stat().st_mode | stat.S_IXUSR)
ping = tmp_path / "ping"
ping.write_text("#!/usr/bin/env bash\nexit ${PING_EXIT:-0}\n", encoding="utf-8")
ping.chmod(ping.stat().st_mode | stat.S_IXUSR)
(tmp_path / "etc").mkdir(exist_ok=True)
full = {
"PATH": os.environ["PATH"],
"WG_DIR": str(tmp_path / "etc"),
"WG_QUICK": str(stub),
"WG_PING": str(ping),
"WG_SUDO": "",
"STUB_LOG": str(tmp_path / "log"),
# pretend the kernel has WireGuard and the tools are present
"WG_INTERFACE": "wg0",
**env,
}
# `ip link add` fails in the test environment, so wireguard-go must exist
go = tmp_path / "wireguard-go"
go.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
go.chmod(go.stat().st_mode | stat.S_IXUSR)
full["PATH"] = f"{tmp_path}:{full['PATH']}"
return subprocess.run(
[BASH, str(SCRIPT), command], env=full, capture_output=True, text=True, check=False
)
FIELDS = {
"WG_PRIVATE_KEY": KEY,
"WG_ADDRESS": "10.8.0.2/32",
"WG_PEER_PUBLIC_KEY": "PEERPUBLICKEY=",
"WG_ENDPOINT": "vpn.example.org:51820",
"WG_ALLOWED_IPS": "10.8.0.0/24",
}
class TestDefinition:
def test_is_a_composite_action(self, document):
assert document["runs"]["using"] == "composite"
def test_inputs_are_the_contract(self, document):
assert set(document["inputs"]) == {
"action", "config", "private-key", "address", "peer-public-key", "endpoint",
"allowed-ips", "preshared-key", "dns", "persistent-keepalive", "interface",
"health-check-host",
}
def test_uses_gitea_contexts_only(self):
text = ACTION.read_text(encoding="utf-8")
assert "gitea.action_path" in text
assert "github." not in text
def test_no_secret_is_interpolated_into_a_run_line(self, document):
for step in document["runs"]["steps"]:
assert "inputs." not in step["run"]
class TestUp:
def test_writes_a_private_config_and_brings_the_interface_up(self, tmp_path):
result = run(tmp_path, "up", **FIELDS)
assert result.returncode == 0, result.stdout + result.stderr
conf = tmp_path / "etc" / "wg0.conf"
text = conf.read_text(encoding="utf-8")
assert f"PrivateKey = {KEY}" in text
assert "AllowedIPs = 10.8.0.0/24" in text
assert "PersistentKeepalive = 25" in text
if os.name != "nt": # Windows has no POSIX file modes
assert stat.S_IMODE(conf.stat().st_mode) == 0o600
assert (tmp_path / "log").read_text(encoding="utf-8").strip() == "up wg0"
def test_a_full_config_is_used_as_given(self, tmp_path):
result = run(tmp_path, "up", WG_CONFIG="[Interface]\nPrivateKey = abc\n")
assert result.returncode == 0, result.stdout
assert (tmp_path / "etc" / "wg0.conf").read_text(encoding="utf-8").startswith("[Interface]")
def test_the_key_is_never_printed(self, tmp_path):
result = run(tmp_path, "up", **FIELDS)
assert KEY not in result.stdout + result.stderr
def test_a_missing_field_fails_with_its_name(self, tmp_path):
fields = {k: v for k, v in FIELDS.items() if k != "WG_ENDPOINT"}
result = run(tmp_path, "up", **fields)
assert result.returncode == 1
assert "::error::[configuration]" in result.stdout
assert "WG_ENDPOINT" in result.stdout
def test_a_bad_interface_name_fails(self, tmp_path):
result = run(tmp_path, "up", **{**FIELDS, "WG_INTERFACE": "../etc"})
assert result.returncode == 1
assert "interface" in result.stdout
def test_a_failing_health_check_fails_the_step(self, tmp_path):
result = run(tmp_path, "up", **FIELDS, WG_HEALTH_CHECK_HOST="10.8.0.1", PING_EXIT="1")
assert result.returncode == 1
assert "::error::[connection]" in result.stdout
def make_exec(path: Path, body: str) -> Path:
path.write_text(body, encoding="utf-8")
path.chmod(path.stat().st_mode | stat.S_IXUSR)
return path
class TestEnvironment:
def test_the_userspace_variable_reaches_wg_quick_through_sudo(self, tmp_path):
# a sudo that resets the environment, as the real one does
sudo = make_exec(
tmp_path / "sudo-reset",
'#!/usr/bin/env bash\nexec env -i "PATH=$PATH" "STUB_LOG=$STUB_LOG" "$@"\n',
)
quick = make_exec(
tmp_path / "quick-env",
'#!/usr/bin/env bash\necho "$WG_QUICK_USERSPACE_IMPLEMENTATION $*" >> "$STUB_LOG"\n',
)
result = run(tmp_path, "up", **FIELDS, WG_SUDO=str(sudo), WG_QUICK=str(quick))
assert result.returncode == 0, result.stdout + result.stderr
assert "wireguard-go up wg0" in (tmp_path / "log").read_text(encoding="utf-8")
def test_a_field_with_a_line_break_is_refused(self, tmp_path):
result = run(tmp_path, "up", **{**FIELDS, "WG_ALLOWED_IPS": "10.8.0.0/24\nPostUp = id"})
assert result.returncode == 1
assert "WG_ALLOWED_IPS must be one line" in result.stdout
assert not (tmp_path / "etc" / "wg0.conf").exists()
def test_the_key_file_is_removed_when_wg_quick_fails(self, tmp_path):
failing = make_exec(tmp_path / "quick-fail", "#!/usr/bin/env bash\nexit 1\n")
result = run(tmp_path, "up", **FIELDS, WG_QUICK=str(failing))
assert result.returncode == 1
assert not (tmp_path / "etc" / "wg0.conf").exists()
class TestDownAndMask:
def test_down_removes_the_config(self, tmp_path):
run(tmp_path, "up", **FIELDS)
result = run(tmp_path, "down")
assert result.returncode == 0
assert not (tmp_path / "etc" / "wg0.conf").exists()
def test_mask_prints_every_secret(self, tmp_path):
result = run(tmp_path, "mask", WG_PRIVATE_KEY=KEY, WG_CONFIG="[Interface]\nPrivateKey = abc")
assert f"::add-mask::{KEY}" in result.stdout
assert "::add-mask::abc" in result.stdout
assert "[Interface]" not in result.stdout # only the key values are masked
def test_unknown_command_fails(self, tmp_path):
assert run(tmp_path, "sideways").returncode == 1
+288
View File
@@ -0,0 +1,288 @@
"""Offline tests for tools/english_comments.py (MIL-013, ADR-0012).
The detector is measured on the corpus in tests/fixtures/comments/; the rates
of criterion 1 of MIL-013 are tests, so a change that lowers them fails.
"""
import csv
import json
import shutil
import subprocess
from pathlib import Path
import pytest
from workflow_source import REPO_ROOT, load_script
ec = load_script(REPO_ROOT / "tools" / "english_comments.py", "english_comments")
CORPUS = REPO_ROOT / "tests" / "fixtures" / "comments"
GIT = shutil.which("git")
def corpus() -> list[tuple[str, str]]:
rows: list[tuple[str, str]] = []
for name in ("english.tsv", "foreign.tsv"):
with (CORPUS / name).open(encoding="utf-8", newline="") as handle:
reader = csv.DictReader(handle, delimiter="\t", quoting=csv.QUOTE_NONE)
rows += [(row["lang"], row["text"]) for row in reader]
return rows
@pytest.fixture(scope="module")
def results():
found: dict[str, list[int]] = {}
for language, text in corpus():
counts = found.setdefault(language, [0, 0])
counts[1] += 1
counts[0] += ec.judge(text) is not None
return found
class TestCorpusRates:
"""Criterion 1 of MIL-013: recall of at least 90 % for Danish and Malay, and at most
one false positive per 200 English comments."""
def test_the_corpus_has_the_size_the_milestone_asks_for(self, results):
assert results["en"][1] >= 200
assert results["da"][1] >= 40
assert results["ms"][1] >= 40
assert sum(v[1] for k, v in results.items() if k not in ("en", "da", "ms")) >= 40
def test_english_comments_are_not_flagged(self, results):
flagged, total = results["en"]
assert flagged <= total // 200
@pytest.mark.parametrize("language", ["da", "ms"])
def test_danish_and_malay_are_found(self, results, language):
flagged, total = results[language]
assert flagged / total >= 0.90
def test_other_languages_are_found(self, results):
for language in ("de", "sv", "no", "fr"):
flagged, total = results[language]
assert flagged / total >= 0.80
class TestWords:
def test_paths_options_identifiers_and_numbers_are_not_words(self):
text = "Run tools/badges.py --metrics code-metrics.json with snake_case camelCase v2 DIR"
assert ec.words(text) == ["Run", "with"]
def test_json_and_code_characters_are_not_words(self):
assert ec.words('{"files": 3, "lines": 120} x = 1 <a|b>') == []
def test_a_label_and_an_acronym_are_dropped(self):
assert ec.words("Usage: lint the JSON files") == ["lint", "the", "files"]
def test_a_hyphenated_word_is_split_and_allowed_words_are_removed(self):
assert ec.words("read-only access", frozenset({"access"})) == ["read", "only"]
def test_a_hidden_file_is_not_a_word_but_an_ellipsis_is_kept(self):
assert ec.words("needs a .clang-format file...") == ["needs", "file"]
def test_quotes_inside_a_word_are_kept_once(self):
assert ec.words("don't can't won''t") == ["don't", "can't"]
class TestExtract:
def test_python_comments_and_docstrings(self):
source = '"""Module line one.\n\nSecond paragraph."""\n\n# a comment\nx = 1 # trailing\n\ndef f():\n """Doc of f."""\n'
found = dict(ec.extract("a.py", source))
assert found[1] == "Module line one."
assert found[5] == "a comment"
assert found[6] == "trailing"
assert found[9] == "Doc of f."
def test_python_that_does_not_parse_falls_back_to_the_scanner(self):
assert dict(ec.extract("a.py", "def (:\n# still a comment\n")) == {2: "still a comment"}
def test_shell_and_sql_line_comments(self):
assert ec.extract("a.sh", "#!/bin/sh\n# real comment\necho '# not'\n") == [(2, "real comment")]
assert ec.extract("a.sql", "SELECT 1; -- one row\n") == [(1, "one row")]
def test_c_block_and_line_comments_lose_their_decoration(self):
source = "/**\n * First line\n * second line\n */\nint x; // trailing\n/// doc\n"
assert ec.extract("a.c", source) == [(2, "First line"), (3, "second line"), (5, "trailing"), (6, "doc")]
def test_a_comment_inside_a_string_is_not_a_comment(self):
assert ec.extract("a.c", 'char *s = "// not a comment";\n') == []
def test_tool_pragmas_are_not_prose(self):
source = "x = 1 # noqa: E501\n# type: ignore\n# pylint: disable=x\n# a real comment\n"
assert ec.extract("a.py", source) == [(4, "a real comment")]
def test_a_file_of_an_unknown_language_has_no_comments(self):
assert ec.extract("notes.md", "# Heading\n") == []
def test_consecutive_lines_are_one_group(self):
lines = [(1, "a"), (2, "b"), (4, "c"), (5, "d"), (5, "e")]
assert ec.groups(lines) == [[(1, "a"), (2, "b")], [(4, "c"), (5, "d"), (5, "e")]]
class TestCheckText:
DANISH = "# Denne funktion beregner totalen for kurven og gemmer resultatet\n"
def test_a_danish_comment_is_a_finding_with_its_line(self):
findings = ec.check_text("a.py", "x = 1\n" + self.DANISH)
assert [(f.path, f.line, f.language) for f in findings] == [("a.py", 2, "da")]
def test_an_english_comment_is_not(self):
assert ec.check_text("a.py", "# This function adds the totals of the basket together\n") == []
def test_only_the_blocks_on_the_given_lines_are_checked(self):
text = self.DANISH + "x = 1\n" + self.DANISH
assert [f.line for f in ec.check_text("a.py", text, {3})] == [3]
assert ec.check_text("a.py", text, {2}) == []
def test_a_danish_line_next_to_an_english_line_is_still_found(self):
english = "# This function adds the totals of the basket together and returns the sum\n"
findings = ec.check_text("a.py", english + self.DANISH)
assert [(f.line, f.end_line, f.language) for f in findings] == [(2, 2, "da")]
def test_a_sentence_cut_over_short_lines_is_judged_as_a_whole(self):
text = "# Denne funktion\n# beregner totalen\n# for kurven\n# og gemmer resultatet\n"
[finding] = ec.check_text("a.py", text)
assert (finding.line, finding.end_line, finding.language) == (1, 4, "da")
def test_only_a_flagged_line_is_reported_when_the_block_is_mixed(self):
text = self.DANISH + "# and this line is plain English text about the totals\n"
assert [f.line for f in ec.check_text("a.py", text, {2})] == []
def test_the_marker_skips_the_block(self):
text = self.DANISH.rstrip() + " # english-comments: ignore\n"
assert ec.check_text("a.py", text) == []
def test_allowed_words_are_not_judged(self):
allowed = frozenset(self.DANISH.lower().replace("#", "").replace(",", "").split())
assert ec.check_text("a.py", self.DANISH, allowed=allowed) == []
def test_a_short_comment_is_not_judged(self):
assert ec.check_text("a.py", "# ret fejlen\n") == []
def test_json_in_a_docstring_is_not_french(self):
source = '"""Count.\n\n {"files": 3, "lines": 120, "comment": 14, "blank": 22,\n "languages": {"Python": {"files": 2, "lines": 100}}}\n"""\n'
assert ec.check_text("a.py", source) == []
def test_usage_lines_with_options_are_not_judged(self):
text = "# Usage: lint-action-scripts.sh [<directory>] (default: actions)\n"
assert ec.check_text("a.sh", text) == []
def test_describe_names_the_language_and_cuts_a_long_text(self):
finding = ec.Finding("a.py", 1, 1, "da", 0.93, "x " * 100)
message = ec.describe(finding)
assert message.startswith('looks Danish (0.93): "') and message.endswith('..."')
class TestAllowedWords:
def test_words_one_per_line_with_comments(self, tmp_path):
file = tmp_path / "allowed.txt"
file.write_text("Kubernetes\n# a comment\nSkat # a Danish tax name\n\n", encoding="utf-8")
assert ec.read_allowed_words(str(file)) == {"kubernetes", "skat"}
def test_no_file_named_is_no_words(self):
assert ec.read_allowed_words(None) == frozenset()
def test_an_unreadable_file_is_an_error(self, tmp_path):
with pytest.raises(ec.CheckError):
ec.read_allowed_words(str(tmp_path / "missing.txt"))
def run_git(directory: Path, *args: str) -> None:
subprocess.run(["git", *args], cwd=directory, check=True, capture_output=True)
@pytest.fixture
def repo(tmp_path):
"""A repository with a base commit on main and a branch that adds comments."""
if GIT is None:
pytest.skip("git is needed")
run_git(tmp_path, "init", "-q", "-b", "main")
run_git(tmp_path, "config", "user.email", "t@example.org")
run_git(tmp_path, "config", "user.name", "t")
run_git(tmp_path, "config", "commit.gpgsign", "false")
(tmp_path / "old.py").write_bytes(b"# Denne gamle funktion beregner totalen for kurven\nx = 1\n")
run_git(tmp_path, "add", ".")
run_git(tmp_path, "commit", "-q", "-m", "base")
run_git(tmp_path, "checkout", "-q", "-b", "feature")
return tmp_path
def commit_file(repo: Path, name: str, content: bytes) -> None:
(repo / name).write_bytes(content)
run_git(repo, "add", ".")
run_git(repo, "commit", "-q", "-m", name)
class TestRepository:
def test_only_the_comments_a_branch_adds_are_checked(self, repo, capsys):
commit_file(repo, "new.py", b"# Denne funktion beregner totalen for kurven\n")
code = ec.main(["check", "--directory", str(repo), "--base", "main", "--output", str(repo / "f.json")])
out = capsys.readouterr().out
assert code == 1
assert "::error file=new.py,line=1::[english-comments] looks Danish" in out
assert "old.py" not in out # the old Danish comment is not this branch's
saved = json.loads((repo / "f.json").read_text(encoding="utf-8"))
assert [(f["path"], f["line"], f["language"]) for f in saved] == [("new.py", 1, "da")]
def test_an_added_line_in_an_old_file_is_checked_and_the_old_ones_are_not(self, repo):
commit_file(repo, "old.py", b"# Denne gamle funktion beregner totalen for kurven\nx = 1\n# Hold the lock while the file is written\n")
findings, files = ec.check_repository(repo, "main", False, [], frozenset(), 0.6, 3)
assert findings == [] and files == 1
def test_a_clean_branch_exits_zero(self, repo, capsys):
commit_file(repo, "new.py", b"# This function adds the totals of the basket together\n")
assert ec.main(["check", "--directory", str(repo), "--base", "main"]) == 0
assert "All comments are in English: 1 file(s) checked." in capsys.readouterr().out
def test_scope_all_checks_every_tracked_file(self, repo, capsys):
assert ec.main(["check", "--directory", str(repo), "--scope", "all"]) == 1
assert "old.py:1" in capsys.readouterr().out
def test_excluded_binary_unknown_and_large_files_are_skipped(self, repo, monkeypatch):
danish = b"# Denne funktion beregner totalen for kurven og gemmer resultatet\n"
(repo / "vendor").mkdir()
commit_file(repo, "vendor/skip.py", danish)
commit_file(repo, "skipme.py", danish)
commit_file(repo, "bin.py", danish + b"\x00")
commit_file(repo, "notes.md", b"# Denne funktion beregner totalen for kurven\n")
monkeypatch.setattr(ec, "MAX_FILE_BYTES", 10)
commit_file(repo, "big.py", danish)
findings, files = ec.check_repository(repo, "main", False, ["skipme.py"], frozenset(), 0.6, 3)
assert (findings, files) == ([], 0)
def test_a_deleted_file_in_the_selection_is_skipped(self, repo):
commit_file(repo, "gone.py", b"x = 1\n")
(repo / "gone.py").unlink()
findings, files = ec.check_repository(repo, "main", False, [], frozenset(), 0.6, 3)
assert findings == []
def test_changed_lines_reads_every_hunk(self, repo):
commit_file(repo, "h.py", b"a\nb\nc\nd\n")
commit_file(repo, "h.py", b"a\nB\nc\nD\nE\n")
assert ec.changed_lines(repo, "main")["h.py"] == {1, 2, 3, 4, 5}
class TestMain:
def test_a_missing_base_is_a_configuration_error(self, repo, capsys):
assert ec.main(["check", "--directory", str(repo)]) == 2
assert "::error::[configuration] give --base" in capsys.readouterr().out
def test_an_unknown_base_is_a_configuration_error(self, repo, capsys):
assert ec.main(["check", "--directory", str(repo), "--base", "nope"]) == 2
assert "failed: fatal" in capsys.readouterr().out
def test_a_missing_directory_is_a_configuration_error(self, tmp_path, capsys):
assert ec.main(["check", "--directory", str(tmp_path / "x"), "--scope", "all"]) == 2
assert "does not exist" in capsys.readouterr().out
@pytest.mark.parametrize("flags", [["--threshold", "0"], ["--threshold", "1.5"], ["--min-words", "0"]])
def test_bad_numbers_are_a_configuration_error(self, tmp_path, capsys, flags):
assert ec.main(["check", "--directory", str(tmp_path), "--scope", "all", *flags]) == 2
assert "threshold" in capsys.readouterr().out
def test_langid_missing_is_a_configuration_error(self, repo, monkeypatch, capsys):
commit_file(repo, "new.py", b"# Denne funktion beregner totalen for kurven og gemmer resultatet\n")
monkeypatch.setattr(ec, "_identifier", None)
monkeypatch.setitem(__import__("sys").modules, "langid.langid", None)
assert ec.main(["check", "--directory", str(repo), "--base", "main"]) == 2
assert "langid is not installed" in capsys.readouterr().out
+288
View File
@@ -0,0 +1,288 @@
"""Offline tests for tools/english_comments_report.py and .gitea/workflows/english-comments.yml.
The Gitea API is a fake that records the writes, so no host is contacted.
"""
import io
import json
import urllib.error
import pytest
import yaml
from workflow_source import REPO_ROOT, load_script
report = load_script(REPO_ROOT / "tools" / "english_comments_report.py", "english_comments_report")
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "english-comments.yml"
TOKEN = "t" * 40
FINDING = {
"path": "src/a.py",
"line": 4,
"end_line": 4,
"language": "da",
"probability": 0.93,
"text": "Denne funktion beregner totalen",
}
class FakeGitea:
"""Lists the comments of one pull request and records the writes."""
def __init__(self, comments=()):
self.comments = list(comments)
self.writes = []
def __call__(self, url, method="GET", headers=None, body=None):
assert headers["Authorization"] == f"token {TOKEN}"
if method == "GET":
return self.comments
self.writes.append((method, url.split("/repos/o/r")[-1], body["body"]))
return {"id": 99}
@pytest.fixture
def env(tmp_path, monkeypatch):
findings = tmp_path / "findings.json"
findings.write_text(json.dumps([FINDING]), encoding="utf-8")
values = {
"GITEA_API_URL": "https://git.example/api/v1/",
"SOURCE_REPOSITORY": "o/r",
"PR_NUMBER": "7",
"PR_AUTHOR": "anna",
"REPORT_TOKEN": TOKEN,
"FINDINGS_FILE": str(findings),
"RUN_URL": "https://git.example/o/r/actions/runs/5",
}
for key, value in values.items():
monkeypatch.setenv(key, value)
monkeypatch.setattr("builtins.print", lambda *a, **k: None)
return findings
def write_findings(path, findings):
path.write_text(json.dumps(findings), encoding="utf-8")
def test_a_finding_writes_one_comment_that_mentions_the_author(env):
gitea = FakeGitea()
assert report.main(request=gitea) == 0
[(method, path, body)] = gitea.writes
assert (method, path) == ("POST", "/issues/7/comments")
assert report.MARKER in body and body.startswith(report.MARKER)
assert "@anna" in body and "`src/a.py`" in body and "| 4 |" in body
assert "Denne funktion beregner totalen" in body
assert "[The run](https://git.example/o/r/actions/runs/5)" in body
def test_a_second_run_updates_the_comment_instead_of_adding_one(env):
old = {"id": 11, "body": f"{report.MARKER}\nsomething older"}
gitea = FakeGitea([{"id": 3, "body": "hello"}, old])
assert report.main(request=gitea) == 0
[(method, path, _)] = gitea.writes
assert (method, path) == ("PATCH", "/issues/comments/11")
def test_an_unchanged_comment_is_not_written_again(env):
body = report.failing_body("anna", [FINDING], "https://git.example/o/r/actions/runs/5")
gitea = FakeGitea([{"id": 11, "body": body}])
assert report.main(request=gitea) == 0
assert gitea.writes == []
def test_a_passing_run_marks_the_comment_resolved_without_a_mention(env):
write_findings(env, [])
gitea = FakeGitea([{"id": 11, "body": f"{report.MARKER}\n@anna failing"}])
assert report.main(request=gitea) == 0
[(method, path, body)] = gitea.writes
assert (method, path) == ("PATCH", "/issues/comments/11")
assert "passes" in body and "@anna" not in body
def test_a_passing_run_with_no_earlier_comment_writes_nothing(env):
write_findings(env, [])
gitea = FakeGitea()
assert report.main(request=gitea) == 0
assert gitea.writes == []
def test_comments_of_other_authors_are_not_touched(env):
gitea = FakeGitea([{"id": 3, "body": "no marker here"}])
report.main(request=gitea)
assert [w[0] for w in gitea.writes] == ["POST"]
def test_all_pages_of_comments_are_searched(env):
page = [{"id": n, "body": "x"} for n in range(report.PAGE_SIZE)]
calls = []
def paged(url, method="GET", headers=None, body=None):
if method != "GET":
calls.append((method, url))
return None
number = int(url.rsplit("page=", 1)[1])
return page if number == 1 else [{"id": 900, "body": report.MARKER}]
assert report.main(request=paged) == 0
assert calls == [("PATCH", "https://git.example/api/v1/repos/o/r/issues/comments/900")]
def test_no_token_writes_nothing_and_does_not_fail(env, monkeypatch):
monkeypatch.setenv("REPORT_TOKEN", "")
gitea = FakeGitea()
assert report.main(request=gitea) == 0
assert gitea.writes == []
def test_a_missing_findings_file_means_the_check_did_not_run(env, monkeypatch):
monkeypatch.setenv("FINDINGS_FILE", str(env.parent / "absent.json"))
assert report.main(request=FakeGitea()) == 0
def test_an_unreadable_findings_file_is_a_configuration_error(env, capsys):
env.write_text("not json", encoding="utf-8")
assert report.main(request=FakeGitea()) == 1
@pytest.mark.parametrize("name", ["GITEA_API_URL", "PR_NUMBER", "PR_AUTHOR"])
def test_a_missing_setting_is_a_configuration_error(env, monkeypatch, name):
monkeypatch.delenv(name)
assert report.main(request=FakeGitea()) == 1
def test_text_in_a_comment_cannot_mention_someone_or_break_the_table():
body = report.failing_body("anna", [{**FINDING, "text": "ping @bob `x` | y\nz"}])
assert "@bob" not in body.replace("@​bob", "")
assert "\\|" in body and "\n| " in body and "`x`" not in body
def test_long_lists_are_cut(env):
many = [{**FINDING, "line": n} for n in range(report.MAX_ROWS + 5)]
body = report.failing_body("anna", many)
assert "...and 5 more" in body
assert body.count("`src/a.py`") == report.MAX_ROWS
class FakeResponse(io.BytesIO):
def __enter__(self):
return self
def __exit__(self, *args):
return False
def test_request_json_sends_the_body_and_reads_the_answer(monkeypatch):
seen = {}
def fake_urlopen(request, timeout):
seen["method"] = request.get_method()
seen["data"] = request.data
return FakeResponse(b'{"id": 5}')
monkeypatch.setattr(report.urllib.request, "urlopen", fake_urlopen)
assert report.request_json("https://h/x", "POST", {"A": "b"}, {"body": "t"}) == {"id": 5}
assert seen == {"method": "POST", "data": b'{"body": "t"}'}
def test_request_json_returns_none_for_an_empty_answer(monkeypatch):
monkeypatch.setattr(report.urllib.request, "urlopen", lambda request, timeout: FakeResponse(b""))
assert report.request_json("https://h/x") is None
@pytest.mark.parametrize(
("code", "category"), [(401, "credentials"), (403, "permissions"), (500, "api")]
)
def test_http_errors_have_a_category(monkeypatch, code, category):
def fail(request, timeout):
raise urllib.error.HTTPError(request.full_url, code, "reason", {}, None)
monkeypatch.setattr(report.urllib.request, "urlopen", fail)
with pytest.raises(report.ReportError, match=rf"\[{category}\]"):
report.request_json("https://h/x?token=secret")
def test_an_unreachable_host_is_an_api_error(monkeypatch):
def fail(request, timeout):
raise urllib.error.URLError("down")
monkeypatch.setattr(report.urllib.request, "urlopen", fail)
with pytest.raises(report.ReportError, match=r"\[api\]"):
report.request_json("https://h/x")
def test_an_api_error_is_printed_without_the_token_and_fails_the_step(env, monkeypatch, capsys):
monkeypatch.undo() # restore print
for key, value in {
"GITEA_API_URL": "https://git.example/api/v1",
"SOURCE_REPOSITORY": "o/r",
"PR_NUMBER": "7",
"PR_AUTHOR": "anna",
"REPORT_TOKEN": TOKEN,
"FINDINGS_FILE": str(env),
}.items():
monkeypatch.setenv(key, value)
def refuse(url, method="GET", headers=None, body=None):
raise report.ReportError("permissions", "the token may not comment")
assert report.main(request=refuse) == 1
out = capsys.readouterr().out
assert "::error::[permissions]" in out and TOKEN not in out
@pytest.fixture(scope="module")
def document():
return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
class TestWorkflow:
def test_is_a_reusable_workflow_with_the_contract_inputs(self, document):
call = document[True]["workflow_call"] # PyYAML reads `on` as True
assert set(call["inputs"]) == {
"scope", "exclude", "allowed-words-file", "threshold", "min-words",
"langid-version", "numpy-version", "tools-ref",
}
assert call["inputs"]["threshold"]["default"] == "0.6"
assert call["inputs"]["min-words"]["default"] == "3"
assert call["inputs"]["scope"]["default"] == "changed"
assert call["secrets"]["REPORT_TOKEN"]["required"] is False
def test_permissions_are_explicit_and_small(self, document):
permissions = document["jobs"]["english-comments"]["permissions"]
assert permissions == {"contents": "read", "issues": "write", "pull-requests": "write"}
def test_checks_out_the_full_history_for_the_merge_base(self, document):
first = document["jobs"]["english-comments"]["steps"][0]
assert first["with"]["fetch-depth"] == 0
def test_the_check_does_not_end_the_job_before_the_author_is_told(self, document):
steps = {s.get("name"): s for s in document["jobs"]["english-comments"]["steps"]}
assert steps["Check the comments"]["continue-on-error"] is True
tell = steps["Tell the author"]
assert tell["continue-on-error"] is True and "always()" in tell["if"]
assert "steps.check.outcome == 'failure'" in steps["Fail the run when a comment is not in English"]["if"]
def test_the_author_step_asks_for_the_pull_request_not_for_the_event_name(self, document):
# in a called workflow gitea.event_name is workflow_call (seen on the instance, MIL-013 task 8)
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
assert "event_name" not in tell["if"]
assert "gitea.event.pull_request.number != ''" in tell["if"]
def test_the_link_to_the_run_uses_the_run_id(self, document):
# the URL of a run on the instance holds its id, not its number (MIL-013 task 8)
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
assert tell["env"]["RUN_URL"].endswith("/actions/runs/${{ gitea.run_id }}")
def test_inputs_reach_the_scripts_as_environment_variables_only(self, document):
for step in document["jobs"]["english-comments"]["steps"]:
assert "inputs." not in step.get("run", "")
assert "secrets." not in step.get("run", "")
def test_the_token_falls_back_to_the_token_of_the_run(self, document):
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
assert "secrets.REPORT_TOKEN || gitea.token" in tell["env"]["REPORT_TOKEN"]
def test_uses_gitea_contexts_only(self):
text = WORKFLOW.read_text(encoding="utf-8")
assert "gitea.event.pull_request.user.login" in text
assert "github." not in text.replace("TirSystem/github-action", "")
+86
View File
@@ -0,0 +1,86 @@
"""Offline tests for tools/lint-action-scripts.sh (MIL-012 task 6).
The script runs shellcheck and shfmt on a folder; the tests give it a clean
script, a script with a shellcheck problem and a badly formatted one, so a
seeded error is shown to fail the run.
"""
import os
import shutil
import subprocess
import sys
from pathlib import Path
import pytest
import yaml
from workflow_source import REPO_ROOT
SCRIPT = REPO_ROOT / "tools" / "lint-action-scripts.sh"
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "validate-workflows.yml"
BASH = shutil.which("bash")
# the tools are in the virtual environment of the interpreter that runs the tests
SEARCH_PATH = os.pathsep.join([str(Path(sys.executable).parent), os.environ.get("PATH", "")])
SHELLCHECK = shutil.which("shellcheck", path=SEARCH_PATH)
SHFMT = shutil.which("shfmt", path=SEARCH_PATH)
HAVE_TOOLS = BASH and SHELLCHECK and SHFMT
CLEAN = '#!/usr/bin/env bash\nmain() {\n echo "$1"\n}\n\nmain "$@"\n'
SHELLCHECK_PROBLEM = '#!/usr/bin/env bash\nmain() {\n echo $1\n}\n\nmain "$@"\n'
BADLY_FORMATTED = '#!/usr/bin/env bash\nmain() {\n echo "$1"\n}\n\nmain "$@"\n'
def lint(folder: Path) -> subprocess.CompletedProcess:
# shfmt reads .editorconfig from the folder upwards; a temporary folder is
# outside this repository, so it gets a copy of the repository's file
if folder != REPO_ROOT / "actions":
shutil.copy(REPO_ROOT / ".editorconfig", folder / ".editorconfig")
env = {**os.environ, "SHELLCHECK": str(SHELLCHECK), "SHFMT": str(SHFMT)}
return subprocess.run(
[BASH, str(SCRIPT), str(folder)],
capture_output=True,
text=True,
check=False,
cwd=REPO_ROOT,
env=env,
)
@pytest.mark.skipif(not HAVE_TOOLS, reason="bash, shellcheck and shfmt are needed")
class TestLint:
def test_a_clean_script_passes(self, tmp_path):
(tmp_path / "ok.sh").write_bytes(CLEAN.encode()) # LF on every platform
result = lint(tmp_path)
assert result.returncode == 0, result.stdout + result.stderr
assert "clean" in result.stdout
def test_a_shellcheck_problem_fails(self, tmp_path):
(tmp_path / "bad.sh").write_bytes(SHELLCHECK_PROBLEM.encode()) # LF on every platform
result = lint(tmp_path)
assert result.returncode == 1
assert "::error::[lint] shellcheck" in result.stdout
def test_a_badly_formatted_script_fails(self, tmp_path):
(tmp_path / "ugly.sh").write_bytes(BADLY_FORMATTED.encode()) # LF on every platform
result = lint(tmp_path)
assert result.returncode == 1
assert "::error::[lint] shfmt" in result.stdout
def test_no_scripts_is_not_a_failure(self, tmp_path):
result = lint(tmp_path)
assert result.returncode == 0
assert "nothing was checked" in result.stdout
def test_the_scripts_of_the_actions_are_clean(self):
result = lint(REPO_ROOT / "actions")
assert result.returncode == 0, result.stdout + result.stderr
class TestWorkflow:
def test_validate_workflows_runs_the_lint_with_pinned_tools(self):
document = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
commands = [step.get("run", "") for step in document["jobs"]["validate"]["steps"]]
assert any("tools/lint-action-scripts.sh" in command for command in commands)
requirements = (REPO_ROOT / "tests" / "requirements.txt").read_text(encoding="utf-8")
assert "shellcheck-py==" in requirements
assert "shfmt-py==" in requirements
+25 -2
View File
@@ -58,8 +58,31 @@ def current_major():
return MAJOR_TAG_FILE.read_text(encoding="utf-8").strip() return MAJOR_TAG_FILE.read_text(encoding="utf-8").strip()
def released_in_any_tag(path):
"""True when some version tag holds the file; False for a script not released yet."""
tags = subprocess.run(
["git", "-C", str(REPO_ROOT), "tag", "--list", "v*"],
capture_output=True,
text=True,
check=False,
).stdout.split()
return any(
subprocess.run(
["git", "-C", str(REPO_ROOT), "cat-file", "-e", f"{tag}:{path}"],
capture_output=True,
check=False,
).returncode
== 0
for tag in tags
)
def script_in_tag(tag, path): def script_in_tag(tag, path):
"""True when the tag holds the file; True when the tag is not in this checkout.""" """True when the tag holds the file; True when the tag is not in this checkout.
A script that no tag holds yet is new in the unreleased version: the release
moves the major tag to it, so it does not count as missing (MIL-013).
"""
known = subprocess.run( known = subprocess.run(
["git", "-C", str(REPO_ROOT), "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"], ["git", "-C", str(REPO_ROOT), "rev-parse", "--verify", "--quiet", f"refs/tags/{tag}"],
capture_output=True, capture_output=True,
@@ -72,7 +95,7 @@ def script_in_tag(tag, path):
capture_output=True, capture_output=True,
check=False, check=False,
) )
return holds.returncode == 0 return holds.returncode == 0 or not released_in_any_tag(path)
def test_major_tag_file_names_the_highest_major_of_the_changelog(): def test_major_tag_file_names_the_highest_major_of_the_changelog():
+5 -3
View File
@@ -47,6 +47,7 @@ PAGES: tuple[Page, ...] = (
Page("QualityChecksCpp", "guides/quality-checks-cpp.md"), Page("QualityChecksCpp", "guides/quality-checks-cpp.md"),
Page("QualityChecksCSharp", "guides/quality-checks-csharp.md"), Page("QualityChecksCSharp", "guides/quality-checks-csharp.md"),
Page("CodeMetrics", "guides/code-metrics.md"), Page("CodeMetrics", "guides/code-metrics.md"),
Page("EnglishComments", "guides/english-comments.md"),
Page("Upgrading", "guides/upgrading.md"), Page("Upgrading", "guides/upgrading.md"),
Page("Capabilities", "README.md", ("Capabilities",)), Page("Capabilities", "README.md", ("Capabilities",)),
) )
@@ -68,6 +69,7 @@ _PAGE_FOR_SOURCE = {
"guides/quality-checks-cpp.md": "QualityChecksCpp", "guides/quality-checks-cpp.md": "QualityChecksCpp",
"guides/quality-checks-csharp.md": "QualityChecksCSharp", "guides/quality-checks-csharp.md": "QualityChecksCSharp",
"guides/code-metrics.md": "CodeMetrics", "guides/code-metrics.md": "CodeMetrics",
"guides/english-comments.md": "EnglishComments",
"guides/upgrading.md": "Upgrading", "guides/upgrading.md": "Upgrading",
} }
@@ -193,9 +195,9 @@ def render_page(page: Page, repo_root: Path = REPO_ROOT) -> str:
# Link text of the sidebar: English, then one list per translation code. # Link text of the sidebar: English, then one list per translation code.
SIDEBAR_TITLE = "TirSystem GitHub Actions" SIDEBAR_TITLE = "TirSystem GitHub Actions"
SIDEBAR_LABELS: dict[str, tuple[str, ...]] = { SIDEBAR_LABELS: dict[str, tuple[str, ...]] = {
"en": ("Home", "Onboarding", "Credentials", "Troubleshooting", "Quality checks (Python, shell)", "Quality checks (C and C++)", "Quality checks (C#)", "Code metrics", "Upgrading", "Capabilities"), "en": ("Home", "Onboarding", "Credentials", "Troubleshooting", "Quality checks (Python, shell)", "Quality checks (C and C++)", "Quality checks (C#)", "Code metrics", "English comments", "Upgrading", "Capabilities"),
"da": ("Hjem", "Tilslutning", "Credentials", "Fejlfinding", "Kvalitetskontrol (Python, shell)", "Kvalitetskontrol (C og C++)", "Kvalitetskontrol (C#)", "Kodemålinger", "Opgradering", "Kapabiliteter"), "da": ("Hjem", "Tilslutning", "Credentials", "Fejlfinding", "Kvalitetskontrol (Python, shell)", "Kvalitetskontrol (C og C++)", "Kvalitetskontrol (C#)", "Kodemålinger", "Engelske kommentarer", "Opgradering", "Kapabiliteter"),
"ms": ("Utama", "Menyertakan", "Kelayakan", "Diagnosis", "Semakan kualiti (Python, shell)", "Semakan kualiti (C dan C++)", "Semakan kualiti (C#)", "Metrik kod", "Menaik taraf", "Keupayaan"), "ms": ("Utama", "Menyertakan", "Kelayakan", "Diagnosis", "Semakan kualiti (Python, shell)", "Semakan kualiti (C dan C++)", "Semakan kualiti (C#)", "Metrik kod", "Komen dalam bahasa Inggeris", "Menaik taraf", "Keupayaan"),
} }
SIDEBAR_NAME = "_Sidebar.md" SIDEBAR_NAME = "_Sidebar.md"
+16 -2
View File
@@ -161,8 +161,17 @@ def _skip_string(line: str, start: int, quote: str, escapes: bool) -> int:
return len(line) return len(line)
def _scan_line(line: str, language: Language, state: _State) -> tuple[bool, bool]: def _scan_line(
"""Scan one line; return (has_code, has_comment) and update the state.""" line: str,
language: Language,
state: _State,
comments: list[str] | None = None,
) -> tuple[bool, bool]:
"""Scan one line; return (has_code, has_comment) and update the state.
When ``comments`` is given, the text of every comment on the line is
appended to it (the English-comments check, ADR-0012, reads that text).
"""
has_code = False has_code = False
has_comment = False has_comment = False
i = 0 i = 0
@@ -174,6 +183,9 @@ def _scan_line(line: str, language: Language, state: _State) -> tuple[bool, bool
has_comment = True has_comment = True
end = line.find(block[1], i) end = line.find(block[1], i)
nested = line.find(block[0], i) if language.nested_block else -1 nested = line.find(block[0], i) if language.nested_block else -1
if comments is not None:
stop = nested if nested != -1 and (end == -1 or nested < end) else end
comments.append(line[i : size if stop == -1 else stop])
if nested != -1 and (end == -1 or nested < end): if nested != -1 and (end == -1 or nested < end):
state.block_depth += 1 state.block_depth += 1
i = nested + len(block[0]) i = nested + len(block[0])
@@ -212,6 +224,8 @@ def _scan_line(line: str, language: Language, state: _State) -> tuple[bool, bool
if comment is not None and line.startswith(comment, i): if comment is not None and line.startswith(comment, i):
word_start = i == 0 or line[i - 1] in _WORD_START word_start = i == 0 or line[i - 1] in _WORD_START
if word_start or not language.hash_at_word_start: if word_start or not language.hash_at_word_start:
if comments is not None:
comments.append(line[i + len(comment) :])
return has_code, True return has_code, True
if language.triple_quotes and ( if language.triple_quotes and (
line.startswith('"""', i) or line.startswith("'''", i) line.startswith('"""', i) or line.startswith("'''", i)
+406
View File
@@ -0,0 +1,406 @@
"""Find comments in source code that are not written in English (MIL-013, ADR-0012).
Run by .gitea/workflows/english-comments.yml. It reads the comments and docstrings
of the source files of the languages of ADR-0010 and judges each comment line
with ``langid`` (restricted to a few candidate languages) after removing
everything that is not a word: paths, options, identifiers, numbers and the words
of the allowed-words file. When no line of a block of consecutive comment lines
is flagged, the block is judged as a whole, so a sentence cut over short lines is
not missed. A text with fewer than ``--min-words`` words left is not judged.
Usage:
python tools/english_comments.py check [--directory DIR] [--scope changed|all]
[--base REF] [--exclude PATTERNS] [--allowed-words FILE]
[--threshold P] [--min-words N] [--output FILE]
With ``--scope changed`` (the default) only the comments on lines that the
current branch added since ``--base`` (a git ref, for example ``origin/main``)
are checked, so a pull request is not blamed for a comment it did not write;
``--scope all`` checks every tracked file. A comment block that contains ``english-comments: ignore`` is
skipped (a quotation, a name).
Every finding is printed as ``::error file=PATH,line=N::...`` and as ``PATH:N:
looks Danish (0.93): "text"``; ``--output`` writes them as JSON for the
reporting step. Exit codes: 0 no finding, 1 at least one finding, 2 a setting
or a missing tool (printed as ``::error::[configuration] ...``).
"""
import argparse
import ast
import importlib.util
import io
import json
import re
import subprocess
import sys
import tokenize
from dataclasses import asdict, dataclass
from pathlib import Path
DEFAULT_THRESHOLD = 0.6
DEFAULT_MIN_WORDS = 3
# a single line of a longer block is judged more strictly than a whole block (ADR-0012)
LINE_THRESHOLD = 0.7
LINE_MIN_WORDS = 4
MAX_FILE_BYTES = 1_000_000
CANDIDATES = ["en", "da", "no", "sv", "de", "fr", "ms", "id", "nl"]
LANGUAGE_NAMES = {
"da": "Danish",
"no": "Norwegian",
"sv": "Swedish",
"de": "German",
"fr": "French",
"ms": "Malay",
"id": "Indonesian",
"nl": "Dutch",
}
IGNORE_MARKER = re.compile(r"english-comments:\s*ignore", re.IGNORECASE)
# comments that configure a tool are not prose
_PRAGMA = re.compile(
r"^(noqa|type:|pragma|shellcheck|fmt:|pylint|ruff:|mypy:|pyright:|eslint|prettier|"
r"-\*-|vim:|coding[:=]|nosec|isort:|nolint|clang-format|cppcheck|lint:)",
re.IGNORECASE,
)
_WORD = re.compile(r"^[^\W\d_]+(?:'[^\W\d_]+)?$")
_NOT_WORD_CHARS = re.compile(r"[\d_/\{}\[\]\"`=<>|$@#%&+^~*]")
_HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
class CheckError(RuntimeError):
"""A setting or tool problem (exit code 2)."""
def _load_sibling(name: str):
path = Path(__file__).resolve().parent / f"{name}.py"
spec = importlib.util.spec_from_file_location(name, path)
assert spec is not None and spec.loader is not None
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
return module
count_loc = _load_sibling("count_loc")
@dataclass(frozen=True)
class Finding:
"""A comment block that is probably not English."""
path: str
line: int
end_line: int
language: str
probability: float
text: str
def _clean(text: str) -> str:
"""Remove comment decoration (``*``, ``/``, ``#``, ``-``, ``=``) from both ends."""
text = re.sub(r"^[/*!#<>=\-\s]+", "", text.strip())
return re.sub(r"[*/\s]+$", "", text).strip()
def _python_comments(source: str) -> list[tuple[int, str]]:
"""Comments (tokenizer) and docstring lines (ast) of a Python file."""
found: list[tuple[int, str]] = []
for token in tokenize.generate_tokens(io.StringIO(source).readline):
if token.type == tokenize.COMMENT:
found.append((token.start[0], _clean(token.string)))
for node in ast.walk(ast.parse(source)):
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
first = node.body[0] if node.body else None
if (
isinstance(first, ast.Expr)
and isinstance(first.value, ast.Constant)
and isinstance(first.value.value, str)
):
for offset, line in enumerate(first.value.value.splitlines()):
found.append((first.value.lineno + offset, _clean(line)))
return found
def _scanned_comments(text: str, language) -> list[tuple[int, str]]:
"""Comments of any other language, from the scanner of the code counter."""
state = count_loc._State()
found: list[tuple[int, str]] = []
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
for number, line in enumerate(lines, start=1):
pieces: list[str] = []
count_loc._scan_line(line, language, state, pieces)
if number == 1 and pieces and pieces[0].startswith("!"):
pieces = pieces[1:] # a shebang line is not a comment
joined = " ".join(_clean(piece) for piece in pieces).strip()
if joined:
found.append((number, joined))
return found
def extract(path: str, text: str) -> list[tuple[int, str]]:
"""Return (line, text) for every comment line of a source file, sorted by line.
A file of a language the code counter does not know has no comments here.
"""
language = count_loc.language_of(path)
if language is None:
return []
if language.name == "Python":
try:
found = _python_comments(text)
except (SyntaxError, tokenize.TokenError):
found = _scanned_comments(text, language)
else:
found = _scanned_comments(text, language)
return sorted((n, t) for n, t in found if t and not _PRAGMA.match(t))
def groups(lines: list[tuple[int, str]]) -> list[list[tuple[int, str]]]:
"""Group comment lines on consecutive line numbers: one group is one comment block."""
found: list[list[tuple[int, str]]] = []
for number, text in lines:
if found and found[-1][-1][0] in (number, number - 1):
found[-1].append((number, text))
else:
found.append([(number, text)])
return found
def words(text: str, allowed: frozenset[str] = frozenset()) -> list[str]:
"""The words of a comment: no URLs, paths, options, identifiers or allowed words."""
out: list[str] = []
tokens = text.split()
if tokens and tokens[0].endswith(":") and tokens[0][:-1].isalpha():
tokens = tokens[1:] # a label such as "Usage:" or "Note:" is not prose
for token in tokens:
if len(token) > 1 and token[0] == "." and token[1].isalpha():
continue # a dotfile, for example one of a linter
token = token.strip("().,;:!?")
# a token with code characters (quotes, braces, `=`, `/`, `_`, digits) is not a word
if not token or _NOT_WORD_CHARS.search(token) or "::" in token:
continue
if token.startswith("-") or "." in token or (len(token) > 1 and token.isupper()):
continue # an option, a file name, an acronym or a constant
if re.search(r"[a-z][A-Z]", token):
continue
for part in token.split("-"):
if len(part) > 1 and _WORD.match(part) and part.lower() not in allowed:
out.append(part)
return out
_identifier = None
def _identifier_model():
global _identifier
if _identifier is None:
try:
from langid.langid import LanguageIdentifier, model
except ImportError:
raise CheckError("langid is not installed (pip install langid==1.1.6).") from None
_identifier = LanguageIdentifier.from_modelstring(model, norm_probs=True)
_identifier.set_languages(CANDIDATES)
return _identifier
def judge(
text: str,
allowed: frozenset[str] = frozenset(),
threshold: float = DEFAULT_THRESHOLD,
min_words: int = DEFAULT_MIN_WORDS,
) -> tuple[str, float] | None:
"""Return (language code, probability) when the text is not English, else None."""
kept = words(text, allowed)
if len(kept) < min_words:
return None
code, probability = _identifier_model().classify(" ".join(kept))
if code != "en" and probability >= threshold:
return code, float(probability)
return None
def check_text(
path: str,
text: str,
lines: set[int] | None = None,
allowed: frozenset[str] = frozenset(),
threshold: float = DEFAULT_THRESHOLD,
min_words: int = DEFAULT_MIN_WORDS,
) -> list[Finding]:
"""Findings of one file; with ``lines`` only the blocks that touch those line numbers."""
findings: list[Finding] = []
for group in groups(extract(path, text)):
body = " ".join(line for _, line in group)
if IGNORE_MARKER.search(body):
continue
found: list[Finding] = []
if len(group) > 1:
# each line on its own, so an English line cannot hide a Danish one next to it
found = [
Finding(path, number, number, verdict[0], round(verdict[1], 2), line)
for number, line in group
if (
verdict := judge(
line, allowed, max(threshold, LINE_THRESHOLD), max(min_words, LINE_MIN_WORDS)
)
)
]
if not found and (verdict := judge(body, allowed, threshold, min_words)):
# the block as a whole, so a sentence cut over short lines is not missed
found = [Finding(path, group[0][0], group[-1][0], verdict[0], round(verdict[1], 2), body)]
findings += [
f
for f in found
if lines is None or any(f.line <= n <= f.end_line for n in lines)
]
return findings
def read_allowed_words(path: str | None) -> frozenset[str]:
"""Words, one per line, ``#`` starts a comment; the file may be missing only when not named."""
if not path:
return frozenset()
try:
content = Path(path).read_text(encoding="utf-8")
except OSError:
raise CheckError(f"allowed-words file '{path}' cannot be read.") from None
return frozenset(
line.split("#", 1)[0].strip().lower()
for line in content.splitlines()
if line.split("#", 1)[0].strip()
)
def _git(directory: Path, *args: str) -> str:
result = subprocess.run(
["git", "-c", "core.quotepath=off", *args],
cwd=directory,
capture_output=True,
text=True,
encoding="utf-8",
check=False,
)
if result.returncode != 0:
raise CheckError(f"git {' '.join(args[:2])} failed: {result.stderr.strip()[:200]}")
return result.stdout
def changed_lines(directory: Path, base: str) -> dict[str, set[int]]:
"""Added or changed line numbers per file since the merge base with ``base``."""
out = _git(directory, "diff", "-U0", "--no-color", "--diff-filter=ACMR", f"{base}...HEAD")
result: dict[str, set[int]] = {}
current: set[int] | None = None
for line in out.splitlines():
if line.startswith("+++ "):
name = line[4:]
current = result.setdefault(name[2:], set()) if name.startswith("b/") else None
continue
hunk = _HUNK.match(line)
if hunk and current is not None:
start = int(hunk.group(1))
size = int(hunk.group(2)) if hunk.group(2) is not None else 1
current.update(range(start, start + size))
return result
def tracked_files(directory: Path) -> list[str]:
return [name for name in _git(directory, "ls-files").splitlines() if name]
def _excluded(path: str, excludes) -> bool:
parts = path.split("/")
if any(part in count_loc.DEFAULT_EXCLUDED_DIRS for part in parts[:-1]):
return True
return excludes.matches(parts[-1], path)
def check_repository(
directory: Path,
base: str | None,
check_all: bool,
patterns: list[str],
allowed: frozenset[str],
threshold: float,
min_words: int,
) -> tuple[list[Finding], int]:
"""Return (findings, number of files read)."""
excludes = count_loc.Excludes(patterns)
if check_all:
selection: dict[str, set[int] | None] = {name: None for name in tracked_files(directory)}
else:
if not base:
raise CheckError("give --base (a git ref such as origin/main) or --all.")
selection = dict(changed_lines(directory, base))
findings: list[Finding] = []
read = 0
for name in sorted(selection):
if count_loc.language_of(name) is None or _excluded(name, excludes):
continue
target = directory / name
if not target.is_file() or target.stat().st_size > MAX_FILE_BYTES:
continue
data = target.read_bytes()
if not count_loc._is_text(data):
continue
read += 1
text = data.decode("utf-8", errors="replace")
findings += check_text(name, text, selection[name], allowed, threshold, min_words)
return findings, read
def describe(finding: Finding) -> str:
name = LANGUAGE_NAMES.get(finding.language, finding.language)
shown = finding.text if len(finding.text) <= 120 else finding.text[:117] + "..."
return f'looks {name} ({finding.probability}): "{shown}"'
def _split(value: str) -> list[str]:
return [item.strip() for item in re.split(r"[,\n]", value) if item.strip()]
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Check that source code comments are in English.")
parser.add_argument("command", choices=["check"])
parser.add_argument("--directory", default=".")
parser.add_argument("--base", default="")
parser.add_argument("--scope", choices=["changed", "all"], default="changed")
parser.add_argument("--exclude", default="")
parser.add_argument("--allowed-words", default="")
parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD)
parser.add_argument("--min-words", type=int, default=DEFAULT_MIN_WORDS)
parser.add_argument("--output", default="")
args = parser.parse_args(argv)
try:
if not 0.0 < args.threshold <= 1.0 or args.min_words < 1:
raise CheckError("threshold must be above 0 and at most 1, and min-words at least 1.")
directory = Path(args.directory)
if not directory.is_dir():
raise CheckError(f"directory '{args.directory}' does not exist.")
allowed = read_allowed_words(args.allowed_words or None)
findings, files = check_repository(
directory,
args.base or None,
args.scope == "all",
_split(args.exclude),
allowed,
args.threshold,
args.min_words,
)
except CheckError as error:
print(f"::error::[configuration] {error}")
return 2
for finding in findings:
message = describe(finding)
print(f"::error file={finding.path},line={finding.line}::[english-comments] {message}")
print(f"{finding.path}:{finding.line}: {message}")
if args.output:
Path(args.output).write_text(
json.dumps([asdict(f) for f in findings], indent=2, ensure_ascii=False) + "\n",
encoding="utf-8",
)
summary = f"{len(findings)} comment(s) not in English in {files} file(s) checked."
print(summary if findings else f"All comments are in English: {files} file(s) checked.")
return 1 if findings else 0
if __name__ == "__main__":
sys.exit(main())
+172
View File
@@ -0,0 +1,172 @@
"""Tell the author of a pull request which comments are not in English (MIL-013).
Run by .gitea/workflows/english-comments.yml after the check. It reads the
findings file of tools/english_comments.py and keeps ONE comment on the pull
request:
- findings: the comment mentions the author and lists each finding; an
existing comment of this check is updated, never a second one added;
- no findings: an existing comment of this check is marked resolved (and no
longer mentions the author); with none nothing is written.
Settings come from the environment:
GITEA_API_URL Gitea API base, for example https://host/api/v1
SOURCE_REPOSITORY owner/repo of the pull request
PR_NUMBER the number of the pull request
PR_AUTHOR the login of the author, mentioned in the comment
REPORT_TOKEN a token that may comment on pull requests (a CI secret);
without it nothing is written and the step ends with a
warning (the check run itself still fails)
FINDINGS_FILE the JSON file written by english_comments.py --output
RUN_URL optional, linked in the comment
Every failure is printed as ``::error::[category] message`` with the categories
configuration, credentials, permissions and api; the token is never printed.
"""
import json
import os
import sys
import urllib.error
import urllib.request
MARKER = "<!-- english-comments-check -->"
REQUEST_TIMEOUT_SECONDS = 30
PAGE_SIZE = 50
MAX_ROWS = 30
class ReportError(RuntimeError):
"""A failure with a category: configuration, credentials, permissions or api."""
def __init__(self, category: str, message: str) -> None:
super().__init__(f"[{category}] {message}")
def request_json(url, method="GET", headers=None, body=None):
request = urllib.request.Request(
url,
data=json.dumps(body).encode("utf-8") if body is not None else None,
headers=headers or {},
method=method,
)
try:
with urllib.request.urlopen(request, timeout=REQUEST_TIMEOUT_SECONDS) as response:
content = response.read()
return json.loads(content) if content else None
except urllib.error.HTTPError as error:
target = url.split("?")[0]
detail = f"Gitea API request failed with HTTP {error.code} ({error.reason}) for {method} {target}"
if error.code == 401:
raise ReportError("credentials", f"{detail}. The token was rejected.") from None
if error.code == 403:
raise ReportError(
"permissions", f"{detail}. The token may not comment on pull requests."
) from None
raise ReportError("api", detail) from None
except (urllib.error.URLError, TimeoutError) as error:
raise ReportError("api", f"Gitea API is unreachable ({error}).") from None
def _cell(text: str, limit: int = 100) -> str:
"""Text safe for a table cell: no pipes, line breaks, mentions or backticks."""
text = " ".join(text.split()).replace("|", "\\|").replace("`", "'").replace("@", "@​")
return text if len(text) <= limit else text[: limit - 3] + "..."
def failing_body(author: str, findings: list[dict], run_url: str = "") -> str:
rows = [
f"| `{_cell(f['path'], 80)}` | {f['line']} | {_cell(str(f['language']))} {f['probability']} | {_cell(f['text'])} |"
for f in findings[:MAX_ROWS]
]
more = len(findings) - MAX_ROWS
lines = [
MARKER,
f"@{author}, the **English comments** check found {len(findings)} comment(s) that do not look "
"like English. Source code comments are written in English only.",
"",
"| File | Line | Looks like | Comment |",
"| --- | --- | --- | --- |",
*rows,
]
if more > 0:
lines.append(f"\n...and {more} more; see the run.")
lines += [
"",
"Rewrite the comment in English. For a quotation or a name, add `english-comments: ignore` "
"to the comment, or add the word to the allowed-words file.",
]
if run_url:
lines.append(f"\n[The run]({run_url})")
return "\n".join(lines)
def resolved_body() -> str:
return f"{MARKER}\nThe **English comments** check passes: every checked comment is in English."
def find_comment(api, headers, request, repository, number):
"""Return the newest comment of this check on the pull request, or None."""
found = None
page = 1
while True:
url = f"{api}/repos/{repository}/issues/{number}/comments?limit={PAGE_SIZE}&page={page}"
comments = request(url, "GET", headers, None) or []
for comment in comments:
if MARKER in (comment.get("body") or ""):
found = comment
if len(comments) < PAGE_SIZE:
return found
page += 1
def main(request=request_json) -> int:
env = os.environ
token = env.get("REPORT_TOKEN", "")
try:
missing = [
name
for name in ("GITEA_API_URL", "SOURCE_REPOSITORY", "PR_NUMBER", "PR_AUTHOR", "FINDINGS_FILE")
if not env.get(name)
]
if missing:
raise ReportError("configuration", f"{', '.join(missing)} not set.")
if not os.path.exists(env["FINDINGS_FILE"]):
print("::warning::The check wrote no findings file (it did not run); nothing to report.")
return 0
try:
with open(env["FINDINGS_FILE"], encoding="utf-8") as handle:
findings = json.load(handle)
except (OSError, ValueError):
raise ReportError("configuration", f"findings file '{env['FINDINGS_FILE']}' cannot be read.") from None
if not token:
print("::warning::No REPORT_TOKEN: the pull request is not commented. The check itself still decides.")
return 0
api = env["GITEA_API_URL"].rstrip("/")
repository = env["SOURCE_REPOSITORY"]
number = env["PR_NUMBER"]
headers = {"Authorization": f"token {token}", "Content-Type": "application/json"}
existing = find_comment(api, headers, request, repository, number)
if findings:
body = failing_body(env["PR_AUTHOR"], findings, env.get("RUN_URL", ""))
elif existing:
body = resolved_body()
else:
print("No findings and no earlier comment: nothing to write.")
return 0
if existing:
if existing.get("body") != body:
request(f"{api}/repos/{repository}/issues/comments/{existing['id']}", "PATCH", headers, {"body": body})
print("Updated the comment of the check.")
else:
request(f"{api}/repos/{repository}/issues/{number}/comments", "POST", headers, {"body": body})
print("Wrote a comment on the pull request.")
return 0
except ReportError as error:
print(f"::error::{error}")
return 1
if __name__ == "__main__":
sys.exit(main())
+44
View File
@@ -0,0 +1,44 @@
#!/usr/bin/env bash
# Lint the shell scripts of the composite actions (MIL-012 task 6): shellcheck,
# and shfmt in the format .editorconfig sets (two spaces, indented case labels).
# validate-workflows.yml runs it on every pull request and push to main.
#
# Usage: lint-action-scripts.sh [<directory>] (default: actions)
# Env: SHELLCHECK, SHFMT the commands to run (default: from the PATH)
# Exit: 0 every script is clean (or there is none); 1 at least one finding or
# a missing tool
# Needs: bash, find, shellcheck, shfmt
SHELLCHECK="${SHELLCHECK:-shellcheck}"
SHFMT="${SHFMT:-shfmt}"
main() {
set -u
local dir="${1:-actions}" failed=0 tool
for tool in "$SHELLCHECK" "$SHFMT"; do
command -v "$tool" >/dev/null 2>&1 || {
echo "::error::[environment] $tool is not installed."
exit 1
}
done
local files=()
while IFS= read -r file; do
files+=("$file")
done < <(find "$dir" -name '*.sh' -type f | sort)
if [ "${#files[@]}" -eq 0 ]; then
echo "::notice::No shell scripts under '$dir'; nothing was checked."
exit 0
fi
"$SHELLCHECK" "${files[@]}" || {
echo "::error::[lint] shellcheck found problems."
failed=1
}
"$SHFMT" -d "${files[@]}" || {
echo "::error::[lint] shfmt would change a script; run shfmt -w."
failed=1
}
[ "$failed" -eq 0 ] && echo "Checked ${#files[@]} script(s): clean."
exit "$failed"
}
main "$@"