Merge pull request 'Add the English-comments check: corpus, ADR-0012, detector, report and reusable workflow' (#211) from mil-013-english-comments into main
Reviewed-on: #211
This commit was merged in pull request #211.
This commit is contained in:
@@ -0,0 +1,116 @@
|
|||||||
|
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
|
||||||
|
if: ${{ always() && gitea.event_name == 'pull_request' }}
|
||||||
|
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_number }}
|
||||||
|
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
|
||||||
@@ -22,6 +22,7 @@ Each release lists its changes in these groups, in this order:
|
|||||||
|
|
||||||
### Added
|
### 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-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.
|
- `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.
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ 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 |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
@@ -31,6 +31,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 +283,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 +587,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
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
# 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 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
|
||||||
@@ -13,7 +13,7 @@ each document lives in *this* project and the next version to use.
|
|||||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 014 |
|
| MIL | Milestone / Gateway | docs/milestones/*.md | 014 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 085 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 085 |
|
||||||
| 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 | 0012 |
|
| 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 |
|
||||||
|
|||||||
@@ -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)) |
|
||||||
|
|||||||
@@ -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)) |
|
||||||
|
|||||||
@@ -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)) |
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
<!-- translation of EnglishComments.md, source sha256 866ce29fd1ea -->
|
||||||
|
|
||||||
|
> 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Indtil udgivelse 2.3.0 findes workflowet kun på `main`, så kald det med `@main`; `@v2` indeholder det
|
||||||
|
endnu ikke.
|
||||||
|
|
||||||
|
| 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 bedømmes som én blok.
|
||||||
|
|
||||||
|
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 |
|
||||||
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
Until release 2.3.0 the workflow exists only on `main`, so call it at `@main`; `@v2` does not
|
||||||
|
contain it yet.
|
||||||
|
|
||||||
|
| 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 are judged as one block.
|
||||||
|
|
||||||
|
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 |
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<!-- translation of EnglishComments.md, source sha256 866ce29fd1ea -->
|
||||||
|
|
||||||
|
> 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
|
||||||
|
```
|
||||||
|
|
||||||
|
Sehingga keluaran 2.3.0, aliran kerja hanya wujud pada `main`, jadi panggil dengan `@main`; `@v2` belum
|
||||||
|
mengandunginya.
|
||||||
|
|
||||||
|
| 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 dinilai sebagai satu blok.
|
||||||
|
|
||||||
|
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
@@ -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
@@ -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
@@ -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)).
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
```
|
||||||
|
|
||||||
|
Until release 2.3.0 the workflow exists only on `main`, so call it at `@main`; `@v2` does not
|
||||||
|
contain it yet.
|
||||||
|
|
||||||
|
| 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 are judged as one block.
|
||||||
|
|
||||||
|
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 |
|
||||||
+120
@@ -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)
|
||||||
Vendored
+301
@@ -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.
|
Vendored
+139
@@ -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
|
||||||
|
@@ -3,3 +3,5 @@ PyYAML>=6,<7
|
|||||||
coverage>=7,<8
|
coverage>=7,<8
|
||||||
shellcheck-py==0.10.0.1
|
shellcheck-py==0.10.0.1
|
||||||
shfmt-py==3.11.0.2
|
shfmt-py==3.11.0.2
|
||||||
|
langid==1.1.6
|
||||||
|
numpy==2.1.3
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
"""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_block(self):
|
||||||
|
lines = [(1, "a"), (2, "b"), (4, "c"), (5, "d"), (5, "e")]
|
||||||
|
assert ec.blocks(lines) == [(1, 2, "a b"), (4, 5, "c d 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_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
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
"""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_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", "")
|
||||||
+25
-2
@@ -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
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -0,0 +1,387 @@
|
|||||||
|
"""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, joins the lines of one comment
|
||||||
|
block, and judges the block 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. A block 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
|
||||||
|
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 blocks(lines: list[tuple[int, str]]) -> list[tuple[int, int, str]]:
|
||||||
|
"""Join comment lines on consecutive line numbers: (first line, last line, text)."""
|
||||||
|
joined: list[tuple[int, int, str]] = []
|
||||||
|
for number, text in lines:
|
||||||
|
if joined and joined[-1][1] in (number, number - 1):
|
||||||
|
first, _, previous = joined[-1]
|
||||||
|
joined[-1] = (first, number, f"{previous} {text}")
|
||||||
|
else:
|
||||||
|
joined.append((number, number, text))
|
||||||
|
return joined
|
||||||
|
|
||||||
|
|
||||||
|
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 hidden file such as .clang-format
|
||||||
|
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 first, last, body in blocks(extract(path, text)):
|
||||||
|
if IGNORE_MARKER.search(body):
|
||||||
|
continue
|
||||||
|
if lines is not None and not any(first <= n <= last for n in lines):
|
||||||
|
continue
|
||||||
|
verdict = judge(body, allowed, threshold, min_words)
|
||||||
|
if verdict:
|
||||||
|
findings.append(Finding(path, first, last, verdict[0], round(verdict[1], 2), body))
|
||||||
|
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())
|
||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user