Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e33b9977a3 | ||
|
|
01178370cb | ||
|
|
8017bc6b39 | ||
|
|
a772088730 | ||
|
|
35f3c4301f | ||
|
|
b8d5bae3ae | ||
|
|
88f852af93 | ||
|
|
39bcaa5bac | ||
|
|
3d46c4b60a | ||
|
|
198830588c | ||
|
|
98a79c7b62 | ||
|
|
eeaf6f5361 | ||
|
|
57d9b63b3c | ||
|
|
a767ded5e1 | ||
|
|
b8e63d0a29 | ||
|
|
7d8cc4522e | ||
|
|
0603c61602 | ||
|
|
51af1466e0 | ||
|
|
ff292e2938 | ||
|
|
e35b1c5674 | ||
|
|
5dedb0ee9b | ||
|
|
b75ed03d3f | ||
|
|
3325bea772 | ||
|
|
a91e0233c1 | ||
|
|
7a30560a74 | ||
|
|
9e1cfa5fa8 | ||
|
|
f0d9d79f4c | ||
|
|
2689264500 | ||
|
|
8eaba331ec | ||
|
|
fce3cf8c6b | ||
|
|
54e83770a5 | ||
|
|
8e093821f1 | ||
|
|
c5d5941869 | ||
|
|
241c26432b | ||
|
|
251e18dfc3 | ||
|
|
4aa3fd38f9 |
@@ -5,3 +5,8 @@ root = true
|
|||||||
end_of_line = lf
|
end_of_line = lf
|
||||||
charset = utf-8
|
charset = utf-8
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.sh]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
switch_case_indent = true
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
name: English comments
|
||||||
|
|
||||||
|
# Reusable workflow (MIL-013, ADR-0012). A repository calls it on its pull
|
||||||
|
# requests with
|
||||||
|
#
|
||||||
|
# on:
|
||||||
|
# pull_request:
|
||||||
|
# jobs:
|
||||||
|
# english-comments:
|
||||||
|
# uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
|
||||||
|
#
|
||||||
|
# It reads the comments and docstrings that the pull request adds, judges their
|
||||||
|
# language, fails the run when one is not English and writes one comment on the
|
||||||
|
# pull request that mentions the author. It changes no file. Mark the check as
|
||||||
|
# required in the branch protection of the target branch to block the merge
|
||||||
|
# (the workflow cannot do that).
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
scope:
|
||||||
|
description: "changed: the comments on the lines the pull request adds; all: every tracked file"
|
||||||
|
type: string
|
||||||
|
default: "changed"
|
||||||
|
exclude:
|
||||||
|
description: "Comma list of file name or path patterns that are not checked (* and ** are wildcards)"
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
allowed-words-file:
|
||||||
|
description: "File of the repository with words that are never judged, one per line"
|
||||||
|
type: string
|
||||||
|
default: ""
|
||||||
|
threshold:
|
||||||
|
description: "Probability, above 0 and at most 1, from which a comment counts as not English (ADR-0012)"
|
||||||
|
type: string
|
||||||
|
default: "0.6"
|
||||||
|
min-words:
|
||||||
|
description: "A comment with fewer words is not judged (ADR-0012)"
|
||||||
|
type: string
|
||||||
|
default: "3"
|
||||||
|
langid-version:
|
||||||
|
description: "Version of the langid package"
|
||||||
|
type: string
|
||||||
|
default: "1.1.6"
|
||||||
|
numpy-version:
|
||||||
|
description: "Version of numpy, which langid needs"
|
||||||
|
type: string
|
||||||
|
default: "2.1.3"
|
||||||
|
tools-ref:
|
||||||
|
description: "Tag or branch of TirSystem/github-action whose tools/english_comments.py is run; pin it to the exact tag together with the workflow"
|
||||||
|
type: string
|
||||||
|
default: "v2"
|
||||||
|
secrets:
|
||||||
|
REPORT_TOKEN:
|
||||||
|
description: "Optional Gitea token that may comment on pull requests; without it the token of the run is used"
|
||||||
|
required: false
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
english-comments:
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 15
|
||||||
|
env:
|
||||||
|
SCOPE: ${{ inputs.scope }}
|
||||||
|
EXCLUDE: ${{ inputs.exclude }}
|
||||||
|
ALLOWED_WORDS: ${{ inputs.allowed-words-file }}
|
||||||
|
THRESHOLD: ${{ inputs.threshold }}
|
||||||
|
MIN_WORDS: ${{ inputs.min-words }}
|
||||||
|
LANGID_VERSION: ${{ inputs.langid-version }}
|
||||||
|
NUMPY_VERSION: ${{ inputs.numpy-version }}
|
||||||
|
BASE_REF: ${{ gitea.base_ref }}
|
||||||
|
FINDINGS_FILE: /tmp/english-comments-findings.json
|
||||||
|
steps:
|
||||||
|
- name: Check out the repository
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Fetch the shared scripts
|
||||||
|
uses: actions/checkout@v4.2.2
|
||||||
|
with:
|
||||||
|
repository: TirSystem/github-action
|
||||||
|
ref: ${{ inputs.tools-ref }}
|
||||||
|
path: shared-scripts
|
||||||
|
|
||||||
|
- name: Install the language detector
|
||||||
|
run: |
|
||||||
|
python3 -m venv /tmp/english-comments-venv
|
||||||
|
/tmp/english-comments-venv/bin/pip install --quiet "langid==${LANGID_VERSION}" "numpy==${NUMPY_VERSION}"
|
||||||
|
|
||||||
|
- name: Check the comments
|
||||||
|
id: check
|
||||||
|
continue-on-error: true
|
||||||
|
run: >-
|
||||||
|
/tmp/english-comments-venv/bin/python shared-scripts/tools/english_comments.py check
|
||||||
|
--scope "$SCOPE" --base "origin/$BASE_REF" --exclude "shared-scripts/**,$EXCLUDE"
|
||||||
|
--allowed-words "$ALLOWED_WORDS" --threshold "$THRESHOLD" --min-words "$MIN_WORDS"
|
||||||
|
--output "$FINDINGS_FILE"
|
||||||
|
|
||||||
|
- name: Tell the author
|
||||||
|
# in a called workflow gitea.event_name is workflow_call, so ask for the pull request itself
|
||||||
|
if: ${{ always() && gitea.event.pull_request.number != '' }}
|
||||||
|
continue-on-error: true
|
||||||
|
env:
|
||||||
|
GITEA_API_URL: ${{ gitea.api_url }}
|
||||||
|
SOURCE_REPOSITORY: ${{ gitea.repository }}
|
||||||
|
PR_NUMBER: ${{ gitea.event.pull_request.number }}
|
||||||
|
PR_AUTHOR: ${{ gitea.event.pull_request.user.login }}
|
||||||
|
RUN_URL: ${{ gitea.server_url }}/${{ gitea.repository }}/actions/runs/${{ gitea.run_id }}
|
||||||
|
REPORT_TOKEN: ${{ secrets.REPORT_TOKEN || gitea.token }}
|
||||||
|
run: python3 shared-scripts/tools/english_comments_report.py
|
||||||
|
|
||||||
|
- name: Fail the run when a comment is not in English
|
||||||
|
if: ${{ steps.check.outcome == 'failure' }}
|
||||||
|
run: exit 1
|
||||||
@@ -26,3 +26,9 @@ jobs:
|
|||||||
|
|
||||||
- name: Check the coverage of the tools
|
- name: Check the coverage of the tools
|
||||||
run: .venv/bin/python -m coverage report
|
run: .venv/bin/python -m coverage report
|
||||||
|
|
||||||
|
- name: Lint the shell scripts of the actions
|
||||||
|
env:
|
||||||
|
SHELLCHECK: .venv/bin/shellcheck
|
||||||
|
SHFMT: .venv/bin/shfmt
|
||||||
|
run: bash tools/lint-action-scripts.sh actions
|
||||||
|
|||||||
@@ -20,6 +20,14 @@ Each release lists its changes in these groups, in this order:
|
|||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
|
## [2.2.0] - 2026-10-21
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `english-comments.yml`, a reusable workflow that judges the language of the comments and docstrings a pull request adds, fails the run when one is not English and writes one comment on the pull request that mentions the author (ADR-0012), with the scripts `tools/english_comments.py` and `tools/english_comments_report.py`, the guide `guides/english-comments.md` and wiki pages in three languages. The scanner of `tools/count_loc.py` can return the text of the comments it finds.
|
||||||
|
- `actions/connect-wireguard`, a composite action that brings a WireGuard tunnel up on a Gitea Actions runner for a deployment, with the guide `guides/vpn-wireguard.md`. Experimental.
|
||||||
|
- `actions/connect-tailscale`, a composite action for Gitea Actions that joins a Tailscale network for a deployment (the Gitea counterpart of the marketplace action `tailscale/github-action`), with the guide `guides/vpn-tailscale.md`. Experimental.
|
||||||
|
|
||||||
## [2.1.1] - 2026-10-14
|
## [2.1.1] - 2026-10-14
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|||||||
@@ -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,82 @@
|
|||||||
|
name: Connect Tailscale
|
||||||
|
description: >-
|
||||||
|
Joins a Tailscale network (tailnet) from a Gitea Actions runner, so the next
|
||||||
|
steps of the job (a deployment) can reach hosts on the tailnet. Written for
|
||||||
|
Gitea Actions; it replaces the GitHub marketplace action tailscale/github-action,
|
||||||
|
which is not verified on Gitea. Run it again with action "down" in a last step
|
||||||
|
with if always().
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: "up joins the tailnet; down logs the node out and stops tailscaled"
|
||||||
|
required: false
|
||||||
|
default: "up"
|
||||||
|
authkey:
|
||||||
|
description: "A Tailscale auth key (a secret). Use an ephemeral, tagged, pre-authorized key. Give authkey or oauth-secret, not both"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
oauth-secret:
|
||||||
|
description: "The secret of a Tailscale OAuth client with the scope auth_keys (a secret). Needs tags"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
tags:
|
||||||
|
description: "ACL tags of the node, comma separated, e.g. tag:ci. Required with oauth-secret"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
hostname:
|
||||||
|
description: "Name of the node in the tailnet. Empty uses the runner name"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
version:
|
||||||
|
description: "Exact Tailscale version to install when tailscale is not on the runner, e.g. 1.80.2. Empty uses the installed tailscale and fails when there is none"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
sha256:
|
||||||
|
description: "Optional SHA-256 of the downloaded archive tailscale_<version>_<arch>.tgz; the install fails when it differs"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
userspace-networking:
|
||||||
|
description: >-
|
||||||
|
true runs tailscaled without a TUN device (no NET_ADMIN needed); tailnet
|
||||||
|
hosts are then reached through the SOCKS5 proxy localhost:1055, e.g. ssh -o
|
||||||
|
ProxyCommand="nc -X 5 -x localhost:1055 %h %p". false creates a TUN device.
|
||||||
|
required: false
|
||||||
|
default: "false"
|
||||||
|
args:
|
||||||
|
description: "Extra arguments for tailscale up, e.g. --accept-routes. Never put a secret here"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
timeout:
|
||||||
|
description: "Seconds to wait for the node to come up"
|
||||||
|
required: false
|
||||||
|
default: "60"
|
||||||
|
health-check-host:
|
||||||
|
description: "Optional tailnet host that must answer a Tailscale ping, else the step fails"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Mask the secrets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TS_AUTHKEY: ${{ inputs.authkey }}
|
||||||
|
TS_OAUTH_SECRET: ${{ inputs.oauth-secret }}
|
||||||
|
run: bash "${{ gitea.action_path }}/connect-tailscale.sh" mask
|
||||||
|
|
||||||
|
- name: Run the Tailscale step
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
TS_ACTION: ${{ inputs.action }}
|
||||||
|
TS_AUTHKEY: ${{ inputs.authkey }}
|
||||||
|
TS_OAUTH_SECRET: ${{ inputs.oauth-secret }}
|
||||||
|
TS_TAGS: ${{ inputs.tags }}
|
||||||
|
TS_HOSTNAME: ${{ inputs.hostname }}
|
||||||
|
TS_VERSION: ${{ inputs.version }}
|
||||||
|
TS_SHA256: ${{ inputs.sha256 }}
|
||||||
|
TS_USERSPACE: ${{ inputs.userspace-networking }}
|
||||||
|
TS_ARGS: ${{ inputs.args }}
|
||||||
|
TS_TIMEOUT: ${{ inputs.timeout }}
|
||||||
|
TS_HEALTH_CHECK_HOST: ${{ inputs.health-check-host }}
|
||||||
|
run: bash "${{ gitea.action_path }}/connect-tailscale.sh" "$TS_ACTION"
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Steps of the composite action actions/connect-tailscale/action.yml.
|
||||||
|
#
|
||||||
|
# connect-tailscale.sh mask print ::add-mask:: for every secret input
|
||||||
|
# connect-tailscale.sh up install tailscale if needed, start tailscaled and join the tailnet
|
||||||
|
# connect-tailscale.sh down log the node out and stop tailscaled
|
||||||
|
#
|
||||||
|
# Settings come from the environment, never from interpolated expressions:
|
||||||
|
# TS_AUTHKEY, TS_OAUTH_SECRET, TS_TAGS, TS_HOSTNAME, TS_VERSION, TS_USERSPACE,
|
||||||
|
# TS_SHA256, TS_ARGS, TS_TIMEOUT, TS_HEALTH_CHECK_HOST. TS_TAILSCALE,
|
||||||
|
# TS_TAILSCALED, TS_SUDO, TS_STATE_DIR, TS_START_WAIT (tries) and
|
||||||
|
# TS_WAIT_INTERVAL (seconds) name the commands, the work folder and the waits,
|
||||||
|
# for the tests.
|
||||||
|
# The auth key is passed to tailscale in a file of mode 600, never on a command
|
||||||
|
# line. Every failure is printed as ::error::[category] message.
|
||||||
|
|
||||||
|
TS_TAILSCALE="${TS_TAILSCALE:-tailscale}"
|
||||||
|
TS_TAILSCALED="${TS_TAILSCALED:-tailscaled}"
|
||||||
|
# sudo is used only when the job is not root: a job container runs as root and
|
||||||
|
# often has no sudo. TS_SUDO overrides it (empty for none).
|
||||||
|
if [ -n "${TS_SUDO+x}" ]; then
|
||||||
|
SUDO="$TS_SUDO"
|
||||||
|
elif [ "$(id -u)" = "0" ]; then
|
||||||
|
SUDO=""
|
||||||
|
else
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
STATE_DIR="${TS_STATE_DIR:-${RUNNER_TEMP:-/tmp}/tailscale}"
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "::error::[$1] $2"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mask_secrets() {
|
||||||
|
local value
|
||||||
|
for value in "${TS_AUTHKEY:-}" "${TS_OAUTH_SECRET:-}"; do
|
||||||
|
[ -n "$value" ] && echo "::add-mask::$value"
|
||||||
|
done
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
validate_inputs() {
|
||||||
|
if [ -n "${TS_AUTHKEY:-}" ] && [ -n "${TS_OAUTH_SECRET:-}" ]; then
|
||||||
|
fail configuration "give authkey or oauth-secret, not both."
|
||||||
|
fi
|
||||||
|
if [ -z "${TS_AUTHKEY:-}" ] && [ -z "${TS_OAUTH_SECRET:-}" ]; then
|
||||||
|
fail configuration "give authkey or oauth-secret."
|
||||||
|
fi
|
||||||
|
if [ -n "${TS_OAUTH_SECRET:-}" ] && [ -z "${TS_TAGS:-}" ]; then
|
||||||
|
fail configuration "tags is required with oauth-secret (for example tag:ci)."
|
||||||
|
fi
|
||||||
|
if [ -n "${TS_TAGS:-}" ]; then
|
||||||
|
[[ "$TS_TAGS" =~ ^tag:[a-zA-Z0-9-]+(,tag:[a-zA-Z0-9-]+)*$ ]] ||
|
||||||
|
fail configuration "tags must look like tag:ci or tag:ci,tag:deploy."
|
||||||
|
fi
|
||||||
|
[[ "$TS_TIMEOUT" =~ ^[0-9]+$ ]] ||
|
||||||
|
fail configuration "timeout must be a number of seconds."
|
||||||
|
[[ "$TS_VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+)?$ ]] ||
|
||||||
|
fail configuration "version must be an exact version like 1.80.2."
|
||||||
|
[[ "${TS_SHA256:-}" =~ ^([0-9a-fA-F]{64})?$ ]] ||
|
||||||
|
fail configuration "sha256 must be 64 hexadecimal digits."
|
||||||
|
case "$TS_USERSPACE" in
|
||||||
|
true | false) ;;
|
||||||
|
*) fail configuration "userspace-networking must be true or false." ;;
|
||||||
|
esac
|
||||||
|
[[ "${TS_HOSTNAME:-}" =~ ^[a-zA-Z0-9-]*$ ]] ||
|
||||||
|
fail configuration "hostname may hold letters, digits and hyphens only."
|
||||||
|
}
|
||||||
|
|
||||||
|
machine_arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | amd64) echo amd64 ;;
|
||||||
|
aarch64 | arm64) echo arm64 ;;
|
||||||
|
*) fail environment "no Tailscale build for $(uname -m)." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
install_tools() {
|
||||||
|
command -v "$TS_TAILSCALE" >/dev/null 2>&1 && command -v "$TS_TAILSCALED" >/dev/null 2>&1 && return 0
|
||||||
|
[ -n "$TS_VERSION" ] ||
|
||||||
|
fail configuration "tailscale is not on the runner; set version (for example 1.80.2) so it can be installed."
|
||||||
|
local arch url tmp
|
||||||
|
arch="$(machine_arch)"
|
||||||
|
url="https://pkgs.tailscale.com/stable/tailscale_${TS_VERSION}_${arch}.tgz"
|
||||||
|
echo "Installing Tailscale $TS_VERSION"
|
||||||
|
tmp="$(mktemp -d)"
|
||||||
|
curl -fsSL "$url" -o "$tmp/tailscale.tgz" ||
|
||||||
|
fail environment "could not download $url."
|
||||||
|
if [ -n "${TS_SHA256:-}" ]; then
|
||||||
|
echo "${TS_SHA256} $tmp/tailscale.tgz" | sha256sum -c --quiet - ||
|
||||||
|
fail environment "the downloaded archive does not match sha256."
|
||||||
|
fi
|
||||||
|
tar -xzf "$tmp/tailscale.tgz" -C "$tmp" --strip-components=1 ||
|
||||||
|
fail environment "could not unpack the Tailscale archive."
|
||||||
|
$SUDO install -m 755 "$tmp/tailscale" "$tmp/tailscaled" /usr/local/bin/ ||
|
||||||
|
fail environment "could not install tailscale (root or sudo is needed)."
|
||||||
|
rm -rf "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
daemon_answers() {
|
||||||
|
"$TS_TAILSCALE" version --daemon >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
start_daemon() {
|
||||||
|
# a daemon that already runs belongs to the host; this action would log it out
|
||||||
|
if daemon_answers; then
|
||||||
|
fail environment "a tailscaled already runs on this runner; the action starts its own and must not take over the host's. Use it on a job container."
|
||||||
|
fi
|
||||||
|
local args=(--state=mem:)
|
||||||
|
if [ "$TS_USERSPACE" = "true" ]; then
|
||||||
|
args+=(--tun=userspace-networking --socks5-server=localhost:1055)
|
||||||
|
fi
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
$SUDO nohup "$TS_TAILSCALED" "${args[@]}" >"$STATE_DIR/tailscaled.log" 2>&1 &
|
||||||
|
: >"$STATE_DIR/started-by-action"
|
||||||
|
wait_for_daemon
|
||||||
|
}
|
||||||
|
|
||||||
|
# wait_for_daemon: tailscaled needs a moment to create its socket
|
||||||
|
wait_for_daemon() {
|
||||||
|
local tries="${TS_START_WAIT:-30}" i
|
||||||
|
for ((i = 0; i < tries; i++)); do
|
||||||
|
daemon_answers && return 0
|
||||||
|
sleep "${TS_WAIT_INTERVAL:-1}"
|
||||||
|
done
|
||||||
|
fail connection "tailscaled did not answer in $tries tries; the last lines of its log: $(tail -n 5 "$STATE_DIR/tailscaled.log" 2>/dev/null | tr '
|
||||||
|
' ' ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
# write_key_file: the auth key in a file of mode 600; prints its path
|
||||||
|
write_key_file() {
|
||||||
|
local file="$STATE_DIR/authkey" key
|
||||||
|
if [ -n "${TS_OAUTH_SECRET:-}" ]; then
|
||||||
|
key="${TS_OAUTH_SECRET}?ephemeral=true&preauthorized=true"
|
||||||
|
else
|
||||||
|
key="$TS_AUTHKEY"
|
||||||
|
fi
|
||||||
|
mkdir -p "$STATE_DIR"
|
||||||
|
(umask 077 && printf '%s' "$key" >"$file")
|
||||||
|
echo "$file"
|
||||||
|
}
|
||||||
|
|
||||||
|
tailnet_up() {
|
||||||
|
validate_inputs
|
||||||
|
install_tools
|
||||||
|
start_daemon
|
||||||
|
local keyfile up_args extra
|
||||||
|
keyfile="$(write_key_file)"
|
||||||
|
up_args=("--auth-key=file:$keyfile" "--timeout=${TS_TIMEOUT}s")
|
||||||
|
[ -z "${TS_HOSTNAME:-}" ] || up_args+=("--hostname=$TS_HOSTNAME")
|
||||||
|
[ -z "${TS_TAGS:-}" ] || up_args+=("--advertise-tags=$TS_TAGS")
|
||||||
|
# TS_ARGS is split on spaces on purpose: it holds flags, never a secret
|
||||||
|
# shellcheck disable=SC2206
|
||||||
|
extra=(${TS_ARGS:-})
|
||||||
|
if ! $SUDO "$TS_TAILSCALE" up "${up_args[@]}" "${extra[@]}"; then
|
||||||
|
rm -f "$keyfile"
|
||||||
|
fail connection "tailscale up failed; the last lines of tailscaled: $(tail -n 5 "$STATE_DIR/tailscaled.log" 2>/dev/null | tr '\n' ' ')"
|
||||||
|
fi
|
||||||
|
rm -f "$keyfile"
|
||||||
|
if [ -n "${TS_HEALTH_CHECK_HOST:-}" ]; then
|
||||||
|
"$TS_TAILSCALE" ping --c 3 --timeout 10s "$TS_HEALTH_CHECK_HOST" >/dev/null 2>&1 ||
|
||||||
|
fail connection "health-check-host '$TS_HEALTH_CHECK_HOST' does not answer on the tailnet."
|
||||||
|
fi
|
||||||
|
echo "Tailscale is up."
|
||||||
|
}
|
||||||
|
|
||||||
|
tailnet_down() {
|
||||||
|
# only a daemon this action started is logged out and stopped
|
||||||
|
if [ ! -f "$STATE_DIR/started-by-action" ]; then
|
||||||
|
echo "This action did not start tailscaled on this runner; nothing to do."
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
$SUDO "$TS_TAILSCALE" logout 2>/dev/null || echo "The node was not logged in."
|
||||||
|
$SUDO "$TS_TAILSCALE" down 2>/dev/null || true
|
||||||
|
$SUDO pkill -x "$(basename "$TS_TAILSCALED")" 2>/dev/null || true
|
||||||
|
rm -rf "$STATE_DIR"
|
||||||
|
echo "Tailscale is down."
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
set -uo pipefail
|
||||||
|
TS_USERSPACE="${TS_USERSPACE:-false}"
|
||||||
|
TS_TIMEOUT="${TS_TIMEOUT:-60}"
|
||||||
|
TS_VERSION="${TS_VERSION:-}"
|
||||||
|
case "${1:-}" in
|
||||||
|
mask) mask_secrets ;;
|
||||||
|
up) tailnet_up ;;
|
||||||
|
down) tailnet_down ;;
|
||||||
|
*) fail configuration "unknown command '${1:-}'; use mask, up or down." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
name: Connect WireGuard
|
||||||
|
description: >-
|
||||||
|
Brings up a WireGuard tunnel on a Gitea Actions runner, so the next steps of
|
||||||
|
the job (a deployment) can reach hosts on a private network. Run it again
|
||||||
|
with action "down" in a last step with if always().
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
action:
|
||||||
|
description: "up brings the tunnel up; down takes it down and removes the configuration"
|
||||||
|
required: false
|
||||||
|
default: "up"
|
||||||
|
config:
|
||||||
|
description: >-
|
||||||
|
A complete wg-quick configuration, stored as a Gitea secret. When set, the
|
||||||
|
individual inputs below are ignored.
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
private-key:
|
||||||
|
description: "Private key of this runner (a secret)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
address:
|
||||||
|
description: "Address of this runner inside the tunnel, e.g. 10.8.0.2/32"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
peer-public-key:
|
||||||
|
description: "Public key of the WireGuard server"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
endpoint:
|
||||||
|
description: "Address of the WireGuard server as host:port"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
allowed-ips:
|
||||||
|
description: "Networks routed through the tunnel, comma separated. Never 0.0.0.0/0 unless the deployment needs it"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
preshared-key:
|
||||||
|
description: "Optional preshared key (a secret)"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
dns:
|
||||||
|
description: "Optional DNS server for the tunnel"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
persistent-keepalive:
|
||||||
|
description: "Seconds between keepalive packets, 0 turns them off"
|
||||||
|
required: false
|
||||||
|
default: "25"
|
||||||
|
interface:
|
||||||
|
description: "Name of the interface"
|
||||||
|
required: false
|
||||||
|
default: "wg0"
|
||||||
|
health-check-host:
|
||||||
|
description: "Optional host behind the tunnel that must answer a ping, else the step fails"
|
||||||
|
required: false
|
||||||
|
default: ""
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Mask the secrets
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
WG_CONFIG: ${{ inputs.config }}
|
||||||
|
WG_PRIVATE_KEY: ${{ inputs.private-key }}
|
||||||
|
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
|
||||||
|
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" mask
|
||||||
|
|
||||||
|
- name: Run the WireGuard step
|
||||||
|
shell: bash
|
||||||
|
env:
|
||||||
|
WG_ACTION: ${{ inputs.action }}
|
||||||
|
WG_CONFIG: ${{ inputs.config }}
|
||||||
|
WG_PRIVATE_KEY: ${{ inputs.private-key }}
|
||||||
|
WG_ADDRESS: ${{ inputs.address }}
|
||||||
|
WG_PEER_PUBLIC_KEY: ${{ inputs.peer-public-key }}
|
||||||
|
WG_ENDPOINT: ${{ inputs.endpoint }}
|
||||||
|
WG_ALLOWED_IPS: ${{ inputs.allowed-ips }}
|
||||||
|
WG_PRESHARED_KEY: ${{ inputs.preshared-key }}
|
||||||
|
WG_DNS: ${{ inputs.dns }}
|
||||||
|
WG_KEEPALIVE: ${{ inputs.persistent-keepalive }}
|
||||||
|
WG_INTERFACE: ${{ inputs.interface }}
|
||||||
|
WG_HEALTH_CHECK_HOST: ${{ inputs.health-check-host }}
|
||||||
|
run: bash "${{ gitea.action_path }}/connect-wireguard.sh" "$WG_ACTION"
|
||||||
@@ -0,0 +1,160 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Steps of the composite action actions/connect-wireguard/action.yml.
|
||||||
|
#
|
||||||
|
# connect-wireguard.sh mask print ::add-mask:: for every secret input
|
||||||
|
# connect-wireguard.sh up write the configuration and bring the tunnel up
|
||||||
|
# connect-wireguard.sh down take the tunnel down and delete the configuration
|
||||||
|
#
|
||||||
|
# Settings come from the environment, never from interpolated expressions:
|
||||||
|
# WG_CONFIG, WG_PRIVATE_KEY, WG_ADDRESS, WG_PEER_PUBLIC_KEY, WG_ENDPOINT,
|
||||||
|
# WG_ALLOWED_IPS, WG_PRESHARED_KEY, WG_DNS, WG_KEEPALIVE, WG_INTERFACE,
|
||||||
|
# WG_HEALTH_CHECK_HOST. WG_DIR is the configuration directory (/etc/wireguard
|
||||||
|
# by default); WG_QUICK, WG_SUDO and WG_PING name the commands, for the tests.
|
||||||
|
# Every failure is printed as ::error::[category] message. A key is never printed.
|
||||||
|
|
||||||
|
WG_DIR="${WG_DIR:-/etc/wireguard}"
|
||||||
|
WG_QUICK="${WG_QUICK:-wg-quick}"
|
||||||
|
WG_PING="${WG_PING:-ping}"
|
||||||
|
# sudo is used only when the job is not root: a job container runs as root and
|
||||||
|
# often has no sudo. WG_SUDO overrides it (empty for none).
|
||||||
|
if [ -n "${WG_SUDO+x}" ]; then
|
||||||
|
SUDO="$WG_SUDO"
|
||||||
|
elif [ "$(id -u)" = "0" ]; then
|
||||||
|
SUDO=""
|
||||||
|
else
|
||||||
|
SUDO="sudo"
|
||||||
|
fi
|
||||||
|
# variables that must reach wg-quick through sudo, which resets the environment
|
||||||
|
WG_QUICK_ENV=()
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
echo "::error::[$1] $2"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
mask_secrets() {
|
||||||
|
local value line key
|
||||||
|
for value in "${WG_PRIVATE_KEY:-}" "${WG_PRESHARED_KEY:-}"; do
|
||||||
|
[ -n "$value" ] && echo "::add-mask::$value"
|
||||||
|
done
|
||||||
|
# a full configuration: mask the value of every PrivateKey and PresharedKey
|
||||||
|
# line, and nothing else, so the rest of the log stays readable
|
||||||
|
while IFS= read -r line; do
|
||||||
|
line="${line//$'␍'/}"
|
||||||
|
key="${line%%=*}"
|
||||||
|
key="${key//[[:space:]]/}"
|
||||||
|
case "${key,,}" in
|
||||||
|
privatekey | presharedkey)
|
||||||
|
value="${line#*=}"
|
||||||
|
value="${value#"${value%%[![:space:]]*}"}"
|
||||||
|
value="${value%"${value##*[![:space:]]}"}"
|
||||||
|
[ -n "$value" ] && echo "::add-mask::$value"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
done <<<"${WG_CONFIG:-}"
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
check_interface() {
|
||||||
|
[[ "$WG_INTERFACE" =~ ^[a-zA-Z0-9_=+.-]{1,15}$ ]] ||
|
||||||
|
fail configuration "interface '$WG_INTERFACE' is not a valid interface name."
|
||||||
|
}
|
||||||
|
|
||||||
|
# render_config: the wg-quick configuration on stdout
|
||||||
|
render_config() {
|
||||||
|
if [ -n "${WG_CONFIG:-}" ]; then
|
||||||
|
printf '%s\n' "$WG_CONFIG"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local name
|
||||||
|
for name in WG_PRIVATE_KEY WG_ADDRESS WG_PEER_PUBLIC_KEY WG_ENDPOINT WG_ALLOWED_IPS; do
|
||||||
|
[ -n "${!name:-}" ] ||
|
||||||
|
fail configuration "set config, or all of private-key, address, peer-public-key, endpoint and allowed-ips ($name is empty)."
|
||||||
|
done
|
||||||
|
for name in WG_PRIVATE_KEY WG_ADDRESS WG_PEER_PUBLIC_KEY WG_ENDPOINT WG_ALLOWED_IPS WG_PRESHARED_KEY WG_DNS; do
|
||||||
|
# a line break would add directives (PostUp runs as root) to the file
|
||||||
|
[[ "${!name:-}" != *[[:cntrl:]]* ]] ||
|
||||||
|
fail configuration "$name must be one line without control characters."
|
||||||
|
done
|
||||||
|
[[ "$WG_KEEPALIVE" =~ ^[0-9]+$ ]] ||
|
||||||
|
fail configuration "persistent-keepalive must be a number of seconds."
|
||||||
|
printf '[Interface]\nPrivateKey = %s\nAddress = %s\n' "$WG_PRIVATE_KEY" "$WG_ADDRESS"
|
||||||
|
[ -z "${WG_DNS:-}" ] || printf 'DNS = %s\n' "$WG_DNS"
|
||||||
|
printf '\n[Peer]\nPublicKey = %s\nEndpoint = %s\nAllowedIPs = %s\nPersistentKeepalive = %s\n' \
|
||||||
|
"$WG_PEER_PUBLIC_KEY" "$WG_ENDPOINT" "$WG_ALLOWED_IPS" "$WG_KEEPALIVE"
|
||||||
|
[ -z "${WG_PRESHARED_KEY:-}" ] || printf 'PresharedKey = %s\n' "$WG_PRESHARED_KEY"
|
||||||
|
}
|
||||||
|
|
||||||
|
install_tools() {
|
||||||
|
command -v "$WG_QUICK" >/dev/null 2>&1 && return 0
|
||||||
|
echo "Installing wireguard-tools"
|
||||||
|
if ! { $SUDO apt-get update -qq &&
|
||||||
|
$SUDO apt-get install -y -qq wireguard-tools resolvconf iproute2 iputils-ping; }; then
|
||||||
|
fail environment "wireguard-tools is not installed and could not be installed (a Debian or Ubuntu runner with root is needed)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# Use the user-space implementation when the kernel has no WireGuard module,
|
||||||
|
# which is the usual case in a Gitea runner container.
|
||||||
|
choose_implementation() {
|
||||||
|
if $SUDO ip link add dev "wgprobe$$" type wireguard 2>/dev/null; then
|
||||||
|
$SUDO ip link delete dev "wgprobe$$" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
if command -v wireguard-go >/dev/null 2>&1 || $SUDO apt-get install -y -qq wireguard-go 2>/dev/null; then
|
||||||
|
WG_QUICK_ENV=(WG_QUICK_USERSPACE_IMPLEMENTATION=wireguard-go)
|
||||||
|
echo "No kernel WireGuard module; using wireguard-go."
|
||||||
|
else
|
||||||
|
fail environment "the runner has no WireGuard kernel module and wireguard-go could not be installed; the runner container needs NET_ADMIN and /dev/net/tun (see guides/vpn-wireguard.md)."
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel_up() {
|
||||||
|
check_interface
|
||||||
|
local conf="$WG_DIR/$WG_INTERFACE.conf" rendered
|
||||||
|
# on failure the captured output is the ::error:: line, which must be shown
|
||||||
|
rendered="$(render_config)" || {
|
||||||
|
echo "$rendered"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
install_tools
|
||||||
|
choose_implementation
|
||||||
|
$SUDO install -d -m 700 "$WG_DIR"
|
||||||
|
(umask 077 && printf '%s\n' "$rendered" | $SUDO tee "$conf" >/dev/null)
|
||||||
|
$SUDO chmod 600 "$conf"
|
||||||
|
# env keeps the userspace variable through sudo; the file holds the key, so
|
||||||
|
# it is removed when the interface does not come up
|
||||||
|
if ! $SUDO env ${WG_QUICK_ENV[@]+"${WG_QUICK_ENV[@]}"} "$WG_QUICK" up "$WG_INTERFACE"; then
|
||||||
|
$SUDO rm -f "$conf"
|
||||||
|
fail connection "wg-quick could not bring $WG_INTERFACE up."
|
||||||
|
fi
|
||||||
|
if [ -n "${WG_HEALTH_CHECK_HOST:-}" ]; then
|
||||||
|
"$WG_PING" -c 3 -W 5 "$WG_HEALTH_CHECK_HOST" >/dev/null 2>&1 ||
|
||||||
|
fail connection "health-check-host '$WG_HEALTH_CHECK_HOST' does not answer through the tunnel."
|
||||||
|
fi
|
||||||
|
echo "WireGuard interface $WG_INTERFACE is up."
|
||||||
|
}
|
||||||
|
|
||||||
|
tunnel_down() {
|
||||||
|
check_interface
|
||||||
|
local conf="$WG_DIR/$WG_INTERFACE.conf"
|
||||||
|
$SUDO "$WG_QUICK" down "$WG_INTERFACE" 2>/dev/null || echo "Interface $WG_INTERFACE was not up."
|
||||||
|
$SUDO rm -f "$conf"
|
||||||
|
echo "WireGuard interface $WG_INTERFACE is down."
|
||||||
|
}
|
||||||
|
|
||||||
|
main() {
|
||||||
|
set -uo pipefail
|
||||||
|
WG_INTERFACE="${WG_INTERFACE:-wg0}"
|
||||||
|
WG_KEEPALIVE="${WG_KEEPALIVE:-25}"
|
||||||
|
case "${1:-}" in
|
||||||
|
mask) mask_secrets ;;
|
||||||
|
up) tunnel_up ;;
|
||||||
|
down) tunnel_down ;;
|
||||||
|
*) fail configuration "unknown command '${1:-}'; use mask, up or down." ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
||||||
|
main "$@"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
# ADR-0011: A VPN for deployment is delivered as a composite action
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | ADR-0011 |
|
||||||
|
| CrossReference | [MIL-012], [ADR-0005], [ADR-0006] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | [3325bea] |
|
||||||
|
|
||||||
|
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
A deployment job often has to reach hosts that are not on the internet. [MIL-012] adds a way to bring a VPN up in the job, for WireGuard and for Tailscale. The GitHub marketplace has `tailscale/github-action` for Tailscale; it is written for GitHub Actions, has not been verified on Gitea Actions, and WireGuard has no such action. Every other shared automation of this repository is a reusable workflow ([ADR-0005], [ADR-0009], [ADR-0010]), so the question is whether a VPN is one too.
|
||||||
|
|
||||||
|
Options:
|
||||||
|
|
||||||
|
1. **A reusable workflow.** Consistent with the rest of the repository. But a called workflow runs as a separate job, on its own runner and network; a tunnel brought up there is gone when that job ends and is never seen by the caller's deployment job. It cannot work.
|
||||||
|
2. **The marketplace action `tailscale/github-action`.** No code to maintain. Written for GitHub, not verified on Gitea, nothing for WireGuard, and an action from outside the instance that receives the VPN secret.
|
||||||
|
3. **A composite action of this repository**, one for each VPN, in `actions/<name>/`, called as a step of the caller's own job with `uses:` and `gitea.action_path` for its script. It runs in the job, so the tunnel is there for the steps after it.
|
||||||
|
|
||||||
|
Facts: a composite action in a subfolder of this repository resolves on the instance in both the short form (`TirSystem/github-action/actions/<name>@<ref>`) and with the full address, and `gitea.action_path` points at its folder (verified in [MIL-012] task 1, runs 417 and 418 of the instance). A key must not reach a log or a command line. A tunnel needs a network interface, so the runner container needs `NET_ADMIN` and `/dev/net/tun`, which this repository cannot give.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
A VPN for deployment is delivered as **a composite action of this repository**, one per VPN: `actions/connect-wireguard` and `actions/connect-tailscale`.
|
||||||
|
|
||||||
|
- Each action has a script file next to its `action.yml` that does the work. The `run:` lines only call it, and every input reaches the script as an environment variable, never as an interpolated expression ([ADR-0005]).
|
||||||
|
- Each action has an `up` and a `down` mode. The caller runs `down` in a last step with `if: always()`.
|
||||||
|
- Secrets are masked first, written only to a file of mode 600 where a file is needed, never printed and never put on a command line.
|
||||||
|
- The scripts are linted (shellcheck and shfmt) and tested offline with stub commands on every pull request.
|
||||||
|
- The actions are versioned with the repository ([ADR-0006]); a new action is a minor release, and callers pin the tag.
|
||||||
|
- The runner is outside the scope of the action: the operator (S03) provides a runner whose container has `NET_ADMIN` and `/dev/net/tun`. The actions state the requirement and fail with a message that names it.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- The tunnel lives in the job that deploys, which a reusable workflow cannot do.
|
||||||
|
- One implementation for Gitea, covered by offline tests; no GitHub-only action receives the VPN secret.
|
||||||
|
- WireGuard and Tailscale share one contract (`up`, `down`, a health check, masked secrets, categorised errors).
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- Two scripts to maintain, and behaviour that stubs cannot show (daemon start-up, `sudo`, the runner's capabilities) is verified only by runs on a real runner ([MIL-012] tasks 3 and 4).
|
||||||
|
- A composite action cannot declare a post step, so the caller must remember the `down` step; a cancelled job can leave a node or a key file behind until the runner cleans up.
|
||||||
|
- The action needs a privileged runner, which widens the attack surface of that runner; the guides tell the operator to keep it to a dedicated label.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- [MIL-012] — the decision behind its deliverable and its tasks 3 and 4.
|
||||||
|
- [ADR-0005] — the same contract style (script file, environment variables, categorised failures) is used for the actions.
|
||||||
|
- [ADR-0006] — a new action is a minor release.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-012]: ../milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[ADR-0005]: ./adr-0005-quality-check-contract.md
|
||||||
|
[ADR-0006]: ./adr-0006-release-process-and-version-pinning.md
|
||||||
|
[ADR-0009]: ./adr-0009-wiki-publishing-reusable-workflow.md
|
||||||
|
[ADR-0010]: ./adr-0010-code-metrics-badges-contract.md
|
||||||
|
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
# ADR-0012: How a comment is judged not to be English
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | ADR-0012 |
|
||||||
|
| CrossReference | [MIL-013], [ADR-0005], [ADR-0010] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Initial version | pending |
|
||||||
|
|
||||||
|
Allowed Status values: `Proposed`, `Accepted`, `Rejected`, `Deprecated`, `Superseded by ADR-NNNN`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
[MIL-013] adds a check that fails a pull request when a comment in its source code is not written in English. A comment is short (most have three to twelve words), is full of identifiers, paths and option names, and Danish and Malay words are often close to English ones. The method has to be offline (no source text leaves the instance, criterion 6 of the milestone), small enough to install on every run, deterministic (the same comment gives the same answer on every run), and measured on a corpus, not chosen by taste (criterion 1: recall at least 90 % for Danish and Malay, at most one false positive per 200 English comments).
|
||||||
|
|
||||||
|
**Corpus** (`tests/fixtures/comments/`): 300 English comments taken from this repository (`english.tsv`: comments and first docstring lines of `tools/`, `tests/`, `actions/` and the workflows, including fragments, paths and option names), and 138 comments in other languages written for the purpose (`foreign.tsv`): 45 Danish, 45 Bahasa Malaysia, and 12 each of German, Swedish, Norwegian and French. The foreign comments are written by the AI, not collected from real code (the TirSystem repositories have none) and not checked by a native reader. They are typical code comments of three to fifteen words, some with identifiers.
|
||||||
|
|
||||||
|
**Methods compared** (`tests/fixtures/comments/compare_detectors.py`; Python 3.13):
|
||||||
|
|
||||||
|
1. A rule-based one: letters outside English (`æ ø å ä ö ü ß é ...`) and word lists for English, Danish, Malay and others.
|
||||||
|
2. `langdetect` 1.0.9, a port of Google's detector (random start unless the seed is fixed).
|
||||||
|
3. `langid` 1.1.6, restricted to the candidate languages (`en da no sv de fr ms id nl`).
|
||||||
|
4. `lingua-language-detector` 2.2.0, restricted to the same candidates.
|
||||||
|
|
||||||
|
**Results** on the corpus, as the number of comments flagged as not English:
|
||||||
|
|
||||||
|
| Method | Danish | Malay | Other (48) | English flagged (of 300) | Install size | Time per 438 comments |
|
||||||
|
| --- | --- | --- | --- | --- | --- | --- |
|
||||||
|
| Rule-based (word lists written after the corpus, so optimistic) | 42/45 | 26/45 | 39/48 | 0 | none | 0.01 s |
|
||||||
|
| `langdetect`, probability at least 0.8 | 41/45 | 42/45 | 47/48 | 14 | 1 MB | 1.45 s |
|
||||||
|
| `langid`, no threshold | 45/45 | 45/45 | 48/48 | 2 | 2 MB and `numpy` | 0.02 s |
|
||||||
|
| `lingua`, confidence at least 0.5 | 29/45 | 41/45 | 47/48 | 0 | 170 MB wheel | 0.14 s |
|
||||||
|
|
||||||
|
Lowering the `lingua` threshold trades one error for the other: at 0.4 it flags 36/45 Danish and 2 English, at 0.2 it flags all Danish and Malay and 14 English. Raising the `langid` threshold on its normalised probability does the same (`0.5`: one English comment flagged; `0.6`: none).
|
||||||
|
|
||||||
|
The two English comments `langid` flagged without a threshold are lines that are mostly a script name and an option (`connect-tailscale.sh mask print ::add-mask:: for every secret input`). A pre-filter that removes tokens that are not words (paths, options, `::` tokens, tokens with digits or underscores, camelCase) and refuses to judge fewer than three words lowers the language signal of such lines, but it made the result worse at a threshold of 0.5 (three English comments flagged), so the threshold and the pre-filter go together:
|
||||||
|
|
||||||
|
| `langid` with the pre-filter | Danish | Malay | Other (48) | English flagged | English not judged (under 3 words) |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| probability at least 0.5, 3 words | 45/45 | 45/45 | 48/48 | 3 | 35 of 300 |
|
||||||
|
| probability at least 0.6, 3 words | 44/45 | 41/45 | 48/48 | 0 | 35 of 300 |
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
A comment is judged **not English** when `langid` 1.1.6, restricted to `en da no sv de fr ms id nl`, with normalised probabilities, ranks a language other than English first with a probability of at least **0.6**, after this pre-filter, and only when the filter leaves **at least three words**:
|
||||||
|
|
||||||
|
- drop every token that is not a word: a token with a code character (`/ \ { } [ ] " ` = < > | $ @ # % & + ^ ~ *`), an underscore, a digit or `::` (a path, a URL, a JSON key, an identifier); a token that starts with `-` (an option), contains `.` (a file name), is written in capitals (an acronym or a constant) or is camelCase; a leading label such as `Usage:` or `Note:`; and the words of the allowed-words file. Split what is left on `-`;
|
||||||
|
- keep a token only if it is letters, with at most one inner apostrophe (letters include `æ ø å ä ö ü` and the other letters of Latin scripts);
|
||||||
|
- a block of several consecutive comment lines is judged **line by line first**, each line only with at least four words and a probability of at least 0.7, so an English line cannot hide a Danish one next to it (the first version judged the joined block and missed exactly that case in the trial of [MIL-013] task 8); when no line is flagged, the joined block is judged with the values above, so a sentence cut over short lines is not missed. A single comment line is judged once, with the values above;
|
||||||
|
- a comment with fewer than three words left is **not judged** (it passes), and so is a comment that holds only identifiers, URLs or numbers.
|
||||||
|
|
||||||
|
On the corpus this gives 0 false positives in 300 English comments, Danish 44/45 (97.8 %), Malay 41/45 (91.1 %), and 48/48 for the other languages, meeting criterion 1 of [MIL-013]. The threshold, the minimum of three words and the candidate languages are inputs of the workflow with these values as defaults; the rates are a test that fails when the code changes them (task 7).
|
||||||
|
|
||||||
|
The versions are pinned (`langid==1.1.6`, `numpy` at a pinned version) and installed in the job's virtual environment like the other tools ([ADR-0005]). Nothing is downloaded at run time and nothing is sent to a service.
|
||||||
|
|
||||||
|
The rules of the pre-filter came from the build, not from the first measurement. A scan of this repository with the first version of the filter found two false positives that the corpus did not contain: a docstring with a JSON example (`{"files": 3, "lines": 120, ...}`, judged French with probability 1.0) and `Usage:` lines with options and a file name (judged French with 0.6 and 0.88). The code-character, option, file-name, acronym and label rules above remove them; the corpus rates stayed the same, and the repository (31 source files) now gives no finding. Tests of the check keep both cases (the JSON docstring and the `Usage:` lines), so a change to the filter is measured against them.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive:**
|
||||||
|
|
||||||
|
- Offline, deterministic, about 2 MB plus `numpy`, 0.06 s per 1 000 comments; no model download.
|
||||||
|
- The cases that matter most for TirSystem (Danish and Malay) are caught at or above the milestone's bar, and the rule is explainable to an author: "these words look Danish".
|
||||||
|
|
||||||
|
**Negative:**
|
||||||
|
|
||||||
|
- **The rates are an estimate, not a guarantee.** The foreign comments were written by the AI and are not checked by a native reader, the threshold and the filter were chosen on the same corpus they are measured on, and the English comments come from one repository. Real Danish and Malay comments will differ; the corpus must grow with every miss or false positive found in use (the allowed-words file and the exception marker of the workflow are the answer to a false positive in the meantime).
|
||||||
|
- Malay recall of 91.1 % leaves little margin over the bar of 90 %; short Malay comments and comments with many English technical terms are the cases it misses.
|
||||||
|
- A short comment (under three words after the filter) is never judged, so a Danish "TODO: ret" passes. This is the price of a low false positive rate.
|
||||||
|
- `langid` is not actively developed (its last release is 1.1.6). It is pinned, and `lingua` is the recorded alternative (better on long text, much larger, and weaker on short Danish at a comparable false positive rate).
|
||||||
|
- The method judges the language of words, not the quality of English: a comment in poor English passes.
|
||||||
|
|
||||||
|
## Affected Artifacts
|
||||||
|
|
||||||
|
- [MIL-013] — the decision behind tasks 3, 4 and 7, and criterion 1.
|
||||||
|
- [ADR-0005] — the tools are pinned and installed in the job's environment, like the quality-check tools.
|
||||||
|
- [ADR-0010] — the comment scanner of the code counter is reused to extract the comments.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-013]: ../milestones/mil-013-english-comments-check.md
|
||||||
|
[ADR-0005]: ./adr-0005-quality-check-contract.md
|
||||||
|
[ADR-0010]: ./adr-0010-code-metrics-badges-contract.md
|
||||||
@@ -10,10 +10,10 @@ each document lives in *this* project and the next version to use.
|
|||||||
| BC | Business Case | docs/business-case.md | 002 |
|
| BC | Business Case | docs/business-case.md | 002 |
|
||||||
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
| SA | Stakeholder Analysis | docs/stakeholder-analysis.md | 002 |
|
||||||
| PP | Project Plan | docs/project-plan.md | 002 |
|
| PP | Project Plan | docs/project-plan.md | 002 |
|
||||||
| MIL | Milestone / Gateway | docs/milestones/*.md | 012 |
|
| MIL | Milestone / Gateway | docs/milestones/*.md | 014 |
|
||||||
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 079 |
|
| RC | SQA Review Record | docs/sqa/reviews/rc-*.md | 086 |
|
||||||
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
| TM | Traceability Matrix | docs/sqa/traceability-matrix.md | 002 |
|
||||||
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0011 |
|
| ADR | Architecture Decision Record | docs/adr/adr-NNNN-*.md | 0013 |
|
||||||
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
|
| UCD | Use Case Diagram | docs/use-case-diagram.md | 002 |
|
||||||
| US | User Story | docs/user-stories.md | 002 |
|
| US | User Story | docs/user-stories.md | 002 |
|
||||||
| UC | Use Case | docs/uc-NNN/uc.md | 009 |
|
| UC | Use Case | docs/uc-NNN/uc.md | 009 |
|
||||||
|
|||||||
+18
-4
@@ -9,8 +9,8 @@
|
|||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-10 | Deprecated | Jens Tirsvad Nielsen | S01 | Scope: wiki pages in English, Danish and Bahasa Malaysia | [08ef26a] |
|
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Objective O6, scope item, assumption, constraint and success criterion 6: shared actions that bring a VPN up for a deployment job (MIL-012); reviewed (RC-081), accepted by the author in chat as stand-in for S01 | [c5d5941] |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Objective O5, scope item and success criterion 5: shared workflow for code metrics and coverage badges (MIL-010); reviewed (RC-067) | [5002450] |
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Objective O7, scope item, assumption, constraint and success criterion 7: a shared workflow that checks that the comments in source code are in English (MIL-013); reviewed (RC-084), accepted by the author in chat as stand-in for S01 | [e35b1c5] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -62,6 +62,12 @@ made once.
|
|||||||
- **O5:** Offer a shared workflow that counts the code files and the lines of
|
- **O5:** Offer a shared workflow that counts the code files and the lines of
|
||||||
code (without comment and blank lines) of a repository, reads its coverage
|
code (without comment and blank lines) of a repository, reads its coverage
|
||||||
report and publishes the three results as SVG badges.
|
report and publishes the three results as SVG badges.
|
||||||
|
- **O6:** Offer shared, versioned actions that bring a VPN (WireGuard or
|
||||||
|
Tailscale) up for a deployment job on a Gitea runner, so the job can reach a
|
||||||
|
private network.
|
||||||
|
- **O7:** Offer a shared workflow that checks that the comments in the source
|
||||||
|
code of a pull request are written in English, blocks the pull request when
|
||||||
|
they are not, and tells its author which comment and why.
|
||||||
|
|
||||||
## Scope
|
## Scope
|
||||||
|
|
||||||
@@ -72,6 +78,8 @@ made once.
|
|||||||
- A Gitea wiki with the same documentation in English, Danish and Bahasa Malaysia (MIL-005).
|
- A Gitea wiki with the same documentation in English, Danish and Bahasa Malaysia (MIL-005).
|
||||||
- Validation of workflow files and their documentation.
|
- Validation of workflow files and their documentation.
|
||||||
- A shared workflow for code metrics and coverage badges (MIL-010).
|
- A shared workflow for code metrics and coverage badges (MIL-010).
|
||||||
|
- Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale (MIL-012).
|
||||||
|
- A shared workflow that checks that comments in source code are in English and tells the author (MIL-013).
|
||||||
|
|
||||||
### Out of Scope
|
### Out of Scope
|
||||||
|
|
||||||
@@ -109,6 +117,8 @@ accepted plans.
|
|||||||
| 3 | Documented use cases that are implemented or marked planned | 100% | README capability table against workflows present |
|
| 3 | Documented use cases that are implemented or marked planned | 100% | README capability table against workflows present |
|
||||||
| 4 | Repositories consuming a shared workflow | At least 1 besides this one by MIL-002 | Consumer list in README |
|
| 4 | Repositories consuming a shared workflow | At least 1 besides this one by MIL-002 | Consumer list in README |
|
||||||
| 5 | Repositories that show the code metrics and coverage badges from the shared workflow | At least 1 besides this one by MIL-010 | README of the consuming repository |
|
| 5 | Repositories that show the code metrics and coverage badges from the shared workflow | At least 1 besides this one by MIL-010 | README of the consuming repository |
|
||||||
|
| 6 | VPN actions that connect a deployment job on the instance, with no secret in the run log | 2 (WireGuard and Tailscale) by MIL-012 | Recorded runs of MIL-012 tasks 3 to 5 |
|
||||||
|
| 7 | Comments in source code that are not in English and reach the default branch of a repository that calls the check | 0 after MIL-013 | The check's run on the default branch; a throwaway pull request with a finding that cannot be merged |
|
||||||
|
|
||||||
## Risks
|
## Risks
|
||||||
|
|
||||||
@@ -124,11 +134,15 @@ accepted plans.
|
|||||||
- The Gitea instance has Actions enabled and an `ubuntu-latest` runner with `python3`.
|
- The Gitea instance has Actions enabled and an `ubuntu-latest` runner with `python3`.
|
||||||
- A mirrored repository has at most one GitHub push mirror. Repositories without a GitHub mirror are skipped successfully.
|
- A mirrored repository has at most one GitHub push mirror. Repositories without a GitHub mirror are skipped successfully.
|
||||||
- S01 (Product Owner) and S05 (DevOps owner and security authority) are both maintainers.
|
- S01 (Product Owner) and S05 (DevOps owner and security authority) are both maintainers.
|
||||||
|
- For the VPN actions (MIL-012), S03 provides a runner whose container has the capability `NET_ADMIN` and the device `/dev/net/tun`, and the test peers; the actions do not configure the runner.
|
||||||
|
- For the English-comments check (MIL-013), S03 provides a Gitea token that may comment on pull requests, as an organization secret, and S01 or S03 decide which repositories mark the check as required; the workflow does not change branch protection.
|
||||||
|
|
||||||
## Constraints
|
## Constraints
|
||||||
|
|
||||||
- Workflows use only tools available on the runner; the current workflow uses the Python standard library only.
|
- Workflows use only tools available on the runner; the current workflow uses the Python standard library only.
|
||||||
- Secrets are supplied as repository or organization secrets, never committed.
|
- Secrets are supplied as repository or organization secrets, never committed.
|
||||||
|
- A VPN key or token never appears in a log, an artifact or a command line (MIL-012).
|
||||||
|
- The English-comments check (MIL-013) runs without sending source text to a service outside the instance, and its token never appears in a log.
|
||||||
- Nothing is written under `src/` or `tests/` before the matching milestone is accepted.
|
- Nothing is written under `src/` or `tests/` before the matching milestone is accepted.
|
||||||
|
|
||||||
## Cost–Benefit Assessment
|
## Cost–Benefit Assessment
|
||||||
@@ -154,5 +168,5 @@ Proceed — the first milestone fixes a known defect in the only existing workfl
|
|||||||
---
|
---
|
||||||
|
|
||||||
[SA-001]: ./stakeholder-analysis.md
|
[SA-001]: ./stakeholder-analysis.md
|
||||||
[08ef26a]: https://git.tirsystem.com/TirSystem/github-action/commit/08ef26a6d3eb98c066b45634c2271e578e6c0a0b
|
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
|
||||||
[5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of task 1 (reproduction and the defaults of the five workflows); reviewed (RC-077), accepted by the AI as stand-in for S01 in autonomous mode | [e11c43f] |
|
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | S01 | Added the results of task 1 (reproduction and the defaults of the five workflows); reviewed (RC-077), accepted by the AI as stand-in for S01 in autonomous mode | [e11c43f] |
|
||||||
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added the results of tasks 2 to 5 (the fix, the test, the guides, the run on the instance); reviewed in RC-078, the release follows | [a6892c3] |
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 2 to 5 (the fix, the test, the guides, the run on the instance); reviewed in RC-078, the release follows; outcome reviewed (RC-079), accepted by the AI as stand-in for S01 in autonomous mode | [a6892c3] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -107,6 +107,13 @@ Tasks 2 to 5 were done on 2026-10-14 on the branch `mil-011-fix`:
|
|||||||
- **Guides (task 4):** `guides/code-metrics.md`, `guides/upgrading.md`, the three quality-check guides, `README.md` and the wiki pages in English, Danish and Bahasa Malaysia; the amendments of ADR-0005, ADR-0008, ADR-0009 and ADR-0010. A search for `code-metrics.yml@v1` finds none.
|
- **Guides (task 4):** `guides/code-metrics.md`, `guides/upgrading.md`, the three quality-check guides, `README.md` and the wiki pages in English, Danish and Bahasa Malaysia; the amendments of ADR-0005, ADR-0008, ADR-0009 and ADR-0010. A search for `code-metrics.yml@v1` finds none.
|
||||||
- **Instance (task 5):** the workflow at `@mil-011-fix` with every input at its default except `badges-branch` ran as run 1509 (job 1649): `tools-ref` resolved to `v2`, the scripts were fetched, and the badges were committed to `badges-verify` (commit `041d991`).
|
- **Instance (task 5):** the workflow at `@mil-011-fix` with every input at its default except `badges-branch` ran as run 1509 (job 1649): `tools-ref` resolved to `v2`, the scripts were fetched, and the badges were committed to `badges-verify` (commit `041d991`).
|
||||||
|
|
||||||
|
|
||||||
|
Tasks 6 and 7 were done on 2026-10-14:
|
||||||
|
|
||||||
|
- **Release (task 6):** the Release workflow ran in dry-run mode (run 1524) and then for real (run 1525); `v2.1.1` and `v2` are `3023461`, `v1` is unchanged at `9bf0a2b` (1.2.2), and the release page has the changelog entry. A call at `@v2` with the defaults ran after the release (run 1526, job 1670): `tools-ref` resolved to `v2`, the scripts were fetched at `3023461`, and the badges were published.
|
||||||
|
- **Framework page (task 7):** `TirSystem/sqa-qc-framework` pull request 57 removed the `tools-ref` workaround from the `Badges` pages in three languages; the wiki is published.
|
||||||
|
- **Outcome:** [RC-079], Go, all eight criteria pass.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
[BC-001]: ../business-case.md
|
[BC-001]: ../business-case.md
|
||||||
@@ -115,5 +122,6 @@ Tasks 2 to 5 were done on 2026-10-14 on the branch `mil-011-fix`:
|
|||||||
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
|
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
|
||||||
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
|
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
|
||||||
[PP-001]: ../project-plan.md
|
[PP-001]: ../project-plan.md
|
||||||
|
[RC-079]: ../sqa/reviews/rc-079-mil-011-outcome.md
|
||||||
[e11c43f]: https://git.tirsystem.com/TirSystem/github-action/commit/e11c43f0a41e7dd9647f7804947d6e464407a597
|
[e11c43f]: https://git.tirsystem.com/TirSystem/github-action/commit/e11c43f0a41e7dd9647f7804947d6e464407a597
|
||||||
[a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb
|
[a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb
|
||||||
|
|||||||
@@ -0,0 +1,129 @@
|
|||||||
|
# MIL-012: VPN actions for deployment (WireGuard and Tailscale)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | MIL-012 |
|
||||||
|
| CrossReference | [BC-001], [US-001], [MIL-004], [MIL-011], [ADR-0006], [ADR-0011] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version; reviewed (RC-080), accepted by the author in chat as stand-in for S01 with target date 2026-11-06 | [c5d5941] |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 1, 6 and 9 and the fixes from the review of the first pull request; reviewed (RC-082), accepted by the author in chat as stand-in for S01 | [3325bea] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Decide whether a deployment job on a Gitea runner can reach a private network through a shared, versioned action, and release it if it can.
|
||||||
|
|
||||||
|
A deployment often has to reach hosts that are not on the internet. The GitHub marketplace has the action `tailscale/github-action` for a Tailscale network; it is written for GitHub Actions and has not been verified on Gitea Actions. WireGuard has no equivalent action at all. Two experimental composite actions for Gitea Actions already exist on the unreleased branch `wireguard-vpn-action` (`actions/connect-wireguard`, `actions/connect-tailscale`), with guides and offline tests (574 tests pass), but neither has run on a real runner, and the one question that decides whether the Tailscale action works is open: whether `tailscaled`, started in the background of one step, is still alive in the next step of the job.
|
||||||
|
|
||||||
|
This milestone runs both on a real runner of the instance, fixes what that shows, and releases them. It adds a capability, so it is a minor release ([ADR-0006]).
|
||||||
|
|
||||||
|
## Deliverable
|
||||||
|
|
||||||
|
Release `2.2.0` of this repository with two composite actions, `actions/connect-wireguard` and `actions/connect-tailscale`, each with a guide, a README section, wiki pages in English, Danish and Bahasa Malaysia, a changelog entry under `Added`, and an offline test. A record of one successful deployment-shaped job per action on the instance. An ADR for the decision to deliver a VPN as a composite action. In `TirSystem/sqa-qc-framework`: the templates `deploy-wireguard.yml` and `deploy-tailscale.yml` pinned to the release tag instead of the branch, in a pull request of their own.
|
||||||
|
|
||||||
|
## Go / No-Go Criteria
|
||||||
|
|
||||||
|
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A `uses:` of a composite action in a subfolder of this repository (`TirSystem/github-action/actions/<name>@<ref>`) resolves on the instance; the form that works (short, or the full address) is recorded | A run starts and executes the action; the form is recorded | No form resolves |
|
||||||
|
| 2 | `connect-wireguard` brings a tunnel up on a runner of the instance, a following step reaches a host behind the tunnel, and the `down` step removes the interface and the file | Recorded run; `wg show` and the file are absent afterwards | The tunnel is not up in the following step, or anything remains |
|
||||||
|
| 3 | `connect-tailscale` joins a tailnet on a runner of the instance in TUN mode and in userspace mode, a following step reaches a tailnet host, and `down` leaves no node in the tailnet | Recorded run for both modes; the node list shows no node afterwards | `tailscaled` does not survive to the next step, or a node remains |
|
||||||
|
| 4 | No secret appears in any log of the runs above (auth key, OAuth secret, WireGuard keys), checked by a search of the run logs for the secret values | Count is 0 | A secret value appears in a log |
|
||||||
|
| 5 | The runner requirements are recorded as verified: the container options, the label, and whether `wireguard-go` or the kernel module was used | Recorded in both guides | A requirement is only assumed |
|
||||||
|
| 6 | The scripts of the actions are linted (shellcheck, shfmt) and their tests run on every pull request in `validate-workflows.yml`; a seeded error fails the run | Fails on a seeded error, passes on the fix | Not part of the validation |
|
||||||
|
| 7 | The README, both guides and the wiki pages (English, Danish, Bahasa Malaysia) describe the actions at `@v2` and no longer call them experimental; a search of `guides/`, `README.md` and `docs/wiki/` finds no `@wireguard-vpn-action` | Count is 0 | A page still names the branch |
|
||||||
|
| 8 | The release is a minor (`2.2.0`): the changelog has an `Added` entry, the major tag `v2` moves to it, and `v1` is not touched | Released | Another level, or `v1` moved |
|
||||||
|
| 9 | The framework templates name the release tag, not the branch, in a pull request of `TirSystem/sqa-qc-framework` opened after release 2.2.0 | Pull request open with the tag | A template names the branch |
|
||||||
|
| 10 | Reviewed | A review record (`RC-*`) for this milestone, the actions, the tests and the guides, each with a verdict of Go | An instance without a record |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Depends on | Reason |
|
||||||
|
| --- | --- |
|
||||||
|
| [MIL-004] | The release process and the Release workflow that publish `2.2.0` |
|
||||||
|
| [MIL-011] | The default `tools-ref` and the `MAJOR_TAG` file; the release is the first after the fix |
|
||||||
|
| [ADR-0006] | A new capability is a minor release, and a moving major tag follows it |
|
||||||
|
| A runner with `NET_ADMIN`, `/dev/net/tun` and a label of its own (S03) | Both actions need a network interface; outside this repository, set up by the runner's operator |
|
||||||
|
| A WireGuard peer and a Tailscale tag with an OAuth client (S03) | Test peers for criteria 2 and 3; secrets held by the operator, never in this repository |
|
||||||
|
| `TirSystem/sqa-qc-framework`, branch `ssdf-workflow-selection-vpn` | Holds the templates and the rules that call the actions; criterion 9 changes it in its own pull request |
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
| Business Case objective / KPI / user story | Reference |
|
||||||
|
| --- | --- |
|
||||||
|
| Business Case objective O6: a shared, versioned action that brings a VPN up for a deployment job on a Gitea runner | [BC-001] |
|
||||||
|
| User stories | [US-001] (no new story: the tasks below are technical; the actions are used by deployment jobs, not by a person) |
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
| Role | Stakeholder ID (SA) |
|
||||||
|
| --- | --- |
|
||||||
|
| Owner | S05 |
|
||||||
|
| Approving reviewer | S01 |
|
||||||
|
|
||||||
|
## Target Date
|
||||||
|
|
||||||
|
2026-11-06 (Friday), proposed: two weeks of work from 2026-10-26, because the trials need a runner with extra privileges and test peers that the operator must set up. It overlaps [MIL-002] (2026-10-19 to 2026-10-31), which is documentation and does not touch the runners' network. The Business Case sets no deadline. S01 confirmed the date when accepting this milestone, in chat on 2026-10-19.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | Check that a subfolder action resolves | On a throwaway branch of a repository, call `actions/connect-wireguard` and `actions/connect-tailscale` with the short form and with the full address, at the branch. Record which form starts a run and the message when one does not. | No | |
|
||||||
|
| 2 | Prepare the trial runner | With the operator: a runner labelled `vpn` whose container has `NET_ADMIN` and `/dev/net/tun`; a WireGuard peer and a host behind it; a Tailscale tag, an OAuth client and a host in the tailnet. Secrets are stored as organization secrets and are not recorded here. | No | |
|
||||||
|
| 3 | Verify `connect-wireguard` | Run a deployment-shaped job (connect, reach the host over the tunnel, disconnect with `if: always()`) with a kernel module and with `wireguard-go`. Record the runs and what remains on the runner. Fix the script where it fails and extend the tests. | No | [MIL-012] |
|
||||||
|
| 4 | Verify `connect-tailscale` | Run the same job in TUN and in userspace mode. Decide whether `tailscaled` survives to the next step; if it does not, change how it is started (for example `setsid`, or a post step) and test it. Record the node list after the run. | No | [MIL-012] |
|
||||||
|
| 5 | Search the logs for secrets | Search every run log of tasks 3 and 4 for the secret values and for the key file path; fix any leak. | No | |
|
||||||
|
| 6 | Lint and test the action scripts in CI | Add shellcheck and shfmt for `actions/**/*.sh` and the two test files to `validate-workflows.yml`; show a seeded error fails the run. | No | [MIL-001] |
|
||||||
|
| 7 | Remove the experimental notices | Update both guides with the verified runner requirements, change `@wireguard-vpn-action` to `@v2` everywhere, and add the README section for the actions. | No | |
|
||||||
|
| 8 | Wiki pages | Add pages for the two actions in English, Danish and Bahasa Malaysia, following the wiki rules of [MIL-005], and register them in the wiki index. | No | [MIL-005] |
|
||||||
|
| 9 | Record the decision and the objective | Write an ADR: a VPN is delivered as a composite action, not as a reusable workflow (a called workflow runs as a separate job), and not through the marketplace action (written for GitHub). Objective O6, its scope item and success criterion 6 are already in [BC-001], and the schedule row, Gantt bar, scope entry, dependency and risk are in [PP-001] (added at acceptance, [RC-080], [RC-081]); this task writes the ADR only. | No | [ADR-0006] |
|
||||||
|
| 10 | Release 2.2.0 | Add the `Added` entry to the changelog, run the Release workflow (dry run first), and check that `v2` points at `v2.2.0` and `v1` is unchanged. | No | [ADR-0006] |
|
||||||
|
| 11 | Pin the framework templates | In `TirSystem/sqa-qc-framework`, change `@wireguard-vpn-action` to `@v2` in the two templates and the branch guide, remove the "experimental" notes that the release resolves, and open the pull request after release 2.2.0. This is a change in another repository, in its own pull request. | No | |
|
||||||
|
| 12 | Review | Record `RC-*` for this milestone, the actions with their tests, and the guides, each with a verdict. | No | |
|
||||||
|
|
||||||
|
|
||||||
|
## Verification results
|
||||||
|
|
||||||
|
Task 1 was run on 2026-10-19 with the throwaway branch `verify-subfolder-action`, which calls both actions in the short form (`TirSystem/github-action/actions/<name>@<ref>`) and with the full address (`https://git.tirsystem.com/TirSystem/github-action/actions/<name>@<ref>`), with every input empty, so that a resolved action fails with its own `[configuration]` message.
|
||||||
|
|
||||||
|
| Call | Result | Evidence |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Either form at `@wireguard-vpn-action` | Fails before the step: `Unable to resolve wireguard-vpn-action: reference not found`. The branch had been deleted when its pull request merged, so the ref did not exist; this is not a defect of the form. | run 417 (`2424`) |
|
||||||
|
| Short form at `@main`, both actions | Resolves and runs: the log shows `Run Pre ...@main`, the action's `mask` step and its main step, which fails with `[configuration] set config, or all of private-key, ... (WG_PRIVATE_KEY is empty)` and `[configuration] give authkey or oauth-secret` | run 418 (`2425`), jobs `wg-short`, `ts-short` |
|
||||||
|
| Full address at `@main`, both actions | The same result as the short form | run 418, jobs `wg-full`, `ts-full` |
|
||||||
|
|
||||||
|
Both forms resolve. The short form is the one the guides show; the full address stays in the guides as the fallback. `gitea.action_path` is set (the script path in the log is under `/var/run/act/actions/<id>/actions/<name>/`). A `uses:` of a branch that no longer exists fails with "reference not found", so a caller of an experimental ref breaks when its branch is deleted.
|
||||||
|
|
||||||
|
Tasks 6 and 9 were done on 2026-10-19 on the branch `mil-012-review-fixes-and-docs`:
|
||||||
|
|
||||||
|
- **Lint (task 6):** `tools/lint-action-scripts.sh` runs shellcheck and shfmt on `actions/**/*.sh`; `validate-workflows.yml` runs it with pinned tools (`shellcheck-py==0.10.0.1`, `shfmt-py==3.11.0.2`, from `tests/requirements.txt`), and `.editorconfig` sets the shell format (two spaces, indented case labels, as the shell conventions say). `tests/test_lint_action_scripts.py` seeds a shellcheck problem and a badly formatted script and shows both fail, and shows the scripts of the actions pass.
|
||||||
|
- **Decision (task 9):** [ADR-0011] (Proposed). Objective O6 and the Project Plan entries were added at the acceptance of this milestone ([RC-080], [RC-081]).
|
||||||
|
|
||||||
|
The review of pull request 194 (ten findings) is answered in the same branch: `tailscaled` is waited for before `tailscale up`; the `wireguard-go` choice survives `sudo`; `sudo` is used only when the job is not root; a `tailscaled` that already runs on the runner is not taken over and `down` leaves a daemon it did not start; the Tailscale install needs an exact `version`, with an optional `sha256`; a configuration field with a line break is refused; only the key values are masked; the key file is removed when `wg-quick` fails; and the guides call `@main` until the release. These are offline-tested with stubs; tasks 3 and 4 still have to show them on a runner.
|
||||||
|
|
||||||
|
Tasks 2, 3, 4, 5, 7, 8, 10, 11 and 12 are open: they need the trial runner (task 2), which is the operator's, or follow the verified guides (tasks 7 and 8) or the release (tasks 10 and 11).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[MIL-001]: ./mil-001-stabilise-metadata-sync.md
|
||||||
|
[MIL-002]: ./mil-002-onboarding-and-operations.md
|
||||||
|
[MIL-004]: ./mil-004-versioned-release.md
|
||||||
|
[MIL-005]: ./mil-005-wiki-and-translations.md
|
||||||
|
[MIL-011]: ./mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[MIL-012]: ./mil-012-vpn-actions-for-deployment.md
|
||||||
|
[RC-080]: ../sqa/reviews/rc-080-mil-012-plan.md
|
||||||
|
[RC-081]: ../sqa/reviews/rc-081-bc-001-o6.md
|
||||||
|
[RC-082]: ../sqa/reviews/rc-082-mil-012-results.md
|
||||||
|
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
|
||||||
|
[ADR-0011]: ../adr/adr-0011-vpn-composite-actions.md
|
||||||
|
[PP-001]: ../project-plan.md
|
||||||
|
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
|
||||||
|
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
# MIL-013: Source code comments are in English
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | MIL-013 |
|
||||||
|
| CrossReference | [BC-001], [US-001], [MIL-004], [MIL-010], [MIL-011], [ADR-0005], [ADR-0006], [ADR-0010], [ADR-0012] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Initial version; reviewed (RC-083), accepted by the author in chat as stand-in for S01 with target date 2026-11-20 | [e35b1c5] |
|
||||||
|
| 2026-10-21 | Accepted | Jens Tirsvad Nielsen | S01 | Added the results of tasks 1 to 8, 10 and 13 and the three defects found in the trial; reviewed (RC-085), accepted by the author in chat as stand-in for S01 | [88f852a] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Decide whether a pull request can be stopped automatically when a comment in its source code is not written in English, and the author told why.
|
||||||
|
|
||||||
|
TirSystem code is read by people who speak Danish, Bahasa Malaysia and English, so the comments of source code are in English only (the shared language of the repositories, and the register the project's coding conventions already ask for). Today nothing enforces it: a Danish or Malay comment is found, if at all, by a human reviewer. This milestone adds a check that finds them on the pull request, fails the check run so the pull request is blocked, and writes a message to the author with the file, the line and the reason.
|
||||||
|
|
||||||
|
The hard part is the detection, not the workflow. A comment is short, full of identifiers, and often a few words; Danish and Malay words are often close to English ones or are names. The first task therefore decides the method on a measured corpus and records it in an ADR; the rest follows from it.
|
||||||
|
|
||||||
|
## Deliverable
|
||||||
|
|
||||||
|
Release `2.3.0` of this repository ([ADR-0006]: a new capability is a minor release) with a reusable workflow `english-comments.yml` that other repositories call with `uses:`, its script files under `tools/` (comment extraction, detection, reporting), a guide, a README section, wiki pages in English, Danish and Bahasa Malaysia, a changelog entry under `Added`, and offline tests. An ADR for the detection method and its error rate. The workflow runs on the pull requests of this repository too. A review record. In the other repositories, in their own pull requests: the rule in the `coding-conventions` skill of `TirSystem/sqa-qc-framework`, and a proposed criterion for the checklist repository.
|
||||||
|
|
||||||
|
## Go / No-Go Criteria
|
||||||
|
|
||||||
|
| # | Criterion (objectively checkable) | Go | No-Go |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The detection method is chosen on a corpus recorded in the repository: at least 40 comments each in Danish and in Bahasa Malaysia, at least 40 in other languages (German, Swedish, Norwegian, French), and at least 200 English comments, among them comments with identifiers, paths and abbreviations. The ADR gives recall and false positives per language | Recall at least 90 % for Danish and Malay, and at most 1 false positive per 200 English comments | A lower rate, or no corpus |
|
||||||
|
| 2 | The check reads comments and docstrings, not code or string values, in every language of [ADR-0010], and a comment line holding only identifiers, URLs, numbers or code is not judged | Fixtures for each language pass | A string value or a code-only comment is reported |
|
||||||
|
| 3 | The workflow checks the files a pull request changes (all tracked files when asked), reports each finding as `file:line`, the language found and the text, and fails the run when there is one | A pull request with one Danish comment fails; one without passes | The run passes with a finding, or fails without one |
|
||||||
|
| 4 | An author can mark an exception on the line (a quotation, a name) and list allowed words in a file of the repository; both are logged | The marked line is not reported | No way to mark an exception |
|
||||||
|
| 5 | On failure the workflow writes one comment on the pull request that mentions the author and lists the findings; a re-run updates that comment instead of adding another, and a passing run marks it resolved | Verified on a throwaway pull request, with the Gitea mention reaching the author | Two comments, no mention, or a comment on a passing run |
|
||||||
|
| 6 | The comment is written with a token that holds the smallest scope that works; it is a CI secret and appears in no log, and no source text is sent to a service outside the instance | A search of the run logs for the token finds none; the detector needs no network | A token in a log, or an external call |
|
||||||
|
| 7 | The failing check blocks the merge: the workflow's check is listed as required, and a pull request with a finding cannot be merged. Setting branch protection is the repository owner's action; this milestone documents it and verifies it with the owner | Verified with S01 or S03 on a throwaway branch | The pull request can still be merged |
|
||||||
|
| 8 | This repository calls the workflow on its own pull requests, and passes | The caller run is green on `main` | Not called, or red |
|
||||||
|
| 9 | The guide, README and wiki pages (English, Danish, Bahasa Malaysia) describe the workflow at `@v2`, and the changelog has an `Added` entry; the release is a minor (`2.3.0`), `v2` moves to it and `v1` is not touched | Released | Another level, or `v1` moved |
|
||||||
|
| 10 | The framework rule and the checklist criterion are open as pull requests of their own repositories after release 2.3.0 | Both open | Missing |
|
||||||
|
| 11 | Reviewed | A review record (`RC-*`) for this milestone, the workflow, the scripts, the tests and the guides, each with a verdict of Go | An instance without a record |
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
| Depends on | Reason |
|
||||||
|
| --- | --- |
|
||||||
|
| [MIL-010] | The code counter that already tells comment lines from code lines for the languages of [ADR-0010]; the extractor builds on its scanner |
|
||||||
|
| [MIL-004] | The release process and the Release workflow that publish `2.3.0` |
|
||||||
|
| [MIL-011] | `MAJOR_TAG` and the `tools-ref` default that a new reusable workflow must follow |
|
||||||
|
| [ADR-0005] | The contract of a shared check: inputs with working defaults, pinned tools, a script file, a failure that names the cause |
|
||||||
|
| An organization secret with a Gitea token that may comment on pull requests (S03) | Criterion 5; created by the operator, never recorded in this repository |
|
||||||
|
| Branch protection of the repositories that adopt the check (S01 or S03) | Criterion 7; changing it is the owner's decision, not made by the workflow |
|
||||||
|
| `TirSystem/sqa-qc-framework` and `TirSystem/SQA-QC-Checklists` | Task 12 changes them in their own pull requests, after the release |
|
||||||
|
|
||||||
|
## Traceability
|
||||||
|
|
||||||
|
| Business Case objective / KPI / user story | Reference |
|
||||||
|
| --- | --- |
|
||||||
|
| Business Case objective O7: a shared workflow that checks that the comments in source code are written in English, blocks the pull request when they are not, and tells the author | [BC-001] |
|
||||||
|
| User stories | [US-001] (no new story: the tasks below are technical; the check is used by the authors of pull requests) |
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
| Role | Stakeholder ID (SA) |
|
||||||
|
| --- | --- |
|
||||||
|
| Owner | S05 |
|
||||||
|
| Approving reviewer | S01 |
|
||||||
|
|
||||||
|
## Target Date
|
||||||
|
|
||||||
|
2026-11-20 (Friday), proposed: two weeks from 2026-11-09, after [MIL-012] (2026-11-06) so that the two milestones do not compete for the one runner and the one reviewer. The Business Case sets no deadline. S01 confirmed the date when accepting this milestone, in chat on 2026-10-19.
|
||||||
|
|
||||||
|
## Tasks
|
||||||
|
|
||||||
|
| # | Task | Summary | Needs its own Use Case/User Story? | Reference |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | Build the corpus | Collect and label the comments of criterion 1 in `tests/fixtures/` (real comments from the TirSystem repositories where they exist, written ones otherwise), with the language of each. English comments come from this repository and others that are known to be English. Record the source of each part. | No | |
|
||||||
|
| 2 | Choose the detection method (ADR) | Compare at least two methods on the corpus: a rule-based one (letters outside English, word lists for Danish and Malay) and a language-identification library restricted to the candidate languages, run offline. Measure recall and false positives per language, the install size and the time on a repository of 1 000 files. Write the ADR with the numbers and the choice. | No | [ADR-0005] |
|
||||||
|
| 3 | Extract comments | Add a script that returns the text, file and line of every comment and docstring for the languages of [ADR-0010], reusing the scanner of `tools/count_loc.py` (it classifies lines today but does not return the text). Skip string values, code and comment lines that hold only identifiers, URLs, numbers or code. | No | [MIL-010] |
|
||||||
|
| 4 | Detect the language | Implement the method of task 2 as a script with a fixed interface (comment in, language and confidence out), the exception marker on the line and the allowed-words file of criterion 4. | No | |
|
||||||
|
| 5 | Report and tell the author | A script that prints each finding as `::error file=...,line=...` and `file:line`, fails the run, and writes or updates one pull request comment that mentions the author, through the Gitea API with the smallest token scope; a passing run marks the comment resolved. A fake API in the tests. | No | |
|
||||||
|
| 6 | Write the reusable workflow | `english-comments.yml` with `workflow_call`, inputs with working defaults (changed files only, languages, allowed-words file, exclude), a pinned `tools-ref` that follows `MAJOR_TAG`, the token as a secret, and explicit job permissions. The `run:` lines only call the script files. | No | [ADR-0005] |
|
||||||
|
| 7 | Offline tests | Tests for the extractor, the detector against the corpus (the rates of criterion 1 as a test that fails below them), the reporting against the fake API, and the workflow definition; the coverage limit of `tools/` holds. | No | |
|
||||||
|
| 8 | Trial on the instance | On a throwaway pull request with one Danish comment, one Malay comment and one marked exception: record the failing run, the comment with the mention, the update on a re-run and the green run after the fix. Search the logs for the token. | No | |
|
||||||
|
| 9 | Block the merge | With S01 or S03, mark the check as required for `main` of this repository and show that a pull request with a finding cannot be merged. Document the setting for other repositories. This task changes branch protection, so it is done by the owner, not by the AI. | No | |
|
||||||
|
| 10 | Guides, README and wiki | Guide, README section, and wiki pages in English, Danish and Bahasa Malaysia, following [MIL-005]; the sidebar. | No | [MIL-005] |
|
||||||
|
| 11 | Call it in this repository, then release | A caller workflow for the pull requests of this repository; the changelog entry; the Release workflow (dry run first); check that `v2` points at `v2.3.0` and `v1` is unchanged. | No | [ADR-0006] |
|
||||||
|
| 12 | The rule in the framework and the checklists | In `TirSystem/sqa-qc-framework`, add the English-comments rule to the `coding-conventions` skill and the call of the workflow to its CI template; in `TirSystem/SQA-QC-Checklists`, propose a criterion. Each in its own pull request, after release 2.3.0. | No | |
|
||||||
|
| 13 | Add MIL-013 to the Business Case and the Project Plan | Objective O7, the scope item and a success criterion in [BC-001]; the schedule row, Gantt bar, scope entry, dependency and a plan risk (false positives on short comments) in [PP-001]; the traceability rows. | No | |
|
||||||
|
| 14 | Review | Record `RC-*` for this milestone, the workflow, the scripts, the tests and the guides, each with a verdict. | No | |
|
||||||
|
|
||||||
|
|
||||||
|
## Verification results
|
||||||
|
|
||||||
|
**Tasks 1 to 7, 10 and 13** were done on 2026-10-19 to 2026-10-21 and merged in pull request 211 (and 196 for task 13): the corpus (300 English, 45 Danish, 45 Malay, 48 other), [ADR-0012] (`Proposed`, not yet reviewed), the extractor and detector, the report script, the reusable workflow `english-comments.yml`, 673 offline tests with 98 % coverage of `tools/`, and the guide, README and wiki in three languages. The detector flags 0 of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments and all 48 of the others; the Danish and Malay comments were written for the test and not checked by a native reader.
|
||||||
|
|
||||||
|
**Task 8 (trial on the instance)** was run on 2026-10-21 with the throwaway branch `verify-english-comments` and its pull request 213 (closed, not merged). The sample file holds an English comment, a Danish comment directly under it, a Malay comment, and a Danish comment marked `english-comments: ignore`.
|
||||||
|
|
||||||
|
| Run | What was called | Result |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 433 (id 2478), first call at `@main` | the workflow of pull request 211 | Fetched the shared scripts, installed `langid` and `numpy` on the runner, found only the Malay comment (line 9, `id` 0.66); the Danish line next to the English one was **missed** (defect 1). The step "Tell the author" was **skipped**, so no comment was written (defect 2). The run failed. |
|
||||||
|
| 435 and 437 (ids 2480, 2482), a debug workflow | the contexts of a called workflow | `gitea.event_name` is `workflow_call`; `gitea.event.pull_request.number` (213), the author login and `gitea.base_ref` (`main`) are set. The conditions `always()`, `always() && ...pull_request.number != ''`, `... && ...pull_request.number`, `... && ...pull_request` and `... && gitea.base_ref != ''` ran the step; `gitea.event_name == 'pull_request'` and `github.event_name == 'pull_request'` did not. |
|
||||||
|
| 440 (id 2485), the fix branch | `english-comments.yml@mil-013-judge-each-line` | Both the Danish line (6, `da` 1.0) and the Malay line (9) were found, the marked line was not reported, the run failed, and the Actions bot (`gitea-actions`) wrote **one** comment (id 8715) that mentions `@Tirsvad` and lists the two findings. |
|
||||||
|
| 443 (id 2488) | the same, with another Danish comment | "Updated the comment of the check": still one comment (id 8715), with the new text. |
|
||||||
|
| 445 (id 2490) | both comments rewritten in English | The run passed, the failing step was skipped, and the comment was marked resolved (a "passes" text, no mention). |
|
||||||
|
| 447 (id 2492) | a Malay comment again | The comment updated; its link `.../actions/runs/2492` equals the `html_url` of the run (defect 3 fixed: the first version linked the run number 440). |
|
||||||
|
|
||||||
|
The three defects are fixed in pull request 212 (open at the time of writing): (1) a block of several lines is judged line by line first (at least four words, probability 0.7), then as a whole; (2) the author step asks for `gitea.event.pull_request.number != ''` instead of the event name; (3) the comment links the run by its id. Each has a test.
|
||||||
|
|
||||||
|
**Secrets:** the four `english-comments` jobs of runs 440, 443, 445 and 447 were searched for `token <20 or more characters>`, `basic <base64>`, `bearer` and for the value of the token used from this machine; nothing was found. `REPORT_TOKEN` is printed as `***` and the clone header as `AUTHORIZATION: basic ***`. The detector made no network call; the only calls were to the Gitea API of the instance.
|
||||||
|
|
||||||
|
**Not shown by the trial:** a call at `@main` of the corrected workflow (it follows the merge of pull request 212); a pull request from a fork (read-only token); a custom `REPORT_TOKEN` secret; the check as a required status that blocks the merge (task 9); and the call of the workflow by this repository (task 11). Tasks 9, 11, 12 and 14 are open.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../business-case.md
|
||||||
|
[US-001]: ../user-stories.md
|
||||||
|
[MIL-004]: ./mil-004-versioned-release.md
|
||||||
|
[MIL-005]: ./mil-005-wiki-and-translations.md
|
||||||
|
[MIL-010]: ./mil-010-code-metrics-badges.md
|
||||||
|
[MIL-011]: ./mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[MIL-012]: ./mil-012-vpn-actions-for-deployment.md
|
||||||
|
[MIL-013]: ./mil-013-english-comments-check.md
|
||||||
|
[ADR-0005]: ../adr/adr-0005-quality-check-contract.md
|
||||||
|
[ADR-0006]: ../adr/adr-0006-release-process-and-version-pinning.md
|
||||||
|
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
|
||||||
|
[PP-001]: ../project-plan.md
|
||||||
|
[RC-083]: ../sqa/reviews/rc-083-mil-013-plan.md
|
||||||
|
[RC-084]: ../sqa/reviews/rc-084-bc-001-o7.md
|
||||||
|
[RC-085]: ../sqa/reviews/rc-085-mil-013-results.md
|
||||||
|
[ADR-0012]: ../adr/adr-0012-english-comment-detection.md
|
||||||
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
|
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
|
||||||
+22
-8
@@ -4,25 +4,27 @@
|
|||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | PP-001 |
|
| ID | PP-001 |
|
||||||
| CrossReference | [BC-001], [SA-001], [ADR-0002], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011] |
|
| CrossReference | [BC-001], [SA-001], [ADR-0002], [US-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013] |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Deprecated | Jens Tirsvad Nielsen | AI as stand-in for S01 | Added MIL-010 (code metrics and coverage badges); reviewed (RC-068) | [5002450] |
|
| 2026-10-19 | Deprecated | Jens Tirsvad Nielsen | S01 | Added MIL-012 (VPN actions for deployment); reviewed (RC-080), accepted by the author in chat as stand-in for S01 | [c5d5941] |
|
||||||
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | S01 | Added MIL-011 (fix the defaults of the code metrics workflow); reviewed (RC-076), accepted by the author in chat as stand-in for S01 | [02e3794] |
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Added MIL-013 (source code comments are in English); reviewed (RC-083), accepted by the author in chat as stand-in for S01 | [e35b1c5] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
This plan schedules eleven milestones that move the repository from one
|
This plan schedules thirteen milestones that move the repository from one
|
||||||
workflow with known defects to a documented, versioned collection of shared
|
workflow with known defects to a documented, versioned collection of shared
|
||||||
workflows. The Business Case sets no deadline, so the dates are the plan's own; they were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
|
workflows. The Business Case sets no deadline, so the dates are the plan's own; they were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
|
||||||
|
|
||||||
## Planning Assumptions
|
## Planning Assumptions
|
||||||
|
|
||||||
- Week 1 starts 2026-10-05; phases are two weeks long; the last decision is 2026-12-26; MIL-007 follows in January 2027 (2027-01-04 to 2027-01-16, confirmed on 2026-10-11 by S01). MIL-008 amends the shared wiki publishing of MIL-006 and runs in parallel with the October and November windows: its work started on 2026-10-13 and its decision date is its own Target Date, 2026-11-15. MIL-009 follows MIL-008 and runs from 2026-11-16 to its proposed Target Date, 2026-11-30, confirmed by S01 in chat on 2026-10-14. MIL-010 (code metrics and coverage badges) follows MIL-007 in January 2027, from 2027-01-18 to its Target Date, 2027-01-30, accepted by the user in chat on 2026-10-14.
|
- Week 1 starts 2026-10-05; phases are two weeks long; the last decision is 2026-12-26; MIL-007 follows in January 2027 (2027-01-04 to 2027-01-16, confirmed on 2026-10-11 by S01). MIL-008 amends the shared wiki publishing of MIL-006 and runs in parallel with the October and November windows: its work started on 2026-10-13 and its decision date is its own Target Date, 2026-11-15. MIL-009 follows MIL-008 and runs from 2026-11-16 to its proposed Target Date, 2026-11-30, confirmed by S01 in chat on 2026-10-14. MIL-010 (code metrics and coverage badges) follows MIL-007 in January 2027, from 2027-01-18 to its Target Date, 2027-01-30, accepted by the user in chat on 2026-10-14.
|
||||||
|
- MIL-012 (VPN actions for deployment) runs from 2026-10-26 to its Target Date, 2026-11-06, proposed by the author and accepted by S01 in chat on 2026-10-19; it overlaps MIL-002 and MIL-003, which do not touch the runners' network.
|
||||||
|
- MIL-013 (source code comments are in English) runs from 2026-11-09 to its Target Date, 2026-11-20, proposed by the author and accepted by S01 in chat on 2026-10-19; it overlaps MIL-009, whose work is on wiki callers and does not touch the runners or the reviewer of the check.
|
||||||
- Work on a milestone starts only after its `MIL-*` document is `Accepted`.
|
- Work on a milestone starts only after its `MIL-*` document is `Accepted`.
|
||||||
- Dates were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
|
- Dates were confirmed on 2026-10-10 by S01, standing in for S05 (ADR-0002).
|
||||||
|
|
||||||
@@ -41,6 +43,8 @@ workflows. The Business Case sets no deadline, so the dates are the plan's own;
|
|||||||
| Migrate the wiki callers and remove the scoped workflow | [MIL-009] | 2026-11-16 to 2026-11-30 | 2026-11-30 | S05 | None yet | Seven repositories publish through a caller, scoped workflow removed, ADR-0004 superseded, released as 2.0.0 | Not created yet (the sync creates it when MIL-009 is Accepted) |
|
| Migrate the wiki callers and remove the scoped workflow | [MIL-009] | 2026-11-16 to 2026-11-30 | 2026-11-30 | S05 | None yet | Seven repositories publish through a caller, scoped workflow removed, ADR-0004 superseded, released as 2.0.0 | Not created yet (the sync creates it when MIL-009 is Accepted) |
|
||||||
| Code metrics and coverage badges | [MIL-010] | 2027-01-18 to 2027-01-30 | 2027-01-30 | S05 | None yet (task 1 writes them) | Reusable workflow that counts code files and lines of code, reads coverage and commits SVG badges, released | Not created yet (the sync creates it when MIL-010 is Accepted) |
|
| Code metrics and coverage badges | [MIL-010] | 2027-01-18 to 2027-01-30 | 2027-01-30 | S05 | None yet (task 1 writes them) | Reusable workflow that counts code files and lines of code, reads coverage and commits SVG badges, released | Not created yet (the sync creates it when MIL-010 is Accepted) |
|
||||||
| Fix the defaults of the code metrics workflow | [MIL-011] | 2026-10-14 to 2026-10-23 | 2026-10-23 | S05 | None (technical tasks) | Patch release 2.1.1: the default `tools-ref` of every reusable workflow names a tag that holds its scripts, a test for it, guides and wiki at `@v2`, the framework Badges page without the workaround | Not created yet (the sync creates it when MIL-011 is Accepted) |
|
| Fix the defaults of the code metrics workflow | [MIL-011] | 2026-10-14 to 2026-10-23 | 2026-10-23 | S05 | None (technical tasks) | Patch release 2.1.1: the default `tools-ref` of every reusable workflow names a tag that holds its scripts, a test for it, guides and wiki at `@v2`, the framework Badges page without the workaround | Not created yet (the sync creates it when MIL-011 is Accepted) |
|
||||||
|
| VPN actions for deployment | [MIL-012] | 2026-10-26 to 2026-11-06 | 2026-11-06 | S05 | None (technical tasks) | Release 2.2.0: composite actions `connect-wireguard` and `connect-tailscale` verified on a runner of the instance, with guides, wiki in three languages and the framework templates pinned to the release | Not created yet (the sync creates it when MIL-012 is Accepted) |
|
||||||
|
| Source code comments are in English | [MIL-013] | 2026-11-09 to 2026-11-20 | 2026-11-20 | S05 | None (technical tasks) | Release 2.3.0: reusable workflow `english-comments.yml` that fails a pull request with a non-English comment and tells the author, with its ADR, tests, guides and wiki, and the rule proposed in the framework | Not created yet (the sync creates it when MIL-013 is Accepted) |
|
||||||
|
|
||||||
```plantuml
|
```plantuml
|
||||||
@startgantt
|
@startgantt
|
||||||
@@ -67,6 +71,10 @@ Project starts 2026-10-05
|
|||||||
[MIL-010 Go/No-Go] happens 2027-01-30
|
[MIL-010 Go/No-Go] happens 2027-01-30
|
||||||
[MIL-011 Defaults fix] starts 2026-10-14 and ends 2026-10-23
|
[MIL-011 Defaults fix] starts 2026-10-14 and ends 2026-10-23
|
||||||
[MIL-011 Go/No-Go] happens 2026-10-23
|
[MIL-011 Go/No-Go] happens 2026-10-23
|
||||||
|
[MIL-012 VPN actions] starts 2026-10-26 and ends 2026-11-06
|
||||||
|
[MIL-012 Go/No-Go] happens 2026-11-06
|
||||||
|
[MIL-013 English comments] starts 2026-11-09 and ends 2026-11-20
|
||||||
|
[MIL-013 Go/No-Go] happens 2026-11-20
|
||||||
@endgantt
|
@endgantt
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -84,6 +92,8 @@ Project starts 2026-10-05
|
|||||||
| The wiki publisher as a workflow that repositories call, from docs/wiki | [MIL-008] |
|
| The wiki publisher as a workflow that repositories call, from docs/wiki | [MIL-008] |
|
||||||
| Moving every publishing repository to the called workflow and removing the scoped one | [MIL-009] |
|
| Moving every publishing repository to the called workflow and removing the scoped one | [MIL-009] |
|
||||||
| A shared workflow for code metrics and coverage badges | [MIL-010], [MIL-011] |
|
| A shared workflow for code metrics and coverage badges | [MIL-010], [MIL-011] |
|
||||||
|
| Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale | [MIL-012] |
|
||||||
|
| A shared workflow that checks that comments in source code are in English and tells the author | [MIL-013] |
|
||||||
|
|
||||||
## Dependencies
|
## Dependencies
|
||||||
|
|
||||||
@@ -93,6 +103,8 @@ MIL-001 → MIL-002 → MIL-003 → MIL-004
|
|||||||
MIL-003 + MIL-004 → MIL-007
|
MIL-003 + MIL-004 → MIL-007
|
||||||
MIL-006 → MIL-008 → MIL-009
|
MIL-006 → MIL-008 → MIL-009
|
||||||
MIL-003 + MIL-004 → MIL-010 → MIL-011
|
MIL-003 + MIL-004 → MIL-010 → MIL-011
|
||||||
|
MIL-004 + MIL-011 → MIL-012
|
||||||
|
MIL-004 + MIL-010 + MIL-011 → MIL-013
|
||||||
```
|
```
|
||||||
|
|
||||||
A No-Go moves every later window by the rework time of the failed milestone.
|
A No-Go moves every later window by the rework time of the failed milestone.
|
||||||
@@ -106,6 +118,8 @@ A No-Go moves every later window by the rework time of the failed milestone.
|
|||||||
| Eight repositories still publish through the scoped workflow that MIL-008 deprecates (MIL-008 task 5) | Removing it would stop their wikis | It is deprecated and kept; a follow-up milestone migrates them and removes it in a major release (RC-061). |
|
| Eight repositories still publish through the scoped workflow that MIL-008 deprecates (MIL-008 task 5) | Removing it would stop their wikis | It is deprecated and kept; a follow-up milestone migrates them and removes it in a major release (RC-061). |
|
||||||
| An organisation has no `TOKEN_FOR_GITEA_WIKI` secret, or a repository fails its first run (MIL-009 tasks 3 and 4) | The wiki of that repository stops updating | MIL-009 task 5 reads each wiki back after the first run; the scoped workflow is removed only after every repository passes. |
|
| An organisation has no `TOKEN_FOR_GITEA_WIKI` secret, or a repository fails its first run (MIL-009 tasks 3 and 4) | The wiki of that repository stops updating | MIL-009 task 5 reads each wiki back after the first run; the scoped workflow is removed only after every repository passes. |
|
||||||
| The token of a run cannot push to a `badges` branch, or the SVG does not render on the mirror (MIL-010) | The delivery of the badges changes shape | MIL-010 task 3 verifies it first; fall back to a badge file in a path of the default branch or to a run artifact. |
|
| The token of a run cannot push to a `badges` branch, or the SVG does not render on the mirror (MIL-010) | The delivery of the badges changes shape | MIL-010 task 3 verifies it first; fall back to a badge file in a path of the default branch or to a run artifact. |
|
||||||
|
| The runner cannot give a job `NET_ADMIN` and `/dev/net/tun`, or `tailscaled` does not survive to the next step (MIL-012) | The actions need another shape (a different start of the daemon, or userspace mode only) | MIL-012 tasks 2 to 4 verify it first on a runner of the instance; the release waits for criteria 2 and 3. |
|
||||||
|
| The check calls a correct comment non-English, or misses a Danish or Malay one (MIL-013) | Authors lose trust in the check, or it is turned off | MIL-013 tasks 1 and 2 measure the method on a corpus before anything is built, and the exception marker and the allowed-words file (criterion 4) take care of the rest. |
|
||||||
| Security review by S05 is a bottleneck | Milestone decisions slip | Keep milestones small and plan review time before each decision date. |
|
| Security review by S05 is a bottleneck | Milestone decisions slip | Keep milestones small and plan review time before each decision date. |
|
||||||
|
|
||||||
## Open Issues
|
## Open Issues
|
||||||
@@ -282,6 +296,8 @@ no tests, no release process.
|
|||||||
[MIL-009]: ./milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
|
[MIL-009]: ./milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
|
||||||
[MIL-010]: ./milestones/mil-010-code-metrics-badges.md
|
[MIL-010]: ./milestones/mil-010-code-metrics-badges.md
|
||||||
[MIL-011]: ./milestones/mil-011-code-metrics-default-tools-ref.md
|
[MIL-011]: ./milestones/mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[MIL-012]: ./milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[MIL-013]: ./milestones/mil-013-english-comments-check.md
|
||||||
[Milestone MIL-001]: https://git.tirsystem.com/TirSystem/github-action/milestone/18
|
[Milestone MIL-001]: https://git.tirsystem.com/TirSystem/github-action/milestone/18
|
||||||
[Milestone MIL-002]: https://git.tirsystem.com/TirSystem/github-action/milestone/19
|
[Milestone MIL-002]: https://git.tirsystem.com/TirSystem/github-action/milestone/19
|
||||||
[Milestone MIL-003]: https://git.tirsystem.com/TirSystem/github-action/milestone/20
|
[Milestone MIL-003]: https://git.tirsystem.com/TirSystem/github-action/milestone/20
|
||||||
@@ -290,7 +306,5 @@ no tests, no release process.
|
|||||||
[Milestone MIL-006]: https://git.tirsystem.com/TirSystem/github-action/milestone/94
|
[Milestone MIL-006]: https://git.tirsystem.com/TirSystem/github-action/milestone/94
|
||||||
[Milestone MIL-007]: https://git.tirsystem.com/TirSystem/github-action/milestone/95
|
[Milestone MIL-007]: https://git.tirsystem.com/TirSystem/github-action/milestone/95
|
||||||
[Milestone MIL-008]: https://git.tirsystem.com/TirSystem/github-action/milestone/152
|
[Milestone MIL-008]: https://git.tirsystem.com/TirSystem/github-action/milestone/152
|
||||||
[b74ee0e]: https://git.tirsystem.com/TirSystem/github-action/commit/b74ee0e5cfd04f25ba5f2c1addc80699147c479a
|
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
|
||||||
[2f267df]: https://git.tirsystem.com/TirSystem/github-action/commit/2f267df2a270087cb7dc482af4ce4f4341e97b03
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
[5002450]: https://git.tirsystem.com/TirSystem/github-action/commit/50024505fe788b1c5e3021c0d1fc3d079590070e
|
|
||||||
[02e3794]: https://git.tirsystem.com/TirSystem/github-action/commit/02e3794d7b05799af2544ca303c99271da788e82
|
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Review Record: MIL-011 outcome (release 2.1.1)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-079 |
|
||||||
|
| CrossReference | [MIL-011], [RC-078] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-14 | Accepted | Jens Tirsvad Nielsen | AI as stand-in for S01 | Initial version; Go; accepted by the AI as stand-in for S01 in autonomous mode, not independent | [4aa3fd3] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: the outcome of [MIL-011]: the release `v2.1.1` of this repository (tag on `3023461`, the release page, the moving major tag `v2`) and the framework `Badges` page (`TirSystem/sqa-qc-framework` pull request 57)
|
||||||
|
- Checklist used: the eight Go/No-Go criteria of [MIL-011]; criteria 1 to 5 were scored in [RC-078] and are not repeated
|
||||||
|
- Scope: re-score of criteria 6, 7 and 8, after the release
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Mode: autonomous, not independent (the reviewer is `AI as stand-in for S01`)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 to 5 | See [RC-078] | Pass | Reproduced (run 1499), every default checked, the default derived from `MAJOR_TAG` with a failing seeded test, no `@v1` for `code-metrics.yml`, a run on the instance (run 1509). |
|
||||||
|
| 6 | The release is a patch (2.1.1); `v2` moves to it, `v1` is untouched | Pass | The Release workflow ran in dry-run mode (run 1524: "Checks passed for v2.1.1 on 3023461 ... would create the release page v2.1.1 and point v2 to it") and then for real (run 1525). `git ls-remote`: `v2.1.1` and `v2` (peeled) are `3023461`; `v1` is `1.2.2` (`9bf0a2b`), unchanged. The release page has the changelog entry as its text. After the release a call at `@v2` with the defaults ran on the instance (run 1526, job 1670): `tools-ref: v2`, the scripts fetched at `3023461`, badges published to `badges-verify`. |
|
||||||
|
| 7 | The framework `Badges` page no longer tells a caller to set `tools-ref` | Pass | `TirSystem/sqa-qc-framework` pull request 57 is merged and the wiki is published: the three pages have no `tools-ref: v2` line and no bullet about it, call the workflow at `@v2`, and say to use 2.1.1 or later, not 2.1.0. |
|
||||||
|
| 8 | Reviewed | Pass | [RC-076], [RC-077], [RC-078] and this record, each with a verdict of Go; [MIL-011] and its plan row are `Accepted`. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - all eight criteria pass. The defect is fixed in release 2.1.1: the default `tools-ref` of the five reusable workflows is the current major (`MAJOR_TAG`), a test keeps it so, and a call at `@v2` with the defaults runs. Two things stay as they are and are reported, not decided here: release `2.1.0` still has the defect (a release tag is never moved, ADR-0006) and callers are told to use 2.1.1 or later; and the other four workflows now fetch their scripts from the current major instead of `1.2.2`, which changes no behaviour for a caller at `@v1`. The AI accepts the record as stand-in for S01 in autonomous mode; there is no independent reader and the decision stays open to challenge in the run summary.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Delete the throwaway branch `verify-code-metrics-defaults` when the evidence is no longer needed (a branch deletion is left to a person) | S05 | 2026-10-23 |
|
||||||
|
| Decide whether the required status check `Sync GitHub mirror metadata / sync-metadata (push)` should stay required on pull requests, where it is only `skipped` and blocks a merge through the API | S05 | 2026-10-30 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[RC-078]: ./rc-078-mil-011-fix.md
|
||||||
|
[RC-076]: ./rc-076-mil-011-plan.md
|
||||||
|
[RC-077]: ./rc-077-mil-011-task-1-results.md
|
||||||
|
[4aa3fd3]: https://git.tirsystem.com/TirSystem/github-action/commit/4aa3fd38f97c26a5d52abc566e958759583ccb85
|
||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Review Record: MIL-012 and PP-001 (MIL-012)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-080 |
|
||||||
|
| CrossReference | [MIL-012], [PP-001], [BC-001], [QC-MIL-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01, after three corrections found in the first read | [c5d5941] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-012]; delta of [PP-001] for the added milestone
|
||||||
|
- Checklist used: [QC-MIL-001] for the milestone; for the plan, the criteria of `PP.md` (no QC checklist exists for PP-001)
|
||||||
|
- Scope: full review of MIL-012; delta re-review of PP-001, earlier record [RC-076]
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | `## Deliverable` names release 2.2.0 with the two composite actions, their guides, README section, wiki pages in three languages, changelog entry and test; a record of one successful job per action; an ADR; and the framework templates pinned to the release tag. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Ten criteria, each with a checkable Go and No-Go: the `uses:` form that resolves, a recorded run per action with the cleanup shown, no secret in any log (a search with a count), verified runner requirements, linting with a seeded error, a search that finds no branch reference, the release level and tags, the framework pull request, the review record. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | [MIL-004] (the Release workflow) and [MIL-011] (`MAJOR_TAG`), [ADR-0006], and the two outside dependencies on S03 (runner, test peers) and on the branch of `TirSystem/sqa-qc-framework`; in the plan's dependency diagram (`MIL-004 + MIL-011 -> MIL-012`). The first read found the operator named S05; the Business Case and the Stakeholder Analysis give runners and secrets to S03, now corrected. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objective O6 and success criterion 6 of [BC-001], added in the same change and reviewed in [RC-081]. The first read found the milestone calling O6 "proposed in task 9", which would have left it untraceable until later; now the objective exists and task 9 writes the ADR only. |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S05, approving reviewer S01; both are in [SA-001]. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-06, confirmed by S01 in chat on 2026-10-19; the Business Case sets no deadline. The window (2026-10-26 to 2026-11-06) overlaps [MIL-002] and [MIL-003], which are documentation and quality checks and do not use the runner's network; the plan says so. |
|
||||||
|
|
||||||
|
## Plan delta (PP-001)
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Every milestone has a `MIL-*` document and the plan's dates equal that document's target date | Pass | MIL-001 to MIL-012 exist; the MIL-012 decision date (2026-11-06) equals its Target Date. |
|
||||||
|
| 2 | Every Business Case scope item is covered by a gateway | Pass | The new scope item "Shared composite actions that connect a deployment job to a private network" is covered by [MIL-012] in the scope table. |
|
||||||
|
| 3 | The schedule respects the Business Case constraints | Pass | No deadline in the Business Case; the window is two weeks. |
|
||||||
|
| 4 | Dependencies between milestones are stated | Pass | `MIL-004 + MIL-011 -> MIL-012`. |
|
||||||
|
| 5 | Every gateway has an owner who is a stakeholder ID | Pass | S05. |
|
||||||
|
| 6 | Plan content is internally consistent | Pass | The first read found a broken link target in the milestone (a wrong ADR-0006 file name and two milestone file names), now checked by resolving every link of the file. The Purpose says "twelve", and the Gantt, the schedule, the scope table and the dependencies agree. |
|
||||||
|
| 7 | Open Issues are current | Pass | A plan risk is added: the runner cannot give `NET_ADMIN` and `/dev/net/tun`, or `tailscaled` does not survive to the next step, with the mitigation in tasks 2 to 4. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - all criteria pass after three corrections found in the first read (the operator S05 where the Business Case says S03, the objective O6 left as a proposal, and three wrong link targets). The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
|
||||||
|
| Task 2: ask the operator for the `vpn` runner and the test peers before task 3 starts; nothing in this milestone creates them | S03 | 2026-10-30 |
|
||||||
|
| The code of both actions, their tests and guides already exist on the unreleased branch `wireguard-vpn-action`; commit it with `Task: MIL-012#N` trailers (tasks 3, 4, 6 and 7 own it) only after the milestone is published as issues | S05 | Before the first commit on that branch |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[PP-001]: ../../project-plan.md
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[SA-001]: ../../stakeholder-analysis.md
|
||||||
|
[MIL-002]: ../../milestones/mil-002-onboarding-and-operations.md
|
||||||
|
[MIL-003]: ../../milestones/mil-003-quality-checks.md
|
||||||
|
[MIL-004]: ../../milestones/mil-004-versioned-release.md
|
||||||
|
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[ADR-0006]: ../../adr/adr-0006-release-process-and-version-pinning.md
|
||||||
|
[RC-076]: ./rc-076-mil-011-plan.md
|
||||||
|
[RC-081]: ./rc-081-bc-001-o6.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
|
||||||
@@ -0,0 +1,53 @@
|
|||||||
|
# Review Record: BC-001 (objective O6, VPN actions for deployment)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-081 |
|
||||||
|
| CrossReference | [BC-001], [QC-BC-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [c5d5941] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [BC-001]
|
||||||
|
- Checklist used: [QC-BC-001]
|
||||||
|
- Scope: delta re-review of criteria 2, 3, 4 and 7 (the change adds objective O6, an In Scope bullet, an assumption, a constraint and success criterion 6); criteria 1, 5, 6 and 8 are re-read and unchanged. Earlier record: [RC-067]
|
||||||
|
- Language and domain: n/a for this delta (the action item of [RC-067] about the Language and Domain rows is still open)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | Unchanged; the Cost-Benefit Assessment labels its qualitative form. |
|
||||||
|
| 2 | Risks are identified with documented impact and mitigation | Pass | The delivery risk of the VPN actions (the runner cannot give the privileges, or the daemon does not survive between steps) is a plan risk in PP-001 with a mitigation; the Business Case risks about secrets already cover a leaked key. |
|
||||||
|
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | Criterion 6: "2 (WireGuard and Tailscale) by MIL-012", measured by the recorded runs of tasks 3 to 5, with no secret in the run log. |
|
||||||
|
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | "Shared composite actions that connect a deployment job to a private network through WireGuard or Tailscale (MIL-012)" is a new In Scope bullet. Out of Scope is unchanged and consistent: operating runners stays out, and the new assumption says S03 provides the privileged runner. |
|
||||||
|
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | Unchanged; the new assumption names S03. |
|
||||||
|
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Unchanged. |
|
||||||
|
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | The assumption (S03 provides a runner with `NET_ADMIN` and `/dev/net/tun`) and the constraint (a VPN key or token never appears in a log, an artifact or a command line) are in their own sections. |
|
||||||
|
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Unchanged: Proceed. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - all eight criteria pass for the revision that adds objective O6, its scope item, assumption, constraint and success criterion 6. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; there is no independent reader and the decision stays open to challenge.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| The Metadata table of BC-001 has no `Language` and `Domain` rows (QC-LANG-001 criterion 1); this predates this change and is carried over from [RC-067]. Add the rows or record in an ADR why they are not needed | S01 | Before the next material change to BC-001 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
|
||||||
|
[RC-067]: ./rc-067-bc-001-o5.md
|
||||||
|
[c5d5941]: https://git.tirsystem.com/TirSystem/github-action/commit/c5d59418698316866807388bf61dcb6166d9fa8b
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
# Review Record: MIL-012 results of tasks 1, 6 and the review fixes (MIL-012)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-082 |
|
||||||
|
| CrossReference | [MIL-012], [QC-MIL-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [3325bea] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-012]
|
||||||
|
- Checklist used: [QC-MIL-001]
|
||||||
|
- Scope: delta re-review of the milestone after [RC-080]: the new `## Verification results` section (task 1, task 6, the fixes made after the review of pull request 194), the added `[ADR-0011]` in the CrossReference, and the claims about open tasks. Criteria 1 to 6 are re-read; none of the milestone's own text for them changed. [ADR-0011] is **not** reviewed here: it stays `Proposed` and needs a review against its own checklist
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | Unchanged: release 2.2.0 with the two actions, guides, wiki pages in three languages, ADR, framework templates pinned. The results section does not claim any of it is delivered; it lists tasks 2 to 5, 7, 8 and 10 to 12 as open. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Unchanged, ten criteria. The results bear on two of them. Criterion 1 (a `uses:` of a subfolder action resolves, the form recorded): runs 417 and 418 show both forms resolve at `@main`, and the failure at the deleted branch is explained by the log text "reference not found". Criterion 6 (scripts linted in CI, a seeded error fails): met by `tools/lint-action-scripts.sh` and its tests on the working branch; the pull-request run of the new step is the final evidence. The other eight criteria are not claimed. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged; [ADR-0011] is added to the CrossReference and exists. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Unchanged: objective O6 and success criterion 6 of [BC-001], reviewed in [RC-081]. |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: owner S05, approving reviewer S01. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2026-11-06. The open tasks 2 to 5 need the operator's runner (S03); the milestone states the dependency, and the date is at risk until the runner exists. |
|
||||||
|
|
||||||
|
## Results check
|
||||||
|
|
||||||
|
| # | Claim in the results | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Task 1: both forms resolve at `@main` | Pass | Run 418 (`.../actions/runs/2425`): jobs `wg-short`, `wg-full`, `ts-short`, `ts-full` each executed the action's `mask` step and main step and failed with the action's own `[configuration]` error. Read from the job logs through the API. |
|
||||||
|
| 2 | Run 417 failed because the branch was deleted | Pass | `git ls-remote` showed no `wireguard-vpn-action` head after pull request 194 merged; the log says `Unable to resolve wireguard-vpn-action: reference not found`. |
|
||||||
|
| 3 | Task 6: lint script, workflow step, pinned tools, seeded-error test | Pass | `tools/lint-action-scripts.sh`; the test file seeds a shellcheck problem and a badly formatted script, shows both fail, and shows the two action scripts pass; the full suite (590 tests) passes locally. Not yet run by the instance's CI. |
|
||||||
|
| 4 | The ten findings of the review of pull request 194 are answered | Pass | Each has a code change and an offline test (the daemon wait with a delayed stub, `sudo` that resets the environment, a host daemon not taken over, a line break refused, the key file removed on failure, `latest` refused). The wording of the results says they are offline-tested with stubs and that tasks 3 and 4 must show them on a runner. |
|
||||||
|
| 5 | Open tasks are listed honestly | Pass | Tasks 2, 3, 4, 5, 7, 8, 10, 11 and 12 are named as open, with the reason. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - the results recorded are accurate and claim no more than the evidence shows. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Review [ADR-0011] against its checklist and accept or change it; it is `Proposed` | S01 | Before task 10 (release 2.2.0) |
|
||||||
|
| Provide the `vpn` runner, the WireGuard peer and the Tailscale tag with an OAuth client for tasks 3 and 4; the target date 2026-11-06 depends on it | S03 | 2026-10-30 |
|
||||||
|
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[ADR-0011]: ../../adr/adr-0011-vpn-composite-actions.md
|
||||||
|
[RC-080]: ./rc-080-mil-012-plan.md
|
||||||
|
[RC-081]: ./rc-081-bc-001-o6.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[3325bea]: https://git.tirsystem.com/TirSystem/github-action/commit/3325bea772304b54e8dfffad592b4afadc3ec2b8
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
# Review Record: MIL-013 and PP-001 (MIL-013)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-083 |
|
||||||
|
| CrossReference | [MIL-013], [PP-001], [BC-001], [QC-MIL-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01, after one correction found in the first read | [e35b1c5] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-013]; delta of [PP-001] for the added milestone
|
||||||
|
- Checklist used: [QC-MIL-001] for the milestone; for the plan, the criteria of `PP.md` (no QC checklist exists for PP-001)
|
||||||
|
- Scope: full review of MIL-013; delta re-review of PP-001, earlier record [RC-080]
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | `## Deliverable` names release 2.3.0 with the reusable workflow, its scripts under `tools/`, guide, README section, wiki pages in three languages, changelog entry, offline tests, an ADR for the detection method and a review record, and the two pull requests in the other repositories. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Eleven criteria, each with a checkable Go and No-Go. The first is measurable (a corpus of named size, recall of at least 90 % for Danish and Malay, at most one false positive per 200 English comments); the others are runs, searches or a verified setting. Criterion 7 (the merge is blocked) depends on a setting that only the repository owner makes, and says so. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | [MIL-010] (the comment scanner), [MIL-004] (the Release workflow), [MIL-011] (`MAJOR_TAG`), [ADR-0005]; the outside dependencies (a Gitea token from S03, branch protection by S01 or S03, the framework and checklist repositories) are named. The first read noted that the planned window of [MIL-004] (2026-11-16 to 2026-11-28) starts after this milestone does; [MIL-004] is already released (`v1`, `v2`), so the dependency is met and the plan says nothing wrong. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Objective O7 and success criterion 7 of [BC-001], added in the same change and reviewed in [RC-084]. The first read found the traceability row still saying O7 was "added at acceptance of this milestone", which is now false; it states the objective plainly. |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Owner S05, approving reviewer S01; both are in [SA-001]. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | 2026-11-20, confirmed by S01 in chat on 2026-10-19; the Business Case sets no deadline. The window (2026-11-09 to 2026-11-20) follows [MIL-012] (2026-11-06) and overlaps [MIL-009], which is wiki callers and does not use the check's token or reviewer. |
|
||||||
|
|
||||||
|
## Plan delta (PP-001)
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | Every milestone has a `MIL-*` document and the plan's dates equal that document's target date | Pass | MIL-001 to MIL-013 exist; the MIL-013 decision date (2026-11-20) equals its Target Date. |
|
||||||
|
| 2 | Every Business Case scope item is covered by a gateway | Pass | The new scope item "A shared workflow that checks that comments in source code are in English and tells the author" is covered by [MIL-013] in the scope table. |
|
||||||
|
| 3 | The schedule respects the Business Case constraints | Pass | No deadline in the Business Case; the window is two weeks. |
|
||||||
|
| 4 | Dependencies between milestones are stated | Pass | `MIL-004 + MIL-010 + MIL-011 -> MIL-013`. |
|
||||||
|
| 5 | Every gateway has an owner who is a stakeholder ID | Pass | S05. |
|
||||||
|
| 6 | Plan content is internally consistent | Pass | The Purpose says "thirteen"; the Gantt, the schedule, the scope table, the dependencies and the planning assumption agree. Unused commit link definitions left by the history roll were removed. |
|
||||||
|
| 7 | Open Issues are current | Pass | A plan risk is added: the check calls a correct comment non-English or misses a Danish or Malay one, with the mitigation in tasks 1 and 2 and the exception marker. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - all criteria pass after one correction found in the first read (the stale "added at acceptance" wording). The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; this is not an independent review.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-02 |
|
||||||
|
| Task 9: decide which repositories mark the check as required and set it; the workflow cannot, and the AI does not change branch protection | S01 | Before task 8 ends |
|
||||||
|
| Ask the operator for a Gitea token that may comment on pull requests, stored as an organization secret, before task 8 | S03 | 2026-11-13 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-013]: ../../milestones/mil-013-english-comments-check.md
|
||||||
|
[PP-001]: ../../project-plan.md
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[SA-001]: ../../stakeholder-analysis.md
|
||||||
|
[MIL-004]: ../../milestones/mil-004-versioned-release.md
|
||||||
|
[MIL-009]: ../../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
|
||||||
|
[MIL-010]: ../../milestones/mil-010-code-metrics-badges.md
|
||||||
|
[MIL-011]: ../../milestones/mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[MIL-012]: ../../milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[ADR-0005]: ../../adr/adr-0005-quality-check-contract.md
|
||||||
|
[RC-080]: ./rc-080-mil-012-plan.md
|
||||||
|
[RC-084]: ./rc-084-bc-001-o7.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Review Record: BC-001 (objective O7, English comments check)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-084 |
|
||||||
|
| CrossReference | [BC-001], [QC-BC-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-19 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [e35b1c5] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [BC-001]
|
||||||
|
- Checklist used: [QC-BC-001]
|
||||||
|
- Scope: delta re-review of criteria 2, 3, 4 and 7 (the change adds objective O7, an In Scope bullet, an assumption, a constraint and success criterion 7); criteria 1, 5, 6 and 8 are re-read and unchanged. Earlier record: [RC-081]
|
||||||
|
- Language and domain: n/a for this delta (the action item of [RC-067] about the Language and Domain rows is still open)
|
||||||
|
- Language reviewer: none
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-19 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | ROI/Cost-Benefit analysis is quantitative, or where qualitative, is explicitly justified | Pass | Unchanged; the Cost-Benefit Assessment labels its qualitative form. |
|
||||||
|
| 2 | Risks are identified with documented impact and mitigation | Pass | The delivery risk (the check wrongly flags a correct comment, or misses a Danish or Malay one) is a plan risk in PP-001 with a mitigation: a measured corpus before the build, an exception marker and an allowed-words file. |
|
||||||
|
| 3 | Success criteria are measurable, stating explicit targets rather than vague aspirations | Pass | Criterion 7: "0 after MIL-013" non-English comments reaching the default branch of a repository that calls the check, measured by the check's run on the default branch and a throwaway pull request that cannot be merged. |
|
||||||
|
| 4 | Scope explicitly separates In Scope vs Out of Scope | Pass | "A shared workflow that checks that comments in source code are in English and tells the author (MIL-013)" is a new In Scope bullet. Out of Scope is unchanged and does not contradict it: "application code of consuming repositories" stays out because the check only reads comments and reports them, and changes nothing in the consuming repository; "operating the Gitea instance or runners" stays out because branch protection is set by S01 or S03, as the new assumption says. |
|
||||||
|
| 5 | Stakeholders are cross-referenced to Stakeholder Analysis IDs rather than re-described inline | Pass | Unchanged; the new assumption names S01 and S03. |
|
||||||
|
| 6 | Methodology and quality-standard foundation are stated explicitly (e.g. ISO/IEC 25010, Larman) | Pass | Unchanged. |
|
||||||
|
| 7 | Assumptions and constraints are explicit and clearly distinguished from one another | Pass | The assumption (S03 provides a token that may comment on pull requests; S01 or S03 mark the check required) and the constraint (no source text leaves the instance, the token never appears in a log) are in their own sections. |
|
||||||
|
| 8 | Document supports executive decision-making with a clear, unambiguous recommendation | Pass | Unchanged: Proceed. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - all eight criteria pass for the revision that adds objective O7, its scope item, assumption, constraint and success criterion 7. The author confirmed the acceptance in chat on 2026-10-19 as stand-in for S01; there is no independent reader and the decision stays open to challenge.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| The Metadata table of BC-001 has no `Language` and `Domain` rows (QC-LANG-001 criterion 1); this predates this change and is carried over from [RC-067]. Add the rows or record in an ADR why they are not needed | S01 | Before the next material change to BC-001 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[QC-BC-001]: ../../../framework/qc/artifacts/qc-business-case.md
|
||||||
|
[RC-067]: ./rc-067-bc-001-o5.md
|
||||||
|
[RC-081]: ./rc-081-bc-001-o6.md
|
||||||
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
# Review Record: MIL-013 results of tasks 1 to 8, 10 and 13 (MIL-013)
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
| Key | Value |
|
||||||
|
| --- | --- |
|
||||||
|
| ID | RC-085 |
|
||||||
|
| CrossReference | [MIL-013], [QC-MIL-001] |
|
||||||
|
|
||||||
|
## Version History
|
||||||
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| 2026-10-21 | Accepted | Jens Tirsvad Nielsen | S01 | Initial version; accepted by the author in chat as stand-in for S01 | [88f852a] |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Artifact Under Review
|
||||||
|
|
||||||
|
- Instance reviewed: [MIL-013]
|
||||||
|
- Checklist used: [QC-MIL-001]
|
||||||
|
- Scope: delta re-review of the milestone after [RC-083]: the new `## Verification results` section (the results of the merged work, the trial on the instance and its three defects, the search of the logs), and the added `[ADR-0012]` in the CrossReference. Criteria 1 to 6 are re-read; none of the milestone's own text for them changed. [ADR-0012] is **not** reviewed here: it stays `Proposed` and needs a review against its own checklist
|
||||||
|
- Language and domain: n/a (technical type)
|
||||||
|
- Mode: supervised, not independent (the author, Jens Tirsvad Nielsen, asked in chat on 2026-10-21 for the acceptance and stands in for S01; the AI prepared the evidence)
|
||||||
|
|
||||||
|
## Checklist Results
|
||||||
|
|
||||||
|
| # | Criterion | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | A concrete deliverable is defined for every gate | Pass | Unchanged: release 2.3.0 with the workflow, scripts, guide, wiki, tests, ADR and the two pull requests in the other repositories. The results do not claim any of it is released. |
|
||||||
|
| 2 | Explicit Go/No-Go criteria are stated for each gate | Pass | Unchanged, eleven criteria. The results bear on criterion 1 (corpus rates, met on the corpus, with the limits of an AI-written corpus said), 3 (a pull request with a Danish comment fails, one without passes: met in runs 440 and 445), 4 (an exception can be marked: met, the marked line was not reported), 5 (one comment that mentions the author, updated, resolved: met when the fix branch is called) and 6 (no token in a log: met for the four jobs searched; the detector made no network call). Criteria 7 to 11 are not claimed. |
|
||||||
|
| 3 | Dependencies on other milestones are explicitly mapped | Pass | Unchanged; [ADR-0012] is added to the CrossReference and exists. |
|
||||||
|
| 4 | Each milestone is traceable to a Business Case objective or KPI | Pass | Unchanged: objective O7 and success criterion 7 of [BC-001] ([RC-084]). |
|
||||||
|
| 5 | Milestone owner and approving reviewer are identified | Pass | Unchanged: owner S05, approving reviewer S01. |
|
||||||
|
| 6 | Milestone has a defined target date consistent with project constraints | Pass | Unchanged: 2026-11-20. Open tasks 9, 11, 12 and 14 and the merge of pull request 212 are before it; task 9 needs the owner. |
|
||||||
|
|
||||||
|
## Results check
|
||||||
|
|
||||||
|
| # | Claim in the results | Status | Evidence/Notes |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 1 | The first call at `@main` found only the Malay comment and skipped the author step | Pass | Read from the log of job 2761 of run 433 (id 2478): one finding at `trial/sample.py:9`, and the step list shows "Tell the author" skipped. |
|
||||||
|
| 2 | In a called workflow `gitea.event_name` is `workflow_call` | Pass | Printed by the debug workflow in runs 435 and 437: `event_name=workflow_call number=213 author=Tirsvad base_ref=main`; the steps with the event-name conditions are listed as skipped, the others as success. |
|
||||||
|
| 3 | The fix branch finds both comments, writes one comment, updates it, resolves it | Pass | Runs 440, 443 and 445 read from the API: the comment list of pull request 213 held one comment (id 8715) after each run; its text after run 445 was the "passes" text without a mention. |
|
||||||
|
| 4 | The run link equals the run's address | Pass | The link in the comment after run 447 is `https://git.tirsystem.com/TirSystem/github-action/actions/runs/2492`, the `html_url` of run 447. |
|
||||||
|
| 5 | No credential in the logs | Pass | A search of four job logs; the only lines matching were `REPORT_TOKEN: ***`. The search patterns cannot find a short or oddly formed secret, which the result says by naming the patterns. |
|
||||||
|
| 6 | The open items are listed honestly | Pass | The result names what the trial did not show: a corrected call at `@main`, a fork pull request, a custom token, the required check, and the call by this repository. |
|
||||||
|
|
||||||
|
## Overall Verdict
|
||||||
|
|
||||||
|
Go - the results recorded are accurate and claim no more than the evidence shows. The author confirmed the acceptance in chat on 2026-10-21 as stand-in for S01; this is not an independent review.
|
||||||
|
|
||||||
|
## Action Items
|
||||||
|
|
||||||
|
| Action | Owner | Due |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Review [ADR-0012] against its checklist and accept or change it; it is `Proposed` | S01 | Before task 11 (release 2.3.0) |
|
||||||
|
| Review and merge pull request 212 (the three defects); call the corrected workflow at `@main` once more | S01 | 2026-11-06 |
|
||||||
|
| Task 9: decide which repositories mark the check as required and set it | S01 | 2026-11-13 |
|
||||||
|
| Confirm this record by S01 when an independent reading is possible | S01 | 2026-11-09 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
[MIL-013]: ../../milestones/mil-013-english-comments-check.md
|
||||||
|
[BC-001]: ../../business-case.md
|
||||||
|
[ADR-0012]: ../../adr/adr-0012-english-comment-detection.md
|
||||||
|
[RC-083]: ./rc-083-mil-013-plan.md
|
||||||
|
[RC-084]: ./rc-084-bc-001-o7.md
|
||||||
|
[QC-MIL-001]: ../../../framework/qc/artifacts/qc-milestones-gateways.md
|
||||||
|
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
|
||||||
@@ -4,13 +4,13 @@
|
|||||||
| Key | Value |
|
| Key | Value |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| ID | TM-001 |
|
| ID | TM-001 |
|
||||||
| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [RC-074], [RC-075], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] |
|
| CrossReference | [BC-001], [SA-001], [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013], [ADR-0011], [ADR-0012], [UC-008], [RC-001], [RC-002], [RC-003], [RC-004], [RC-005], [RC-006], [RC-007], [RC-008], [RC-009], [RC-010], [RC-011], [RC-012], [RC-013], [RC-014], [RC-015], [RC-016], [RC-017], [RC-018], [RC-019], [RC-020], [RC-021], [RC-022], [RC-023], [RC-024], [RC-025], [RC-026], [RC-027], [RC-028], [RC-029], [RC-030], [RC-031], [RC-032], [RC-033], [RC-034], [RC-035], [RC-036], [RC-037], [RC-038], [RC-039], [RC-040], [RC-041], [RC-042], [RC-043], [RC-044], [RC-045], [RC-046], [RC-047], [RC-048], [RC-049], [RC-050], [RC-051], [RC-052], [RC-053], [RC-054], [RC-055], [RC-056], [RC-057], [RC-058], [RC-059], [RC-060], [RC-061], [RC-062], [RC-063], [RC-064], [RC-065], [RC-066], [RC-067], [RC-068], [RC-069], [RC-070], [RC-071], [RC-072], [RC-073], [RC-074], [RC-075], [RC-076], [RC-077], [RC-078], [RC-079], [RC-080], [RC-081], [RC-082], [RC-083], [RC-084], [RC-085], [ADR-0007], [ADR-0008], [ADR-0009], [ADR-0010], [UC-007], [UC-006], [ADR-0006], [UC-005], [ADR-0005], [UCD-001], [US-001], [UC-001], [UC-002], [UC-003], [UC-004], [ADR-0001], [ADR-0002], [ADR-0003], [ADR-0004] |
|
||||||
|
|
||||||
## Version History
|
## Version History
|
||||||
| Date | Status | Author | Reviewer | Change | Commit |
|
| Date | Status | Author | Reviewer | Change | Commit |
|
||||||
| --- | --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- | --- |
|
||||||
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-011 and RC-076 (fix the defaults of the code metrics workflow) | [3d7a9df] |
|
| 2026-10-19 | Proposed | Jens Tirsvad Nielsen | S01 | Added MIL-013 (English comments check), RC-083 and RC-084 (its plan and objective O7) | [e35b1c5] |
|
||||||
| 2026-10-14 | Proposed | Jens Tirsvad Nielsen | S01 | Added RC-077 and RC-078 (MIL-011 task 1 results, and the fix of the defaults) | [a6892c3] |
|
| 2026-10-21 | Proposed | Jens Tirsvad Nielsen | S01 | Added ADR-0012 and RC-085 (MIL-013 results of the trial) | [88f852a] |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -25,13 +25,15 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
|
| Artifact Instance | Type | Upstream (Backward Link) | Downstream (Forward Link) | Last Reviewed (RC-ID) |
|
||||||
| --- | --- | --- | --- | --- |
|
| --- | --- | --- | --- | --- |
|
||||||
| [SA-001] | SA | - | [BC-001], [UCD-001] | [RC-004] |
|
| [SA-001] | SA | - | [BC-001], [UCD-001] | [RC-004] |
|
||||||
| [BC-001] | BC | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006] | [RC-001], [RC-008], [RC-067] |
|
| [BC-001] | BC | [SA-001] | [PP-001], [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-012], [MIL-013] | [RC-001], [RC-008], [RC-067], [RC-081], [RC-084] |
|
||||||
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [US-001] | [RC-002], [RC-009], [RC-015], [RC-017], [RC-025], [RC-032], [RC-053], [RC-062], [RC-064], [RC-068] |
|
| [PP-001] | PP | [BC-001], [SA-001] | [MIL-001], [MIL-002], [MIL-003], [MIL-004], [MIL-005], [MIL-006], [MIL-007], [MIL-008], [MIL-009], [MIL-010], [MIL-011], [MIL-012], [MIL-013], [US-001] | [RC-002], [RC-009], [RC-015], [RC-017], [RC-025], [RC-032], [RC-053], [RC-062], [RC-064], [RC-068], [RC-080], [RC-083] |
|
||||||
| [MIL-001] | MIL | [BC-001], [PP-001] | [MIL-002] | [RC-003], [RC-026] |
|
| [MIL-001] | MIL | [BC-001], [PP-001] | [MIL-002] | [RC-003], [RC-026] |
|
||||||
| [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] |
|
| [MIL-002] | MIL | [BC-001], [PP-001], [MIL-001] | [MIL-003], [MIL-005] | [RC-005], [RC-027] |
|
||||||
| [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] |
|
| [MIL-003] | MIL | [BC-001], [PP-001], [MIL-002] | [MIL-004] | [RC-006], [RC-028], [RC-033], [RC-039] |
|
||||||
| [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] |
|
| [MIL-004] | MIL | [BC-001], [PP-001], [MIL-003] | - | [RC-007], [RC-029], [RC-034], [RC-048] |
|
||||||
| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072], [RC-073] |
|
| [ADR-0010] | ADR | [MIL-010], [UC-008], [US-001], [ADR-0005], [ADR-0006] | - | [RC-072], [RC-073] |
|
||||||
|
| [ADR-0011] | ADR | [MIL-012], [ADR-0005], [ADR-0006] | - | - |
|
||||||
|
| [ADR-0012] | ADR | [MIL-013], [ADR-0005], [ADR-0010] | - | - |
|
||||||
| [RC-001] | RC | [BC-001] | - | - |
|
| [RC-001] | RC | [BC-001] | - | - |
|
||||||
| [RC-002] | RC | [PP-001] | - | - |
|
| [RC-002] | RC | [PP-001] | - | - |
|
||||||
| [RC-003] | RC | [MIL-001] | - | - |
|
| [RC-003] | RC | [MIL-001] | - | - |
|
||||||
@@ -44,7 +46,9 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| [MIL-008] | MIL | [BC-001], [PP-001], [MIL-006], [ADR-0004] | [MIL-009] | [RC-060], [RC-061] |
|
| [MIL-008] | MIL | [BC-001], [PP-001], [MIL-006], [ADR-0004] | [MIL-009] | [RC-060], [RC-061] |
|
||||||
| [MIL-009] | MIL | [BC-001], [PP-001], [MIL-008], [ADR-0004], [ADR-0006], [ADR-0009] | - | [RC-064], [RC-066] |
|
| [MIL-009] | MIL | [BC-001], [PP-001], [MIL-008], [ADR-0004], [ADR-0006], [ADR-0009] | - | [RC-064], [RC-066] |
|
||||||
| [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | [MIL-011] | [RC-068], [RC-074], [RC-075] |
|
| [MIL-010] | MIL | [BC-001], [PP-001], [MIL-003], [MIL-004] | [MIL-011] | [RC-068], [RC-074], [RC-075] |
|
||||||
| [MIL-011] | MIL | [BC-001], [PP-001], [MIL-010], [ADR-0006], [ADR-0010] | - | [RC-076], [RC-077], [RC-078] |
|
| [MIL-011] | MIL | [BC-001], [PP-001], [MIL-010], [ADR-0006], [ADR-0010] | [MIL-012] | [RC-076], [RC-077], [RC-078], [RC-079] |
|
||||||
|
| [MIL-012] | MIL | [BC-001], [PP-001], [MIL-004], [MIL-011], [ADR-0006], [ADR-0011] | - | [RC-080], [RC-082] |
|
||||||
|
| [MIL-013] | MIL | [BC-001], [PP-001], [MIL-004], [MIL-010], [MIL-011], [ADR-0005], [ADR-0006], [ADR-0010] | - | [RC-083], [RC-085] |
|
||||||
| [RC-007] | RC | [MIL-004] | - | - |
|
| [RC-007] | RC | [MIL-004] | - | - |
|
||||||
| [RC-008] | RC | [BC-001] | - | - |
|
| [RC-008] | RC | [BC-001] | - | - |
|
||||||
| [RC-009] | RC | [PP-001] | - | - |
|
| [RC-009] | RC | [PP-001] | - | - |
|
||||||
@@ -136,6 +140,13 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
| [RC-076] | RC | [MIL-011], [PP-001] | - | - |
|
| [RC-076] | RC | [MIL-011], [PP-001] | - | - |
|
||||||
| [RC-077] | RC | [MIL-011] | - | - |
|
| [RC-077] | RC | [MIL-011] | - | - |
|
||||||
| [RC-078] | RC | [MIL-011] | - | - |
|
| [RC-078] | RC | [MIL-011] | - | - |
|
||||||
|
| [RC-079] | RC | [MIL-011] | - | - |
|
||||||
|
| [RC-080] | RC | [MIL-012], [PP-001] | - | - |
|
||||||
|
| [RC-081] | RC | [BC-001] | - | - |
|
||||||
|
| [RC-082] | RC | [MIL-012] | - | - |
|
||||||
|
| [RC-083] | RC | [MIL-013], [PP-001] | - | - |
|
||||||
|
| [RC-084] | RC | [BC-001] | - | - |
|
||||||
|
| [RC-085] | RC | [MIL-013] | - | - |
|
||||||
|
|
||||||
## Coverage Notes
|
## Coverage Notes
|
||||||
|
|
||||||
@@ -159,8 +170,12 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[MIL-008]: ../milestones/mil-008-wiki-reusable-workflow.md
|
[MIL-008]: ../milestones/mil-008-wiki-reusable-workflow.md
|
||||||
[MIL-010]: ../milestones/mil-010-code-metrics-badges.md
|
[MIL-010]: ../milestones/mil-010-code-metrics-badges.md
|
||||||
[MIL-011]: ../milestones/mil-011-code-metrics-default-tools-ref.md
|
[MIL-011]: ../milestones/mil-011-code-metrics-default-tools-ref.md
|
||||||
|
[MIL-012]: ../milestones/mil-012-vpn-actions-for-deployment.md
|
||||||
|
[MIL-013]: ../milestones/mil-013-english-comments-check.md
|
||||||
[MIL-009]: ../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
|
[MIL-009]: ../milestones/mil-009-migrate-wiki-callers-and-remove-scoped-workflow.md
|
||||||
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
|
[ADR-0010]: ../adr/adr-0010-code-metrics-badges-contract.md
|
||||||
|
[ADR-0011]: ../adr/adr-0011-vpn-composite-actions.md
|
||||||
|
[ADR-0012]: ../adr/adr-0012-english-comment-detection.md
|
||||||
[RC-001]: ./reviews/rc-001-business-case.md
|
[RC-001]: ./reviews/rc-001-business-case.md
|
||||||
[RC-002]: ./reviews/rc-002-project-plan.md
|
[RC-002]: ./reviews/rc-002-project-plan.md
|
||||||
[RC-003]: ./reviews/rc-003-mil-001.md
|
[RC-003]: ./reviews/rc-003-mil-001.md
|
||||||
@@ -249,6 +264,13 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[RC-076]: ./reviews/rc-076-mil-011-plan.md
|
[RC-076]: ./reviews/rc-076-mil-011-plan.md
|
||||||
[RC-077]: ./reviews/rc-077-mil-011-task-1-results.md
|
[RC-077]: ./reviews/rc-077-mil-011-task-1-results.md
|
||||||
[RC-078]: ./reviews/rc-078-mil-011-fix.md
|
[RC-078]: ./reviews/rc-078-mil-011-fix.md
|
||||||
|
[RC-079]: ./reviews/rc-079-mil-011-outcome.md
|
||||||
|
[RC-080]: ./reviews/rc-080-mil-012-plan.md
|
||||||
|
[RC-081]: ./reviews/rc-081-bc-001-o6.md
|
||||||
|
[RC-082]: ./reviews/rc-082-mil-012-results.md
|
||||||
|
[RC-083]: ./reviews/rc-083-mil-013-plan.md
|
||||||
|
[RC-084]: ./reviews/rc-084-bc-001-o7.md
|
||||||
|
[RC-085]: ./reviews/rc-085-mil-013-results.md
|
||||||
[ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md
|
[ADR-0008]: ../adr/adr-0008-cpp-csharp-quality-check-contract.md
|
||||||
[ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md
|
[ADR-0009]: ../adr/adr-0009-wiki-publishing-reusable-workflow.md
|
||||||
[ADR-0007]: ../adr/adr-0007-release-publish-destination.md
|
[ADR-0007]: ../adr/adr-0007-release-publish-destination.md
|
||||||
@@ -259,4 +281,5 @@ updated whenever an artifact instance is created or reviewed.
|
|||||||
[UC-005]: ../uc-005/uc.md
|
[UC-005]: ../uc-005/uc.md
|
||||||
[ADR-0005]: ../adr/adr-0005-quality-check-contract.md
|
[ADR-0005]: ../adr/adr-0005-quality-check-contract.md
|
||||||
[3d7a9df]: https://git.tirsystem.com/TirSystem/github-action/commit/3d7a9df3a3a4712dcc5214a71b0569d90122bf4e
|
[3d7a9df]: https://git.tirsystem.com/TirSystem/github-action/commit/3d7a9df3a3a4712dcc5214a71b0569d90122bf4e
|
||||||
[a6892c3]: https://git.tirsystem.com/TirSystem/github-action/commit/a6892c372aca9f2e12826a4c3b260290a0656ecb
|
[e35b1c5]: https://git.tirsystem.com/TirSystem/github-action/commit/e35b1c56748a50430503f5518602469f93262357
|
||||||
|
[88f852a]: https://git.tirsystem.com/TirSystem/github-action/commit/88f852af9398468b91e88fe0190ce5c35dcf0fa2
|
||||||
|
|||||||
@@ -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 9dafcabbc807 -->
|
||||||
|
|
||||||
|
> Dette er en oversættelse af [`EnglishComments`](EnglishComments). Den engelske side er autoritativ, og ændringer her bliver overskrevet, næste gang wikien publiceres.
|
||||||
|
|
||||||
|
[English](EnglishComments) | Dansk | [Bahasa Melayu](EnglishComments.ms)
|
||||||
|
|
||||||
|
# Kommentarer i kildekoden på engelsk
|
||||||
|
|
||||||
|
Sådan kalder en softwareudvikler det fælles workflow, der kontrollerer, at
|
||||||
|
kommentarerne og docstrings i en pull request er skrevet på engelsk. Workflowet
|
||||||
|
lader kørslen fejle, når én ikke er det, og skriver én kommentar på pull
|
||||||
|
requesten, der nævner forfatteren. Det ændrer ingen fil og sender ingen
|
||||||
|
kildetekst ud af instansen. Beslutningen bag er ADR-0012 i dette repository.
|
||||||
|
|
||||||
|
## Kald af workflowet
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: English comments
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
english-comments:
|
||||||
|
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
|
||||||
|
with:
|
||||||
|
exclude: "docs/generated/**"
|
||||||
|
allowed-words-file: .english-comments-allowed-words
|
||||||
|
```
|
||||||
|
|
||||||
|
| Input | Standard | Betydning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scope` | `changed` | `changed` kontrollerer kommentarerne på de linjer, pull requesten tilføjer; `all` kontrollerer hver sporet fil |
|
||||||
|
| `exclude` | | Kommasepareret liste af fil- eller stimønstre, der ikke kontrolleres (`*` og `**` er jokertegn) |
|
||||||
|
| `allowed-words-file` | | En fil i repositoryet med ord, der aldrig bedømmes, ét pr. linje; `#` starter en kommentar |
|
||||||
|
| `threshold` | `0.6` | Sandsynligheden, over 0 og højst 1, hvorfra en kommentar regnes for ikke at være engelsk |
|
||||||
|
| `min-words` | `3` | En kommentar med færre ord bedømmes ikke |
|
||||||
|
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | De fastlåste pakker, detektoren bruger |
|
||||||
|
| `tools-ref` | den nuværende hovedversion | Det tag i dette repository, hvis scripts køres; fastlås det sammen med workflowet |
|
||||||
|
|
||||||
|
Den valgfri secret `REPORT_TOKEN` er et Gitea-token, der må kommentere på pull
|
||||||
|
requests. Uden den bruger workflowet kørslens eget token, så kommentaren
|
||||||
|
skrives af Actions-botten. En pull request fra en fork har et token med kun
|
||||||
|
læseadgang: kontrollen får stadig kørslen til at fejle, men der skrives ingen
|
||||||
|
kommentar.
|
||||||
|
|
||||||
|
## Hvad der kontrolleres
|
||||||
|
|
||||||
|
Kommentarerne og docstrings i filer på de sprog, kodetælleren kender
|
||||||
|
([kodemålinger](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
|
||||||
|
JavaScript, TypeScript, Go, Rust og SQL. Strenge og kode læses ikke. Kommentarlinjer
|
||||||
|
efter hinanden danner en blok. Hver linje i en blok bedømmes for sig (mindst fire ord og en
|
||||||
|
sandsynlighed på mindst 0,7), så en engelsk linje ikke kan skjule en dansk ved siden af; når ingen linje
|
||||||
|
markeres, bedømmes blokken som helhed (mindst `min-words` ord og `threshold`), så en sætning delt over korte
|
||||||
|
linjer ikke overses.
|
||||||
|
|
||||||
|
Før en blok bedømmes, fjernes alt, der ikke er et ord: stier, URL'er, tilvalg
|
||||||
|
(`--output`), filnavne, identifikatorer (`snake_case`, `camelCase`), tal,
|
||||||
|
akronymer og konstanter (`JSON`), en indledende etiket som `Usage:` og ordene
|
||||||
|
i filen med tilladte ord. En blok med færre end `min-words` ord tilbage
|
||||||
|
bedømmes ikke, så en kort kommentar slipper igennem. Kommentarer, der
|
||||||
|
konfigurerer et værktøj (`noqa`, `type:`, `pragma`, `shellcheck`), læses ikke.
|
||||||
|
|
||||||
|
## Når kontrollen fejler
|
||||||
|
|
||||||
|
Kørslen fejler, hvert fund vises som en annotation (`::error file=...,line=...`)
|
||||||
|
og i loggen som `file:line: looks Danish (0.93): "the comment"`, og én kommentar
|
||||||
|
på pull requesten opregner dem og nævner forfatteren. En ny kørsel opdaterer den
|
||||||
|
kommentar; når pull requesten består, markeres kommentaren som løst og nævner
|
||||||
|
ikke længere nogen.
|
||||||
|
|
||||||
|
Ret det ved at skrive kommentaren om på engelsk. For et citat eller et navn, der
|
||||||
|
ikke er engelsk, kan du enten skrive `english-comments: ignore` i kommentaren
|
||||||
|
(det springer hele den kommentarblok over, den står i), eller tilføje ordet til
|
||||||
|
filen med tilladte ord.
|
||||||
|
|
||||||
|
## Blokering af sammenfletningen
|
||||||
|
|
||||||
|
En fejlende kørsel blokerer ikke i sig selv en sammenfletning. Markér kontrollen
|
||||||
|
som påkrævet i grenbeskyttelsen af målgrenen: kør workflowet én gang på en pull
|
||||||
|
request og vælg dets post på listen over statuskontroller (det præcise navn
|
||||||
|
dannes af det kaldende workflow, dets job og hændelsen). At indstille
|
||||||
|
grenbeskyttelse er repositoryejerens handling; workflowet ændrer den ikke.
|
||||||
|
|
||||||
|
## Hvor præcis den er
|
||||||
|
|
||||||
|
Detektoren er `langid`, begrænset til engelsk, dansk, norsk, svensk, tysk,
|
||||||
|
fransk, malajisk, indonesisk og hollandsk. På repositoryets korpus markerer den
|
||||||
|
ingen af 300 engelske kommentarer, 44 af 45 danske og 41 af 45 malajiske
|
||||||
|
kommentarer og alle 48 tyske, svenske, norske og franske (ADR-0012). Dens
|
||||||
|
grænser:
|
||||||
|
|
||||||
|
- en kommentar på ét eller to ord, eller en der mest består af navne, bedømmes ikke;
|
||||||
|
- en kommentar på dårligt engelsk slipper igennem, og en kommentar, der blander
|
||||||
|
engelske fagudtryk med få danske eller malajiske ord, kan slippe igennem;
|
||||||
|
- korpussets danske og malajiske kommentarer er skrevet til testen og ikke samlet
|
||||||
|
fra rigtig kode, så rigtig kode vil vise andre fejl. Meld en misset kommentar
|
||||||
|
eller et falsk fund med kommentaren, så kommer den med i korpussen.
|
||||||
|
|
||||||
|
## Fejlfinding
|
||||||
|
|
||||||
|
| Besked | Årsag |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] give --base` | Workflowet blev startet af en hændelse, der ikke er en pull request, med `scope: changed`; kald det på `pull_request` eller brug `scope: all` |
|
||||||
|
| `[configuration] git diff failed` | Basisgrenen findes ikke i checkout; workflowet henter hele historikken, så tjek at pull requesten har en målgren |
|
||||||
|
| `[configuration] langid is not installed` | Installationstrinnet fejlede; tjek de fastlåste versioner |
|
||||||
|
| `[permissions] ... may not comment on pull requests` | Tokenet må ikke kommentere; giv `REPORT_TOKEN` skriveadgang til issues og pull requests |
|
||||||
|
| Et fund, der ikke er et fund | Tilføj ordene til filen med tilladte ord, eller `english-comments: ignore` til kommentaren |
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<!-- Generated from guides/english-comments.md by tools/build_wiki.py. Edit the source, not this page. -->
|
||||||
|
|
||||||
|
> This page is generated from [`guides/english-comments.md`](https://git.tirsystem.com/TirSystem/github-action/src/branch/main/guides/english-comments.md). Changes made here are overwritten the next time the wiki is published.
|
||||||
|
|
||||||
|
English | [Dansk](EnglishComments.da) | [Bahasa Melayu](EnglishComments.ms)
|
||||||
|
|
||||||
|
# Keeping source code comments in English
|
||||||
|
|
||||||
|
How a software engineer calls the shared workflow that checks that the comments
|
||||||
|
and docstrings of a pull request are written in English. The workflow fails the
|
||||||
|
run when one is not, and writes one comment on the pull request that mentions
|
||||||
|
the author. It changes no file and sends no source text outside the instance.
|
||||||
|
Its decision is ADR-0012 in this repository.
|
||||||
|
|
||||||
|
## Calling the workflow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: English comments
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
english-comments:
|
||||||
|
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
|
||||||
|
with:
|
||||||
|
exclude: "docs/generated/**"
|
||||||
|
allowed-words-file: .english-comments-allowed-words
|
||||||
|
```
|
||||||
|
|
||||||
|
| Input | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scope` | `changed` | `changed` checks the comments on the lines the pull request adds; `all` checks every tracked file |
|
||||||
|
| `exclude` | | Comma list of file name or path patterns that are not checked (`*` and `**` are wildcards) |
|
||||||
|
| `allowed-words-file` | | A file of the repository with words that are never judged, one per line, `#` starts a comment |
|
||||||
|
| `threshold` | `0.6` | The probability, above 0 and at most 1, from which a comment counts as not English |
|
||||||
|
| `min-words` | `3` | A comment with fewer words is not judged |
|
||||||
|
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | The pinned packages the detector needs |
|
||||||
|
| `tools-ref` | the current major | The tag of this repository whose scripts are run; pin it together with the workflow |
|
||||||
|
|
||||||
|
The optional secret `REPORT_TOKEN` is a Gitea token that may comment on pull
|
||||||
|
requests. Without it the workflow uses the token of the run, so the comment is
|
||||||
|
written by the Actions bot. A pull request from a fork has a read-only token:
|
||||||
|
the check still fails the run, but no comment is written.
|
||||||
|
|
||||||
|
## What is checked
|
||||||
|
|
||||||
|
The comments and docstrings of the files of the languages in the code counter
|
||||||
|
([code metrics](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
|
||||||
|
JavaScript, TypeScript, Go, Rust and SQL. Strings and code are not read.
|
||||||
|
Consecutive comment lines form a block. Each line of a block is judged on its own
|
||||||
|
(at least four words and a probability of at least 0.7), so an English line cannot
|
||||||
|
hide a Danish one next to it; when no line is flagged, the block is judged as a whole
|
||||||
|
(at least `min-words` words and `threshold`), so a sentence cut over short lines is not missed.
|
||||||
|
|
||||||
|
Before a block is judged, everything that is not a word is removed: paths, URLs,
|
||||||
|
options (`--output`), file names, identifiers (`snake_case`, `camelCase`),
|
||||||
|
numbers, acronyms and constants (`JSON`), a leading label such as `Usage:`, and
|
||||||
|
the words of the allowed-words file. A block with fewer than `min-words` words
|
||||||
|
left is not judged, so a short comment passes. Comments that configure a tool
|
||||||
|
(`noqa`, `type:`, `pragma`, `shellcheck`) are not read.
|
||||||
|
|
||||||
|
## When the check fails
|
||||||
|
|
||||||
|
The run fails, each finding is shown as an annotation (`::error file=...,line=...`)
|
||||||
|
and in the log as `file:line: looks Danish (0.93): "the comment"`, and one comment
|
||||||
|
on the pull request lists them and mentions the author. A new run updates that
|
||||||
|
comment; when the pull request passes, the comment is marked resolved and no
|
||||||
|
longer mentions anyone.
|
||||||
|
|
||||||
|
Fix it by rewriting the comment in English. For a quotation or a name that is
|
||||||
|
not English, either put `english-comments: ignore` in the comment (it skips the
|
||||||
|
whole comment block it is in), or add the word to the allowed-words file.
|
||||||
|
|
||||||
|
## Blocking the merge
|
||||||
|
|
||||||
|
A failing run does not block a merge by itself. In the branch protection of the
|
||||||
|
target branch, mark the check as required: run the workflow once on a pull
|
||||||
|
request and choose its entry in the list of status checks (the exact name is
|
||||||
|
made from the calling workflow, its job and the event). Setting branch
|
||||||
|
protection is the repository owner's action; the workflow does not change it.
|
||||||
|
|
||||||
|
## How accurate it is
|
||||||
|
|
||||||
|
The detector is `langid`, limited to English, Danish, Norwegian, Swedish,
|
||||||
|
German, French, Malay, Indonesian and Dutch. On the corpus of the repository it
|
||||||
|
flags none of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments
|
||||||
|
and all of the 48 German, Swedish, Norwegian and French ones (ADR-0012). Its
|
||||||
|
limits:
|
||||||
|
|
||||||
|
- a comment of one or two words, or one that is mostly names, is not judged;
|
||||||
|
- a comment in poor English passes, and a comment that mixes English technical
|
||||||
|
terms with few Danish or Malay words can pass;
|
||||||
|
- the Danish and Malay comments of the corpus were written for the test, not
|
||||||
|
collected from real code, so real code will show other misses. Report a miss
|
||||||
|
or a false positive with the comment, and it joins the corpus.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Message | Cause |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] give --base` | The workflow was started by an event that is not a pull request with `scope: changed`; call it on `pull_request` or use `scope: all` |
|
||||||
|
| `[configuration] git diff failed` | The base branch is not in the checkout; the workflow fetches the full history, so check that the pull request has a target branch |
|
||||||
|
| `[configuration] langid is not installed` | The install step failed; check the pinned versions |
|
||||||
|
| `[permissions] ... may not comment on pull requests` | The token cannot comment; give `REPORT_TOKEN` write access to issues and pull requests |
|
||||||
|
| A finding that is not a finding | Add the words to the allowed-words file, or `english-comments: ignore` to the comment |
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
<!-- translation of EnglishComments.md, source sha256 9dafcabbc807 -->
|
||||||
|
|
||||||
|
> Ini ialah terjemahan [`EnglishComments`](EnglishComments). Halaman bahasa Inggeris ialah teks yang sah, dan perubahan di sini akan ditimpa pada penerbitan wiki yang seterusnya.
|
||||||
|
|
||||||
|
[English](EnglishComments) | [Dansk](EnglishComments.da) | Bahasa Melayu
|
||||||
|
|
||||||
|
# Memastikan komen kod sumber dalam bahasa Inggeris
|
||||||
|
|
||||||
|
Cara seorang jurutera perisian memanggil aliran kerja bersama yang menyemak bahawa
|
||||||
|
komen dan docstring sesuatu pull request ditulis dalam bahasa Inggeris. Aliran
|
||||||
|
kerja menggagalkan larian apabila ada yang bukan bahasa Inggeris, dan menulis satu
|
||||||
|
komen pada pull request yang menyebut penulisnya. Ia tidak mengubah sebarang fail
|
||||||
|
dan tidak menghantar teks sumber keluar daripada instans. Keputusan di
|
||||||
|
belakangnya ialah ADR-0012 dalam repositori ini.
|
||||||
|
|
||||||
|
## Memanggil aliran kerja
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: English comments
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
english-comments:
|
||||||
|
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
|
||||||
|
with:
|
||||||
|
exclude: "docs/generated/**"
|
||||||
|
allowed-words-file: .english-comments-allowed-words
|
||||||
|
```
|
||||||
|
|
||||||
|
| Input | Lalai | Maksud |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scope` | `changed` | `changed` menyemak komen pada baris yang ditambah oleh pull request; `all` menyemak setiap fail yang dijejaki |
|
||||||
|
| `exclude` | | Senarai corak nama fail atau laluan yang dipisahkan koma dan tidak disemak (`*` dan `**` ialah kad liar) |
|
||||||
|
| `allowed-words-file` | | Fail dalam repositori dengan perkataan yang tidak pernah dinilai, satu pada setiap baris; `#` memulakan komen |
|
||||||
|
| `threshold` | `0.6` | Kebarangkalian, melebihi 0 dan paling banyak 1, yang mulai daripadanya sesuatu komen dianggap bukan bahasa Inggeris |
|
||||||
|
| `min-words` | `3` | Komen dengan perkataan yang lebih sedikit tidak dinilai |
|
||||||
|
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | Pakej tetap yang diperlukan oleh pengesan |
|
||||||
|
| `tools-ref` | major semasa | Tag repositori ini yang skripnya dijalankan; tetapkan bersama aliran kerja |
|
||||||
|
|
||||||
|
Secret pilihan `REPORT_TOKEN` ialah token Gitea yang boleh memberi komen pada pull
|
||||||
|
request. Tanpanya, aliran kerja menggunakan token larian itu sendiri, jadi komen
|
||||||
|
ditulis oleh bot Actions. Pull request daripada fork mempunyai token baca sahaja:
|
||||||
|
semakan masih menggagalkan larian, tetapi tiada komen ditulis.
|
||||||
|
|
||||||
|
## Apa yang disemak
|
||||||
|
|
||||||
|
Komen dan docstring dalam fail bahasa yang dikenali oleh pengira kod
|
||||||
|
([metrik kod](CodeMetrics)): Python, shell, PowerShell, C, C++, C#, Java,
|
||||||
|
JavaScript, TypeScript, Go, Rust dan SQL. String dan kod tidak dibaca. Baris komen
|
||||||
|
yang berturutan membentuk satu blok. Setiap baris dalam blok dinilai secara berasingan (sekurang-kurangnya
|
||||||
|
empat perkataan dan kebarangkalian sekurang-kurangnya 0.7), jadi baris bahasa Inggeris tidak dapat menyembunyikan
|
||||||
|
baris bahasa Denmark di sebelahnya; apabila tiada baris ditandakan, blok dinilai secara keseluruhan
|
||||||
|
(sekurang-kurangnya `min-words` perkataan dan `threshold`), supaya ayat yang dipecahkan kepada baris pendek tidak terlepas.
|
||||||
|
|
||||||
|
Sebelum sesuatu blok dinilai, segala yang bukan perkataan dibuang: laluan, URL,
|
||||||
|
pilihan (`--output`), nama fail, pengecam (`snake_case`, `camelCase`), nombor,
|
||||||
|
akronim dan pemalar (`JSON`), label di permulaan seperti `Usage:`, dan perkataan
|
||||||
|
dalam fail perkataan yang dibenarkan. Blok yang tinggal kurang daripada
|
||||||
|
`min-words` perkataan tidak dinilai, jadi komen pendek lulus. Komen yang
|
||||||
|
mengkonfigurasi alat (`noqa`, `type:`, `pragma`, `shellcheck`) tidak dibaca.
|
||||||
|
|
||||||
|
## Apabila semakan gagal
|
||||||
|
|
||||||
|
Larian gagal, setiap penemuan dipaparkan sebagai anotasi (`::error file=...,line=...`)
|
||||||
|
dan dalam log sebagai `file:line: looks Danish (0.93): "the comment"`, dan satu
|
||||||
|
komen pada pull request menyenaraikannya serta menyebut penulisnya. Larian baharu
|
||||||
|
mengemas kini komen itu; apabila pull request lulus, komen ditandakan selesai dan
|
||||||
|
tidak lagi menyebut sesiapa.
|
||||||
|
|
||||||
|
Betulkannya dengan menulis semula komen dalam bahasa Inggeris. Untuk petikan atau
|
||||||
|
nama yang bukan bahasa Inggeris, sama ada letakkan `english-comments: ignore`
|
||||||
|
dalam komen (ia melangkau seluruh blok komen tempat ia berada), atau tambahkan
|
||||||
|
perkataan itu ke dalam fail perkataan yang dibenarkan.
|
||||||
|
|
||||||
|
## Menyekat penggabungan
|
||||||
|
|
||||||
|
Larian yang gagal tidak menyekat penggabungan dengan sendirinya. Dalam
|
||||||
|
perlindungan cabang bagi cabang sasaran, tandakan semakan sebagai wajib: jalankan
|
||||||
|
aliran kerja sekali pada sebuah pull request dan pilih entrinya dalam senarai
|
||||||
|
semakan status (nama tepatnya dibentuk daripada aliran kerja pemanggil, kerjanya
|
||||||
|
dan peristiwa). Menetapkan perlindungan cabang ialah tindakan pemilik repositori;
|
||||||
|
aliran kerja tidak mengubahnya.
|
||||||
|
|
||||||
|
## Sejauh mana ia tepat
|
||||||
|
|
||||||
|
Pengesan ialah `langid`, terhad kepada bahasa Inggeris, Denmark, Norway, Sweden,
|
||||||
|
Jerman, Perancis, Melayu, Indonesia dan Belanda. Pada korpus repositori, ia
|
||||||
|
menandakan tiada satu pun daripada 300 komen bahasa Inggeris, 44 daripada 45
|
||||||
|
komen bahasa Denmark dan 41 daripada 45 komen bahasa Melayu, serta kesemua 48
|
||||||
|
komen bahasa Jerman, Sweden, Norway dan Perancis (ADR-0012). Batasannya:
|
||||||
|
|
||||||
|
- komen satu atau dua perkataan, atau yang kebanyakannya nama, tidak dinilai;
|
||||||
|
- komen dalam bahasa Inggeris yang lemah lulus, dan komen yang mencampurkan istilah
|
||||||
|
teknikal bahasa Inggeris dengan beberapa perkataan Denmark atau Melayu boleh lulus;
|
||||||
|
- komen bahasa Denmark dan Melayu dalam korpus ditulis untuk ujian, bukan dikumpulkan
|
||||||
|
daripada kod sebenar, jadi kod sebenar akan menunjukkan kesilapan lain. Laporkan
|
||||||
|
komen yang terlepas atau penemuan palsu bersama komennya, dan ia akan masuk ke
|
||||||
|
dalam korpus.
|
||||||
|
|
||||||
|
## Diagnosis
|
||||||
|
|
||||||
|
| Mesej | Punca |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] give --base` | Aliran kerja dimulakan oleh peristiwa yang bukan pull request dengan `scope: changed`; panggil pada `pull_request` atau gunakan `scope: all` |
|
||||||
|
| `[configuration] git diff failed` | Cabang asas tiada dalam checkout; aliran kerja mengambil seluruh sejarah, jadi semak bahawa pull request mempunyai cabang sasaran |
|
||||||
|
| `[configuration] langid is not installed` | Langkah pemasangan gagal; semak versi yang ditetapkan |
|
||||||
|
| `[permissions] ... may not comment on pull requests` | Token tidak boleh memberi komen; berikan `REPORT_TOKEN` akses tulis kepada isu dan pull request |
|
||||||
|
| Penemuan yang bukan penemuan | Tambahkan perkataan ke dalam fail perkataan yang dibenarkan, atau `english-comments: ignore` pada komen |
|
||||||
+44
-3
@@ -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)
|
||||||
|
|||||||
+1
-1
Submodule framework updated: 70fc558d22...408e3a5ace
@@ -0,0 +1,100 @@
|
|||||||
|
# Keeping source code comments in English
|
||||||
|
|
||||||
|
How a software engineer calls the shared workflow that checks that the comments
|
||||||
|
and docstrings of a pull request are written in English. The workflow fails the
|
||||||
|
run when one is not, and writes one comment on the pull request that mentions
|
||||||
|
the author. It changes no file and sends no source text outside the instance.
|
||||||
|
Its decision is ADR-0012 in this repository.
|
||||||
|
|
||||||
|
## Calling the workflow
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: English comments
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
english-comments:
|
||||||
|
uses: TirSystem/github-action/.gitea/workflows/english-comments.yml@v2
|
||||||
|
with:
|
||||||
|
exclude: "docs/generated/**"
|
||||||
|
allowed-words-file: .english-comments-allowed-words
|
||||||
|
```
|
||||||
|
|
||||||
|
| Input | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `scope` | `changed` | `changed` checks the comments on the lines the pull request adds; `all` checks every tracked file |
|
||||||
|
| `exclude` | | Comma list of file name or path patterns that are not checked (`*` and `**` are wildcards) |
|
||||||
|
| `allowed-words-file` | | A file of the repository with words that are never judged, one per line, `#` starts a comment |
|
||||||
|
| `threshold` | `0.6` | The probability, above 0 and at most 1, from which a comment counts as not English |
|
||||||
|
| `min-words` | `3` | A comment with fewer words is not judged |
|
||||||
|
| `langid-version`, `numpy-version` | `1.1.6`, `2.1.3` | The pinned packages the detector needs |
|
||||||
|
| `tools-ref` | the current major | The tag of this repository whose scripts are run; pin it together with the workflow |
|
||||||
|
|
||||||
|
The optional secret `REPORT_TOKEN` is a Gitea token that may comment on pull
|
||||||
|
requests. Without it the workflow uses the token of the run, so the comment is
|
||||||
|
written by the Actions bot. A pull request from a fork has a read-only token:
|
||||||
|
the check still fails the run, but no comment is written.
|
||||||
|
|
||||||
|
## What is checked
|
||||||
|
|
||||||
|
The comments and docstrings of the files of the languages in the code counter
|
||||||
|
([code metrics](code-metrics.md)): Python, shell, PowerShell, C, C++, C#, Java,
|
||||||
|
JavaScript, TypeScript, Go, Rust and SQL. Strings and code are not read.
|
||||||
|
Consecutive comment lines form a block. Each line of a block is judged on its own
|
||||||
|
(at least four words and a probability of at least 0.7), so an English line cannot
|
||||||
|
hide a Danish one next to it; when no line is flagged, the block is judged as a whole
|
||||||
|
(at least `min-words` words and `threshold`), so a sentence cut over short lines is not missed.
|
||||||
|
|
||||||
|
Before a block is judged, everything that is not a word is removed: paths, URLs,
|
||||||
|
options (`--output`), file names, identifiers (`snake_case`, `camelCase`),
|
||||||
|
numbers, acronyms and constants (`JSON`), a leading label such as `Usage:`, and
|
||||||
|
the words of the allowed-words file. A block with fewer than `min-words` words
|
||||||
|
left is not judged, so a short comment passes. Comments that configure a tool
|
||||||
|
(`noqa`, `type:`, `pragma`, `shellcheck`) are not read.
|
||||||
|
|
||||||
|
## When the check fails
|
||||||
|
|
||||||
|
The run fails, each finding is shown as an annotation (`::error file=...,line=...`)
|
||||||
|
and in the log as `file:line: looks Danish (0.93): "the comment"`, and one comment
|
||||||
|
on the pull request lists them and mentions the author. A new run updates that
|
||||||
|
comment; when the pull request passes, the comment is marked resolved and no
|
||||||
|
longer mentions anyone.
|
||||||
|
|
||||||
|
Fix it by rewriting the comment in English. For a quotation or a name that is
|
||||||
|
not English, either put `english-comments: ignore` in the comment (it skips the
|
||||||
|
whole comment block it is in), or add the word to the allowed-words file.
|
||||||
|
|
||||||
|
## Blocking the merge
|
||||||
|
|
||||||
|
A failing run does not block a merge by itself. In the branch protection of the
|
||||||
|
target branch, mark the check as required: run the workflow once on a pull
|
||||||
|
request and choose its entry in the list of status checks (the exact name is
|
||||||
|
made from the calling workflow, its job and the event). Setting branch
|
||||||
|
protection is the repository owner's action; the workflow does not change it.
|
||||||
|
|
||||||
|
## How accurate it is
|
||||||
|
|
||||||
|
The detector is `langid`, limited to English, Danish, Norwegian, Swedish,
|
||||||
|
German, French, Malay, Indonesian and Dutch. On the corpus of the repository it
|
||||||
|
flags none of 300 English comments, 44 of 45 Danish and 41 of 45 Malay comments
|
||||||
|
and all of the 48 German, Swedish, Norwegian and French ones (ADR-0012). Its
|
||||||
|
limits:
|
||||||
|
|
||||||
|
- a comment of one or two words, or one that is mostly names, is not judged;
|
||||||
|
- a comment in poor English passes, and a comment that mixes English technical
|
||||||
|
terms with few Danish or Malay words can pass;
|
||||||
|
- the Danish and Malay comments of the corpus were written for the test, not
|
||||||
|
collected from real code, so real code will show other misses. Report a miss
|
||||||
|
or a false positive with the comment, and it joins the corpus.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Message | Cause |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] give --base` | The workflow was started by an event that is not a pull request with `scope: changed`; call it on `pull_request` or use `scope: all` |
|
||||||
|
| `[configuration] git diff failed` | The base branch is not in the checkout; the workflow fetches the full history, so check that the pull request has a target branch |
|
||||||
|
| `[configuration] langid is not installed` | The install step failed; check the pinned versions |
|
||||||
|
| `[permissions] ... may not comment on pull requests` | The token cannot comment; give `REPORT_TOKEN` write access to issues and pull requests |
|
||||||
|
| A finding that is not a finding | Add the words to the allowed-words file, or `english-comments: ignore` to the comment |
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
# Reaching a tailnet with Tailscale
|
||||||
|
|
||||||
|
The composite action `actions/connect-tailscale` joins a Tailscale network from
|
||||||
|
a Gitea Actions runner, so the next steps of the same job (a deployment over
|
||||||
|
SSH, a migration) can reach hosts on the tailnet. It does what the GitHub
|
||||||
|
marketplace action `tailscale/github-action` does, written for Gitea Actions:
|
||||||
|
that action is built for GitHub and has not been verified on Gitea. It is
|
||||||
|
**experimental**: it is released in 2.2.0, and it has not yet run on a
|
||||||
|
runner with a real tailnet (MIL-012). For a plain tunnel without a third-party service, see
|
||||||
|
[WireGuard](vpn-wireguard.md).
|
||||||
|
|
||||||
|
A node belongs to one job: every job that deploys joins on its own and logs out
|
||||||
|
in a last step. It cannot be a reusable workflow, because a called workflow runs
|
||||||
|
as a separate job.
|
||||||
|
|
||||||
|
## Use it
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
|
- name: Connect Tailscale
|
||||||
|
uses: TirSystem/github-action/actions/connect-tailscale@v2
|
||||||
|
with:
|
||||||
|
oauth-secret: ${{ secrets.TS_OAUTH_SECRET }}
|
||||||
|
tags: tag:ci
|
||||||
|
version: 1.80.2
|
||||||
|
health-check-host: deploy-host
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: ssh deploy@deploy-host ./deploy.sh
|
||||||
|
|
||||||
|
- name: Disconnect Tailscale
|
||||||
|
if: always()
|
||||||
|
uses: TirSystem/github-action/actions/connect-tailscale@v2
|
||||||
|
with:
|
||||||
|
action: down
|
||||||
|
```
|
||||||
|
|
||||||
|
If the short form does not resolve on your instance (Gitea looks for the action
|
||||||
|
on the host set in `DEFAULT_ACTIONS_URL`), use the full address:
|
||||||
|
`https://git.tirsystem.com/TirSystem/github-action/actions/connect-tailscale@v2`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Input | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `action` | `up` | `up` or `down` |
|
||||||
|
| `authkey` | | A Tailscale auth key, as a secret. Give this or `oauth-secret` |
|
||||||
|
| `oauth-secret` | | The secret of an OAuth client with the scope `auth_keys`, as a secret. Needs `tags` |
|
||||||
|
| `tags` | | ACL tags of the node, `tag:ci` or `tag:ci,tag:deploy`. Required with `oauth-secret` |
|
||||||
|
| `hostname` | runner name | Name of the node in the tailnet |
|
||||||
|
| `version` | | The exact version to install when the runner has no `tailscale`, for example `1.80.2`. Empty uses the installed one and fails when there is none |
|
||||||
|
| `sha256` | | The SHA-256 of the downloaded archive `tailscale_<version>_<arch>.tgz`; the install fails when it differs. Recommended |
|
||||||
|
| `userspace-networking` | `false` | `true` runs without a TUN device |
|
||||||
|
| `args` | | Extra flags for `tailscale up`, for example `--accept-routes`. Never a secret |
|
||||||
|
| `timeout` | `60` | Seconds to wait for the node |
|
||||||
|
| `health-check-host` | | A tailnet host that must answer a Tailscale ping |
|
||||||
|
|
||||||
|
## Set it up
|
||||||
|
|
||||||
|
1. In the Tailscale admin console define a tag for CI (`tag:ci`) in the policy
|
||||||
|
file, with the owners that may use it, and an access rule that lets
|
||||||
|
`tag:ci` reach only the hosts and ports the deployment needs.
|
||||||
|
2. Create either an **OAuth client** with the scope `auth_keys` (write) for that
|
||||||
|
tag, or an **auth key** that is reusable only when needed, ephemeral,
|
||||||
|
pre-authorized and tagged. The OAuth client is preferred: it does not expire
|
||||||
|
the way a key does, and each run gets a fresh ephemeral key.
|
||||||
|
3. Store the secret as a secret of the organization, with a name that does not
|
||||||
|
start with `GITEA_` or `GITHUB_`.
|
||||||
|
4. Rotate the secret when a person with access leaves.
|
||||||
|
|
||||||
|
The state of the node is kept in memory. With an OAuth client the key is
|
||||||
|
ephemeral, so the node disappears from the tailnet when the job ends, even if
|
||||||
|
the last step does not run. With an auth key that is not ephemeral, a cancelled
|
||||||
|
job can leave the node in the tailnet until `down` runs or it is removed in the
|
||||||
|
admin console: create the key as ephemeral.
|
||||||
|
|
||||||
|
## The runner
|
||||||
|
|
||||||
|
- The job runs as root, or `sudo` is available (the action uses `sudo` only
|
||||||
|
when the job is not root). The action downloads the Tailscale binaries from
|
||||||
|
`pkgs.tailscale.com` when `tailscale` is not on the runner; `version` is then
|
||||||
|
required, and `sha256` makes the install check the archive. A runner image
|
||||||
|
that already contains Tailscale skips the download.
|
||||||
|
- The action starts its own `tailscaled`. It refuses to run when a `tailscaled`
|
||||||
|
already answers on the runner, because that daemon belongs to the host and
|
||||||
|
`down` would log it out: use the action on job containers, not on a runner
|
||||||
|
that runs jobs directly on a host with its own Tailscale.
|
||||||
|
- **TUN mode** (the default) needs the capability `NET_ADMIN` and the device
|
||||||
|
`/dev/net/tun` in the runner container: in the `act_runner` configuration set
|
||||||
|
under `container:` `options: "--cap-add=NET_ADMIN --device=/dev/net/tun"`.
|
||||||
|
- **Userspace mode** (`userspace-networking: true`) needs neither, but the
|
||||||
|
tailnet is then reached only through a SOCKS5 proxy on `localhost:1055`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -o ProxyCommand="nc -X 5 -x localhost:1055 %h %p" deploy@deploy-host
|
||||||
|
```
|
||||||
|
|
||||||
|
Tools that cannot use a proxy do not work in this mode.
|
||||||
|
- Use a dedicated runner with a label of its own (for example `vpn`) for jobs
|
||||||
|
that join a tailnet.
|
||||||
|
- The daemon is started in the background of the "Run the Tailscale step". That
|
||||||
|
it stays alive for the following steps of the job has to be confirmed on the
|
||||||
|
runner in use; the first trial must do so.
|
||||||
|
|
||||||
|
## What the action does
|
||||||
|
|
||||||
|
- masks the secrets in the log first;
|
||||||
|
- passes the auth key to `tailscale up` in a file of mode 600 (`--auth-key=file:`),
|
||||||
|
never on a command line, and deletes the file at once;
|
||||||
|
- prints errors as `::error::[category] message`, with the categories
|
||||||
|
`configuration`, `environment` and `connection`;
|
||||||
|
- waits until `tailscaled` answers before `tailscale up` (up to 30 seconds);
|
||||||
|
- on `down` logs the node out, stops `tailscaled` and deletes its work folder,
|
||||||
|
and only when this action started that daemon.
|
||||||
|
|
||||||
|
The `file:` form of `--auth-key` needs a recent Tailscale; with an old pinned
|
||||||
|
version `tailscale up` reports an invalid key.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Message | Cause |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] give authkey or oauth-secret` | Neither was given, or both |
|
||||||
|
| `[configuration] tags is required` | `oauth-secret` without `tags` |
|
||||||
|
| `[environment] could not download` | The runner cannot reach `pkgs.tailscale.com`, or the version does not exist |
|
||||||
|
| `[connection] tailscale up failed` | A wrong or expired key, a tag the client may not use, or a missing TUN device (try `userspace-networking`); the last lines of `tailscaled` follow |
|
||||||
|
| `[connection] health-check-host ... does not answer` | The access rule does not let `tag:ci` reach the host |
|
||||||
@@ -0,0 +1,108 @@
|
|||||||
|
# Reaching a private network with WireGuard
|
||||||
|
|
||||||
|
The composite action `actions/connect-wireguard` brings a WireGuard tunnel up on
|
||||||
|
a Gitea Actions runner, so the next steps of the same job (a deployment over
|
||||||
|
SSH, a database migration) can reach hosts that are not on the internet. It is
|
||||||
|
the WireGuard counterpart of the Tailscale action. It is **experimental**: it
|
||||||
|
is released in 2.2.0, and it has not yet run on a runner with a real tunnel
|
||||||
|
(MIL-012).
|
||||||
|
|
||||||
|
A tunnel belongs to one job: every job that deploys brings its own tunnel up
|
||||||
|
and takes it down. It cannot be a reusable workflow, because a called workflow
|
||||||
|
runs as a separate job.
|
||||||
|
|
||||||
|
## Use it
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4.2.2
|
||||||
|
|
||||||
|
- name: Connect the VPN
|
||||||
|
uses: TirSystem/github-action/actions/connect-wireguard@v2
|
||||||
|
with:
|
||||||
|
config: ${{ secrets.WG_CONFIG }}
|
||||||
|
health-check-host: 10.8.0.1
|
||||||
|
|
||||||
|
- name: Deploy
|
||||||
|
run: ssh deploy@10.8.0.10 ./deploy.sh
|
||||||
|
|
||||||
|
- name: Disconnect the VPN
|
||||||
|
if: always()
|
||||||
|
uses: TirSystem/github-action/actions/connect-wireguard@v2
|
||||||
|
with:
|
||||||
|
action: down
|
||||||
|
```
|
||||||
|
|
||||||
|
If the short form does not resolve on your instance (Gitea looks for the action
|
||||||
|
on the host set in `DEFAULT_ACTIONS_URL`), use the full address:
|
||||||
|
`https://git.tirsystem.com/TirSystem/github-action/actions/connect-wireguard@v2`.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
| Input | Default | Meaning |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `action` | `up` | `up` or `down` |
|
||||||
|
| `config` | | A complete wg-quick file, as a secret. When set, the fields below are ignored |
|
||||||
|
| `private-key`, `address`, `peer-public-key`, `endpoint`, `allowed-ips` | | The fields of a configuration, all five needed without `config` |
|
||||||
|
| `preshared-key`, `dns` | | Optional |
|
||||||
|
| `persistent-keepalive` | `25` | Seconds |
|
||||||
|
| `interface` | `wg0` | Interface name |
|
||||||
|
| `health-check-host` | | A host behind the tunnel; the step fails when it does not answer a ping |
|
||||||
|
|
||||||
|
## Set it up
|
||||||
|
|
||||||
|
1. On the WireGuard server add one peer for CI, with its own key pair and an
|
||||||
|
address inside the tunnel (`10.8.0.2/32`). Let it reach only the hosts the
|
||||||
|
deployment needs.
|
||||||
|
2. Store the configuration (or the private key) as a secret of the
|
||||||
|
organization, never in the repository. Give the secret a name that does not
|
||||||
|
start with `GITEA_` or `GITHUB_`.
|
||||||
|
3. Set `allowed-ips` to the private network only. `0.0.0.0/0` sends all of the
|
||||||
|
job's traffic, including the checkout, through the tunnel.
|
||||||
|
4. Rotate the key when a person with access to the secret leaves.
|
||||||
|
|
||||||
|
## The runner must allow it
|
||||||
|
|
||||||
|
A tunnel needs a network interface, so the runner needs more than a plain job
|
||||||
|
container:
|
||||||
|
|
||||||
|
- the job runs as root, or `sudo` is available (the action uses `sudo` only
|
||||||
|
when the job is not root, and installs `wireguard-tools` with `apt` when it is
|
||||||
|
missing, so a Debian or Ubuntu image);
|
||||||
|
- the container has the capability `NET_ADMIN` and the device `/dev/net/tun`.
|
||||||
|
In the `act_runner` configuration set under `container:`
|
||||||
|
`options: "--cap-add=NET_ADMIN --device=/dev/net/tun"`;
|
||||||
|
- without the kernel module the action uses `wireguard-go` (user space), which
|
||||||
|
needs the same capability and device.
|
||||||
|
|
||||||
|
Use a dedicated runner with a label of its own (for example `vpn`) and put
|
||||||
|
`runs-on: vpn` on the jobs that need it, so the other jobs do not get the extra
|
||||||
|
privilege.
|
||||||
|
|
||||||
|
## What the action does
|
||||||
|
|
||||||
|
- masks the keys in the log before anything else (the values of `PrivateKey`
|
||||||
|
and `PresharedKey` of a full configuration, and the key inputs);
|
||||||
|
- refuses an input that holds a line break, because an extra line in the file
|
||||||
|
could add a `PostUp` command that runs as root;
|
||||||
|
- writes `/etc/wireguard/<interface>.conf` with mode 600 and brings it up with
|
||||||
|
`wg-quick` (through `env`, so the `wireguard-go` choice survives `sudo`); the
|
||||||
|
file is removed again when the interface does not come up;
|
||||||
|
- prints errors as `::error::[category] message`, with the categories
|
||||||
|
`configuration`, `environment` and `connection`;
|
||||||
|
- on `down` takes the interface down and deletes the file.
|
||||||
|
|
||||||
|
It never prints a key. Do not turn on shell tracing (`set -x`) in the steps
|
||||||
|
that follow.
|
||||||
|
|
||||||
|
## Troubleshooting
|
||||||
|
|
||||||
|
| Message | Cause |
|
||||||
|
| --- | --- |
|
||||||
|
| `[configuration] ... is empty` | Neither `config` nor all five fields were given |
|
||||||
|
| `[environment] ... NET_ADMIN and /dev/net/tun` | The runner container lacks the capability or the device |
|
||||||
|
| `[connection] wg-quick could not bring wg0 up` | A wrong key, endpoint or a firewall; run `wg show` on the server |
|
||||||
|
| `[connection] health-check-host ... does not answer` | The tunnel is up but the host is not in `allowed-ips`, or is blocked |
|
||||||
+120
@@ -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
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
pytest>=8,<10
|
pytest>=8,<10
|
||||||
PyYAML>=6,<7
|
PyYAML>=6,<7
|
||||||
coverage>=7,<8
|
coverage>=7,<8
|
||||||
|
shellcheck-py==0.10.0.1
|
||||||
|
shfmt-py==3.11.0.2
|
||||||
|
langid==1.1.6
|
||||||
|
numpy==2.1.3
|
||||||
|
|||||||
@@ -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,212 @@
|
|||||||
|
"""Offline tests for the composite action actions/connect-tailscale.
|
||||||
|
|
||||||
|
The shell script is run with stub tailscale and tailscaled commands (no sudo,
|
||||||
|
no network), so the arguments, the key file, the masking and the failure
|
||||||
|
messages are checked without touching the runner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from workflow_source import REPO_ROOT
|
||||||
|
|
||||||
|
ACTION_DIR = REPO_ROOT / "actions" / "connect-tailscale"
|
||||||
|
ACTION = ACTION_DIR / "action.yml"
|
||||||
|
SCRIPT = ACTION_DIR / "connect-tailscale.sh"
|
||||||
|
BASH = shutil.which("bash")
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(BASH is None, reason="bash is needed to run the action script")
|
||||||
|
|
||||||
|
AUTHKEY = "tskey-auth-SECRETSECRETSECRET"
|
||||||
|
OAUTH = "tskey-client-SECRETSECRETSECRET"
|
||||||
|
|
||||||
|
# the stub records its arguments and, for `up`, the content of the key file
|
||||||
|
TAILSCALE_STUB = """#!/usr/bin/env bash
|
||||||
|
echo "$*" >> "$STUB_LOG"
|
||||||
|
case "$1" in
|
||||||
|
up)
|
||||||
|
for a in "$@"; do
|
||||||
|
case "$a" in --auth-key=file:*) cat "${a#--auth-key=file:}" > "$STUB_KEY" ;; esac
|
||||||
|
done
|
||||||
|
exit "${UP_EXIT:-0}" ;;
|
||||||
|
ping) exit "${PING_EXIT:-0}" ;;
|
||||||
|
version)
|
||||||
|
# the daemon answers only after tailscaled was started, and after DAEMON_DELAY calls
|
||||||
|
[ -f "$STUB_DAEMON" ] || exit 1
|
||||||
|
n=$(( $(cat "$STUB_DAEMON") + 1 )); echo "$n" > "$STUB_DAEMON"
|
||||||
|
[ "$n" -gt "${DAEMON_DELAY:-0}" ] ;;
|
||||||
|
esac
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def document() -> dict:
|
||||||
|
return yaml.safe_load(ACTION.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def make_stub(path: Path, body: str) -> None:
|
||||||
|
path.write_text(body, encoding="utf-8")
|
||||||
|
path.chmod(path.stat().st_mode | stat.S_IXUSR)
|
||||||
|
|
||||||
|
|
||||||
|
def run(tmp_path: Path, command: str, **env: str) -> subprocess.CompletedProcess:
|
||||||
|
make_stub(tmp_path / "tailscale", TAILSCALE_STUB)
|
||||||
|
make_stub(
|
||||||
|
tmp_path / "tailscaled",
|
||||||
|
'#!/usr/bin/env bash\necho "$*" >> "$STUB_LOG"\necho 0 > "$STUB_DAEMON"\n',
|
||||||
|
)
|
||||||
|
full = {
|
||||||
|
"PATH": f"{tmp_path}:{os.environ['PATH']}",
|
||||||
|
"TS_SUDO": "",
|
||||||
|
"TS_STATE_DIR": str(tmp_path / "state"),
|
||||||
|
"STUB_LOG": str(tmp_path / "log"),
|
||||||
|
"STUB_KEY": str(tmp_path / "key"),
|
||||||
|
"STUB_DAEMON": str(tmp_path / "daemon"),
|
||||||
|
"TS_WAIT_INTERVAL": "0.05",
|
||||||
|
"TS_START_WAIT": "100",
|
||||||
|
**env,
|
||||||
|
}
|
||||||
|
return subprocess.run(
|
||||||
|
[BASH, str(SCRIPT), command], env=full, capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def log(tmp_path: Path) -> str:
|
||||||
|
return (tmp_path / "log").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefinition:
|
||||||
|
def test_is_a_composite_action(self, document):
|
||||||
|
assert document["runs"]["using"] == "composite"
|
||||||
|
|
||||||
|
def test_inputs_are_the_contract(self, document):
|
||||||
|
assert set(document["inputs"]) == {
|
||||||
|
"action", "authkey", "oauth-secret", "tags", "hostname", "version",
|
||||||
|
"sha256", "userspace-networking", "args", "timeout", "health-check-host",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_uses_gitea_contexts_only(self):
|
||||||
|
text = ACTION.read_text(encoding="utf-8")
|
||||||
|
assert "gitea.action_path" in text
|
||||||
|
assert "github." not in text.replace("tailscale/github-action", "").replace("GitHub", "")
|
||||||
|
|
||||||
|
def test_no_input_is_interpolated_into_a_run_line(self, document):
|
||||||
|
for step in document["runs"]["steps"]:
|
||||||
|
assert "inputs." not in step["run"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestUp:
|
||||||
|
def test_joins_with_an_auth_key_file_not_the_key_on_the_command_line(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_TAGS="tag:ci", TS_HOSTNAME="ci-1")
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
calls = log(tmp_path)
|
||||||
|
assert AUTHKEY not in calls
|
||||||
|
assert AUTHKEY not in result.stdout + result.stderr
|
||||||
|
assert (tmp_path / "key").read_text(encoding="utf-8") == AUTHKEY
|
||||||
|
assert "--advertise-tags=tag:ci" in calls
|
||||||
|
assert "--hostname=ci-1" in calls
|
||||||
|
assert not (tmp_path / "state" / "authkey").exists()
|
||||||
|
|
||||||
|
def test_an_oauth_secret_becomes_an_ephemeral_preauthorized_key(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_OAUTH_SECRET=OAUTH, TS_TAGS="tag:ci")
|
||||||
|
assert result.returncode == 0, result.stdout
|
||||||
|
assert (tmp_path / "key").read_text(encoding="utf-8") == f"{OAUTH}?ephemeral=true&preauthorized=true"
|
||||||
|
|
||||||
|
def test_userspace_networking_starts_the_daemon_without_a_tun_device(self, tmp_path):
|
||||||
|
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_USERSPACE="true")
|
||||||
|
assert "--tun=userspace-networking --socks5-server=localhost:1055" in log(tmp_path)
|
||||||
|
|
||||||
|
def test_extra_args_are_passed(self, tmp_path):
|
||||||
|
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_ARGS="--accept-routes --shields-up")
|
||||||
|
assert "--accept-routes --shields-up" in log(tmp_path)
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("env", "message"),
|
||||||
|
[
|
||||||
|
({}, "give authkey or oauth-secret"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_OAUTH_SECRET": OAUTH}, "not both"),
|
||||||
|
({"TS_OAUTH_SECRET": OAUTH}, "tags is required"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_TAGS": "ci"}, "tags must look like"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_TIMEOUT": "soon"}, "timeout"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "1.x"}, "version"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_VERSION": "latest"}, "version"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_SHA256": "abc"}, "sha256"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_USERSPACE": "yes"}, "userspace-networking"),
|
||||||
|
({"TS_AUTHKEY": AUTHKEY, "TS_HOSTNAME": "bad name"}, "hostname"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_bad_inputs_fail_before_anything_starts(self, tmp_path, env, message):
|
||||||
|
result = run(tmp_path, "up", **env)
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[configuration]" in result.stdout
|
||||||
|
assert message in result.stdout
|
||||||
|
assert not (tmp_path / "log").exists()
|
||||||
|
|
||||||
|
def test_a_failing_up_fails_and_deletes_the_key_file(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, UP_EXIT="1")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[connection]" in result.stdout
|
||||||
|
assert not (tmp_path / "state" / "authkey").exists()
|
||||||
|
assert AUTHKEY not in result.stdout
|
||||||
|
|
||||||
|
def test_a_failing_health_check_fails_the_step(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_HEALTH_CHECK_HOST="db", PING_EXIT="1")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "health-check-host" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestDaemon:
|
||||||
|
def test_waits_until_the_daemon_answers(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="5")
|
||||||
|
assert result.returncode == 0, result.stdout
|
||||||
|
assert "up --auth-key" in log(tmp_path)
|
||||||
|
|
||||||
|
def test_a_daemon_that_never_answers_fails_before_up(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, DAEMON_DELAY="1000", TS_START_WAIT="3")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[connection] tailscaled did not answer" in result.stdout
|
||||||
|
assert "up --auth-key" not in log(tmp_path)
|
||||||
|
|
||||||
|
def test_a_daemon_of_the_host_is_not_taken_over(self, tmp_path):
|
||||||
|
(tmp_path / "daemon").write_text("0", encoding="utf-8")
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[environment] a tailscaled already runs" in result.stdout
|
||||||
|
calls = log(tmp_path) # only the probe ran: no second daemon, no up
|
||||||
|
assert "--state=mem:" not in calls
|
||||||
|
assert "up --auth-key" not in calls
|
||||||
|
|
||||||
|
def test_down_leaves_a_daemon_it_did_not_start(self, tmp_path):
|
||||||
|
(tmp_path / "daemon").write_text("0", encoding="utf-8")
|
||||||
|
result = run(tmp_path, "down")
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "nothing to do" in result.stdout
|
||||||
|
assert not (tmp_path / "log").exists()
|
||||||
|
|
||||||
|
def test_without_tailscale_a_version_is_required(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", TS_AUTHKEY=AUTHKEY, TS_TAILSCALE="no-such-tailscale")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[configuration] tailscale is not on the runner; set version" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownAndMask:
|
||||||
|
def test_down_logs_out_and_removes_the_state(self, tmp_path):
|
||||||
|
run(tmp_path, "up", TS_AUTHKEY=AUTHKEY)
|
||||||
|
result = run(tmp_path, "down")
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "logout" in log(tmp_path)
|
||||||
|
assert not (tmp_path / "state").exists()
|
||||||
|
|
||||||
|
def test_mask_prints_every_secret(self, tmp_path):
|
||||||
|
result = run(tmp_path, "mask", TS_AUTHKEY=AUTHKEY, TS_OAUTH_SECRET=OAUTH)
|
||||||
|
assert f"::add-mask::{AUTHKEY}" in result.stdout
|
||||||
|
assert f"::add-mask::{OAUTH}" in result.stdout
|
||||||
|
|
||||||
|
def test_unknown_command_fails(self, tmp_path):
|
||||||
|
assert run(tmp_path, "sideways").returncode == 1
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
"""Offline tests for the composite action actions/connect-wireguard.
|
||||||
|
|
||||||
|
The shell script is run with stub commands (no sudo, no wg-quick, no network),
|
||||||
|
so the generated configuration, the masking and the failure messages are
|
||||||
|
checked without touching the runner.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import stat
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from workflow_source import REPO_ROOT
|
||||||
|
|
||||||
|
ACTION_DIR = REPO_ROOT / "actions" / "connect-wireguard"
|
||||||
|
ACTION = ACTION_DIR / "action.yml"
|
||||||
|
SCRIPT = ACTION_DIR / "connect-wireguard.sh"
|
||||||
|
BASH = shutil.which("bash")
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(BASH is None, reason="bash is needed to run the action script")
|
||||||
|
|
||||||
|
KEY = "PRIVATEKEYPRIVATEKEYPRIVATEKEYPRIVATEKEY00="
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def document() -> dict:
|
||||||
|
return yaml.safe_load(ACTION.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
def run(tmp_path: Path, command: str, **env: str) -> subprocess.CompletedProcess:
|
||||||
|
"""Run the script with a stub wg-quick that records its arguments."""
|
||||||
|
stub = tmp_path / "wg-quick"
|
||||||
|
stub.write_text('#!/usr/bin/env bash\necho "$*" >> "$STUB_LOG"\n', encoding="utf-8")
|
||||||
|
stub.chmod(stub.stat().st_mode | stat.S_IXUSR)
|
||||||
|
ping = tmp_path / "ping"
|
||||||
|
ping.write_text("#!/usr/bin/env bash\nexit ${PING_EXIT:-0}\n", encoding="utf-8")
|
||||||
|
ping.chmod(ping.stat().st_mode | stat.S_IXUSR)
|
||||||
|
(tmp_path / "etc").mkdir(exist_ok=True)
|
||||||
|
full = {
|
||||||
|
"PATH": os.environ["PATH"],
|
||||||
|
"WG_DIR": str(tmp_path / "etc"),
|
||||||
|
"WG_QUICK": str(stub),
|
||||||
|
"WG_PING": str(ping),
|
||||||
|
"WG_SUDO": "",
|
||||||
|
"STUB_LOG": str(tmp_path / "log"),
|
||||||
|
# pretend the kernel has WireGuard and the tools are present
|
||||||
|
"WG_INTERFACE": "wg0",
|
||||||
|
**env,
|
||||||
|
}
|
||||||
|
# `ip link add` fails in the test environment, so wireguard-go must exist
|
||||||
|
go = tmp_path / "wireguard-go"
|
||||||
|
go.write_text("#!/usr/bin/env bash\n", encoding="utf-8")
|
||||||
|
go.chmod(go.stat().st_mode | stat.S_IXUSR)
|
||||||
|
full["PATH"] = f"{tmp_path}:{full['PATH']}"
|
||||||
|
return subprocess.run(
|
||||||
|
[BASH, str(SCRIPT), command], env=full, capture_output=True, text=True, check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FIELDS = {
|
||||||
|
"WG_PRIVATE_KEY": KEY,
|
||||||
|
"WG_ADDRESS": "10.8.0.2/32",
|
||||||
|
"WG_PEER_PUBLIC_KEY": "PEERPUBLICKEY=",
|
||||||
|
"WG_ENDPOINT": "vpn.example.org:51820",
|
||||||
|
"WG_ALLOWED_IPS": "10.8.0.0/24",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestDefinition:
|
||||||
|
def test_is_a_composite_action(self, document):
|
||||||
|
assert document["runs"]["using"] == "composite"
|
||||||
|
|
||||||
|
def test_inputs_are_the_contract(self, document):
|
||||||
|
assert set(document["inputs"]) == {
|
||||||
|
"action", "config", "private-key", "address", "peer-public-key", "endpoint",
|
||||||
|
"allowed-ips", "preshared-key", "dns", "persistent-keepalive", "interface",
|
||||||
|
"health-check-host",
|
||||||
|
}
|
||||||
|
|
||||||
|
def test_uses_gitea_contexts_only(self):
|
||||||
|
text = ACTION.read_text(encoding="utf-8")
|
||||||
|
assert "gitea.action_path" in text
|
||||||
|
assert "github." not in text
|
||||||
|
|
||||||
|
def test_no_secret_is_interpolated_into_a_run_line(self, document):
|
||||||
|
for step in document["runs"]["steps"]:
|
||||||
|
assert "inputs." not in step["run"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestUp:
|
||||||
|
def test_writes_a_private_config_and_brings_the_interface_up(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", **FIELDS)
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
conf = tmp_path / "etc" / "wg0.conf"
|
||||||
|
text = conf.read_text(encoding="utf-8")
|
||||||
|
assert f"PrivateKey = {KEY}" in text
|
||||||
|
assert "AllowedIPs = 10.8.0.0/24" in text
|
||||||
|
assert "PersistentKeepalive = 25" in text
|
||||||
|
if os.name != "nt": # Windows has no POSIX file modes
|
||||||
|
assert stat.S_IMODE(conf.stat().st_mode) == 0o600
|
||||||
|
assert (tmp_path / "log").read_text(encoding="utf-8").strip() == "up wg0"
|
||||||
|
|
||||||
|
def test_a_full_config_is_used_as_given(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", WG_CONFIG="[Interface]\nPrivateKey = abc\n")
|
||||||
|
assert result.returncode == 0, result.stdout
|
||||||
|
assert (tmp_path / "etc" / "wg0.conf").read_text(encoding="utf-8").startswith("[Interface]")
|
||||||
|
|
||||||
|
def test_the_key_is_never_printed(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", **FIELDS)
|
||||||
|
assert KEY not in result.stdout + result.stderr
|
||||||
|
|
||||||
|
def test_a_missing_field_fails_with_its_name(self, tmp_path):
|
||||||
|
fields = {k: v for k, v in FIELDS.items() if k != "WG_ENDPOINT"}
|
||||||
|
result = run(tmp_path, "up", **fields)
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[configuration]" in result.stdout
|
||||||
|
assert "WG_ENDPOINT" in result.stdout
|
||||||
|
|
||||||
|
def test_a_bad_interface_name_fails(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", **{**FIELDS, "WG_INTERFACE": "../etc"})
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "interface" in result.stdout
|
||||||
|
|
||||||
|
def test_a_failing_health_check_fails_the_step(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", **FIELDS, WG_HEALTH_CHECK_HOST="10.8.0.1", PING_EXIT="1")
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[connection]" in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def make_exec(path: Path, body: str) -> Path:
|
||||||
|
path.write_text(body, encoding="utf-8")
|
||||||
|
path.chmod(path.stat().st_mode | stat.S_IXUSR)
|
||||||
|
return path
|
||||||
|
|
||||||
|
|
||||||
|
class TestEnvironment:
|
||||||
|
def test_the_userspace_variable_reaches_wg_quick_through_sudo(self, tmp_path):
|
||||||
|
# a sudo that resets the environment, as the real one does
|
||||||
|
sudo = make_exec(
|
||||||
|
tmp_path / "sudo-reset",
|
||||||
|
'#!/usr/bin/env bash\nexec env -i "PATH=$PATH" "STUB_LOG=$STUB_LOG" "$@"\n',
|
||||||
|
)
|
||||||
|
quick = make_exec(
|
||||||
|
tmp_path / "quick-env",
|
||||||
|
'#!/usr/bin/env bash\necho "$WG_QUICK_USERSPACE_IMPLEMENTATION $*" >> "$STUB_LOG"\n',
|
||||||
|
)
|
||||||
|
result = run(tmp_path, "up", **FIELDS, WG_SUDO=str(sudo), WG_QUICK=str(quick))
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
assert "wireguard-go up wg0" in (tmp_path / "log").read_text(encoding="utf-8")
|
||||||
|
|
||||||
|
def test_a_field_with_a_line_break_is_refused(self, tmp_path):
|
||||||
|
result = run(tmp_path, "up", **{**FIELDS, "WG_ALLOWED_IPS": "10.8.0.0/24\nPostUp = id"})
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "WG_ALLOWED_IPS must be one line" in result.stdout
|
||||||
|
assert not (tmp_path / "etc" / "wg0.conf").exists()
|
||||||
|
|
||||||
|
def test_the_key_file_is_removed_when_wg_quick_fails(self, tmp_path):
|
||||||
|
failing = make_exec(tmp_path / "quick-fail", "#!/usr/bin/env bash\nexit 1\n")
|
||||||
|
result = run(tmp_path, "up", **FIELDS, WG_QUICK=str(failing))
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert not (tmp_path / "etc" / "wg0.conf").exists()
|
||||||
|
|
||||||
|
|
||||||
|
class TestDownAndMask:
|
||||||
|
def test_down_removes_the_config(self, tmp_path):
|
||||||
|
run(tmp_path, "up", **FIELDS)
|
||||||
|
result = run(tmp_path, "down")
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert not (tmp_path / "etc" / "wg0.conf").exists()
|
||||||
|
|
||||||
|
def test_mask_prints_every_secret(self, tmp_path):
|
||||||
|
result = run(tmp_path, "mask", WG_PRIVATE_KEY=KEY, WG_CONFIG="[Interface]\nPrivateKey = abc")
|
||||||
|
assert f"::add-mask::{KEY}" in result.stdout
|
||||||
|
assert "::add-mask::abc" in result.stdout
|
||||||
|
assert "[Interface]" not in result.stdout # only the key values are masked
|
||||||
|
|
||||||
|
def test_unknown_command_fails(self, tmp_path):
|
||||||
|
assert run(tmp_path, "sideways").returncode == 1
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
"""Offline tests for tools/english_comments.py (MIL-013, ADR-0012).
|
||||||
|
|
||||||
|
The detector is measured on the corpus in tests/fixtures/comments/; the rates
|
||||||
|
of criterion 1 of MIL-013 are tests, so a change that lowers them fails.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from workflow_source import REPO_ROOT, load_script
|
||||||
|
|
||||||
|
ec = load_script(REPO_ROOT / "tools" / "english_comments.py", "english_comments")
|
||||||
|
CORPUS = REPO_ROOT / "tests" / "fixtures" / "comments"
|
||||||
|
GIT = shutil.which("git")
|
||||||
|
|
||||||
|
|
||||||
|
def corpus() -> list[tuple[str, str]]:
|
||||||
|
rows: list[tuple[str, str]] = []
|
||||||
|
for name in ("english.tsv", "foreign.tsv"):
|
||||||
|
with (CORPUS / name).open(encoding="utf-8", newline="") as handle:
|
||||||
|
reader = csv.DictReader(handle, delimiter="\t", quoting=csv.QUOTE_NONE)
|
||||||
|
rows += [(row["lang"], row["text"]) for row in reader]
|
||||||
|
return rows
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def results():
|
||||||
|
found: dict[str, list[int]] = {}
|
||||||
|
for language, text in corpus():
|
||||||
|
counts = found.setdefault(language, [0, 0])
|
||||||
|
counts[1] += 1
|
||||||
|
counts[0] += ec.judge(text) is not None
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
class TestCorpusRates:
|
||||||
|
"""Criterion 1 of MIL-013: recall of at least 90 % for Danish and Malay, and at most
|
||||||
|
one false positive per 200 English comments."""
|
||||||
|
|
||||||
|
def test_the_corpus_has_the_size_the_milestone_asks_for(self, results):
|
||||||
|
assert results["en"][1] >= 200
|
||||||
|
assert results["da"][1] >= 40
|
||||||
|
assert results["ms"][1] >= 40
|
||||||
|
assert sum(v[1] for k, v in results.items() if k not in ("en", "da", "ms")) >= 40
|
||||||
|
|
||||||
|
def test_english_comments_are_not_flagged(self, results):
|
||||||
|
flagged, total = results["en"]
|
||||||
|
assert flagged <= total // 200
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("language", ["da", "ms"])
|
||||||
|
def test_danish_and_malay_are_found(self, results, language):
|
||||||
|
flagged, total = results[language]
|
||||||
|
assert flagged / total >= 0.90
|
||||||
|
|
||||||
|
def test_other_languages_are_found(self, results):
|
||||||
|
for language in ("de", "sv", "no", "fr"):
|
||||||
|
flagged, total = results[language]
|
||||||
|
assert flagged / total >= 0.80
|
||||||
|
|
||||||
|
|
||||||
|
class TestWords:
|
||||||
|
def test_paths_options_identifiers_and_numbers_are_not_words(self):
|
||||||
|
text = "Run tools/badges.py --metrics code-metrics.json with snake_case camelCase v2 DIR"
|
||||||
|
assert ec.words(text) == ["Run", "with"]
|
||||||
|
|
||||||
|
def test_json_and_code_characters_are_not_words(self):
|
||||||
|
assert ec.words('{"files": 3, "lines": 120} x = 1 <a|b>') == []
|
||||||
|
|
||||||
|
def test_a_label_and_an_acronym_are_dropped(self):
|
||||||
|
assert ec.words("Usage: lint the JSON files") == ["lint", "the", "files"]
|
||||||
|
|
||||||
|
def test_a_hyphenated_word_is_split_and_allowed_words_are_removed(self):
|
||||||
|
assert ec.words("read-only access", frozenset({"access"})) == ["read", "only"]
|
||||||
|
|
||||||
|
def test_a_hidden_file_is_not_a_word_but_an_ellipsis_is_kept(self):
|
||||||
|
assert ec.words("needs a .clang-format file...") == ["needs", "file"]
|
||||||
|
|
||||||
|
def test_quotes_inside_a_word_are_kept_once(self):
|
||||||
|
assert ec.words("don't can't won''t") == ["don't", "can't"]
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtract:
|
||||||
|
def test_python_comments_and_docstrings(self):
|
||||||
|
source = '"""Module line one.\n\nSecond paragraph."""\n\n# a comment\nx = 1 # trailing\n\ndef f():\n """Doc of f."""\n'
|
||||||
|
found = dict(ec.extract("a.py", source))
|
||||||
|
assert found[1] == "Module line one."
|
||||||
|
assert found[5] == "a comment"
|
||||||
|
assert found[6] == "trailing"
|
||||||
|
assert found[9] == "Doc of f."
|
||||||
|
|
||||||
|
def test_python_that_does_not_parse_falls_back_to_the_scanner(self):
|
||||||
|
assert dict(ec.extract("a.py", "def (:\n# still a comment\n")) == {2: "still a comment"}
|
||||||
|
|
||||||
|
def test_shell_and_sql_line_comments(self):
|
||||||
|
assert ec.extract("a.sh", "#!/bin/sh\n# real comment\necho '# not'\n") == [(2, "real comment")]
|
||||||
|
assert ec.extract("a.sql", "SELECT 1; -- one row\n") == [(1, "one row")]
|
||||||
|
|
||||||
|
def test_c_block_and_line_comments_lose_their_decoration(self):
|
||||||
|
source = "/**\n * First line\n * second line\n */\nint x; // trailing\n/// doc\n"
|
||||||
|
assert ec.extract("a.c", source) == [(2, "First line"), (3, "second line"), (5, "trailing"), (6, "doc")]
|
||||||
|
|
||||||
|
def test_a_comment_inside_a_string_is_not_a_comment(self):
|
||||||
|
assert ec.extract("a.c", 'char *s = "// not a comment";\n') == []
|
||||||
|
|
||||||
|
def test_tool_pragmas_are_not_prose(self):
|
||||||
|
source = "x = 1 # noqa: E501\n# type: ignore\n# pylint: disable=x\n# a real comment\n"
|
||||||
|
assert ec.extract("a.py", source) == [(4, "a real comment")]
|
||||||
|
|
||||||
|
def test_a_file_of_an_unknown_language_has_no_comments(self):
|
||||||
|
assert ec.extract("notes.md", "# Heading\n") == []
|
||||||
|
|
||||||
|
def test_consecutive_lines_are_one_group(self):
|
||||||
|
lines = [(1, "a"), (2, "b"), (4, "c"), (5, "d"), (5, "e")]
|
||||||
|
assert ec.groups(lines) == [[(1, "a"), (2, "b")], [(4, "c"), (5, "d"), (5, "e")]]
|
||||||
|
|
||||||
|
|
||||||
|
class TestCheckText:
|
||||||
|
DANISH = "# Denne funktion beregner totalen for kurven og gemmer resultatet\n"
|
||||||
|
|
||||||
|
def test_a_danish_comment_is_a_finding_with_its_line(self):
|
||||||
|
findings = ec.check_text("a.py", "x = 1\n" + self.DANISH)
|
||||||
|
assert [(f.path, f.line, f.language) for f in findings] == [("a.py", 2, "da")]
|
||||||
|
|
||||||
|
def test_an_english_comment_is_not(self):
|
||||||
|
assert ec.check_text("a.py", "# This function adds the totals of the basket together\n") == []
|
||||||
|
|
||||||
|
def test_only_the_blocks_on_the_given_lines_are_checked(self):
|
||||||
|
text = self.DANISH + "x = 1\n" + self.DANISH
|
||||||
|
assert [f.line for f in ec.check_text("a.py", text, {3})] == [3]
|
||||||
|
assert ec.check_text("a.py", text, {2}) == []
|
||||||
|
|
||||||
|
def test_a_danish_line_next_to_an_english_line_is_still_found(self):
|
||||||
|
english = "# This function adds the totals of the basket together and returns the sum\n"
|
||||||
|
findings = ec.check_text("a.py", english + self.DANISH)
|
||||||
|
assert [(f.line, f.end_line, f.language) for f in findings] == [(2, 2, "da")]
|
||||||
|
|
||||||
|
def test_a_sentence_cut_over_short_lines_is_judged_as_a_whole(self):
|
||||||
|
text = "# Denne funktion\n# beregner totalen\n# for kurven\n# og gemmer resultatet\n"
|
||||||
|
[finding] = ec.check_text("a.py", text)
|
||||||
|
assert (finding.line, finding.end_line, finding.language) == (1, 4, "da")
|
||||||
|
|
||||||
|
def test_only_a_flagged_line_is_reported_when_the_block_is_mixed(self):
|
||||||
|
text = self.DANISH + "# and this line is plain English text about the totals\n"
|
||||||
|
assert [f.line for f in ec.check_text("a.py", text, {2})] == []
|
||||||
|
|
||||||
|
def test_the_marker_skips_the_block(self):
|
||||||
|
text = self.DANISH.rstrip() + " # english-comments: ignore\n"
|
||||||
|
assert ec.check_text("a.py", text) == []
|
||||||
|
|
||||||
|
def test_allowed_words_are_not_judged(self):
|
||||||
|
allowed = frozenset(self.DANISH.lower().replace("#", "").replace(",", "").split())
|
||||||
|
assert ec.check_text("a.py", self.DANISH, allowed=allowed) == []
|
||||||
|
|
||||||
|
def test_a_short_comment_is_not_judged(self):
|
||||||
|
assert ec.check_text("a.py", "# ret fejlen\n") == []
|
||||||
|
|
||||||
|
def test_json_in_a_docstring_is_not_french(self):
|
||||||
|
source = '"""Count.\n\n {"files": 3, "lines": 120, "comment": 14, "blank": 22,\n "languages": {"Python": {"files": 2, "lines": 100}}}\n"""\n'
|
||||||
|
assert ec.check_text("a.py", source) == []
|
||||||
|
|
||||||
|
def test_usage_lines_with_options_are_not_judged(self):
|
||||||
|
text = "# Usage: lint-action-scripts.sh [<directory>] (default: actions)\n"
|
||||||
|
assert ec.check_text("a.sh", text) == []
|
||||||
|
|
||||||
|
def test_describe_names_the_language_and_cuts_a_long_text(self):
|
||||||
|
finding = ec.Finding("a.py", 1, 1, "da", 0.93, "x " * 100)
|
||||||
|
message = ec.describe(finding)
|
||||||
|
assert message.startswith('looks Danish (0.93): "') and message.endswith('..."')
|
||||||
|
|
||||||
|
|
||||||
|
class TestAllowedWords:
|
||||||
|
def test_words_one_per_line_with_comments(self, tmp_path):
|
||||||
|
file = tmp_path / "allowed.txt"
|
||||||
|
file.write_text("Kubernetes\n# a comment\nSkat # a Danish tax name\n\n", encoding="utf-8")
|
||||||
|
assert ec.read_allowed_words(str(file)) == {"kubernetes", "skat"}
|
||||||
|
|
||||||
|
def test_no_file_named_is_no_words(self):
|
||||||
|
assert ec.read_allowed_words(None) == frozenset()
|
||||||
|
|
||||||
|
def test_an_unreadable_file_is_an_error(self, tmp_path):
|
||||||
|
with pytest.raises(ec.CheckError):
|
||||||
|
ec.read_allowed_words(str(tmp_path / "missing.txt"))
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(directory: Path, *args: str) -> None:
|
||||||
|
subprocess.run(["git", *args], cwd=directory, check=True, capture_output=True)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def repo(tmp_path):
|
||||||
|
"""A repository with a base commit on main and a branch that adds comments."""
|
||||||
|
if GIT is None:
|
||||||
|
pytest.skip("git is needed")
|
||||||
|
run_git(tmp_path, "init", "-q", "-b", "main")
|
||||||
|
run_git(tmp_path, "config", "user.email", "t@example.org")
|
||||||
|
run_git(tmp_path, "config", "user.name", "t")
|
||||||
|
run_git(tmp_path, "config", "commit.gpgsign", "false")
|
||||||
|
(tmp_path / "old.py").write_bytes(b"# Denne gamle funktion beregner totalen for kurven\nx = 1\n")
|
||||||
|
run_git(tmp_path, "add", ".")
|
||||||
|
run_git(tmp_path, "commit", "-q", "-m", "base")
|
||||||
|
run_git(tmp_path, "checkout", "-q", "-b", "feature")
|
||||||
|
return tmp_path
|
||||||
|
|
||||||
|
|
||||||
|
def commit_file(repo: Path, name: str, content: bytes) -> None:
|
||||||
|
(repo / name).write_bytes(content)
|
||||||
|
run_git(repo, "add", ".")
|
||||||
|
run_git(repo, "commit", "-q", "-m", name)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRepository:
|
||||||
|
def test_only_the_comments_a_branch_adds_are_checked(self, repo, capsys):
|
||||||
|
commit_file(repo, "new.py", b"# Denne funktion beregner totalen for kurven\n")
|
||||||
|
code = ec.main(["check", "--directory", str(repo), "--base", "main", "--output", str(repo / "f.json")])
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert code == 1
|
||||||
|
assert "::error file=new.py,line=1::[english-comments] looks Danish" in out
|
||||||
|
assert "old.py" not in out # the old Danish comment is not this branch's
|
||||||
|
saved = json.loads((repo / "f.json").read_text(encoding="utf-8"))
|
||||||
|
assert [(f["path"], f["line"], f["language"]) for f in saved] == [("new.py", 1, "da")]
|
||||||
|
|
||||||
|
def test_an_added_line_in_an_old_file_is_checked_and_the_old_ones_are_not(self, repo):
|
||||||
|
commit_file(repo, "old.py", b"# Denne gamle funktion beregner totalen for kurven\nx = 1\n# Hold the lock while the file is written\n")
|
||||||
|
findings, files = ec.check_repository(repo, "main", False, [], frozenset(), 0.6, 3)
|
||||||
|
assert findings == [] and files == 1
|
||||||
|
|
||||||
|
def test_a_clean_branch_exits_zero(self, repo, capsys):
|
||||||
|
commit_file(repo, "new.py", b"# This function adds the totals of the basket together\n")
|
||||||
|
assert ec.main(["check", "--directory", str(repo), "--base", "main"]) == 0
|
||||||
|
assert "All comments are in English: 1 file(s) checked." in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_scope_all_checks_every_tracked_file(self, repo, capsys):
|
||||||
|
assert ec.main(["check", "--directory", str(repo), "--scope", "all"]) == 1
|
||||||
|
assert "old.py:1" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_excluded_binary_unknown_and_large_files_are_skipped(self, repo, monkeypatch):
|
||||||
|
danish = b"# Denne funktion beregner totalen for kurven og gemmer resultatet\n"
|
||||||
|
(repo / "vendor").mkdir()
|
||||||
|
commit_file(repo, "vendor/skip.py", danish)
|
||||||
|
commit_file(repo, "skipme.py", danish)
|
||||||
|
commit_file(repo, "bin.py", danish + b"\x00")
|
||||||
|
commit_file(repo, "notes.md", b"# Denne funktion beregner totalen for kurven\n")
|
||||||
|
monkeypatch.setattr(ec, "MAX_FILE_BYTES", 10)
|
||||||
|
commit_file(repo, "big.py", danish)
|
||||||
|
findings, files = ec.check_repository(repo, "main", False, ["skipme.py"], frozenset(), 0.6, 3)
|
||||||
|
assert (findings, files) == ([], 0)
|
||||||
|
|
||||||
|
def test_a_deleted_file_in_the_selection_is_skipped(self, repo):
|
||||||
|
commit_file(repo, "gone.py", b"x = 1\n")
|
||||||
|
(repo / "gone.py").unlink()
|
||||||
|
findings, files = ec.check_repository(repo, "main", False, [], frozenset(), 0.6, 3)
|
||||||
|
assert findings == []
|
||||||
|
|
||||||
|
def test_changed_lines_reads_every_hunk(self, repo):
|
||||||
|
commit_file(repo, "h.py", b"a\nb\nc\nd\n")
|
||||||
|
commit_file(repo, "h.py", b"a\nB\nc\nD\nE\n")
|
||||||
|
assert ec.changed_lines(repo, "main")["h.py"] == {1, 2, 3, 4, 5}
|
||||||
|
|
||||||
|
|
||||||
|
class TestMain:
|
||||||
|
def test_a_missing_base_is_a_configuration_error(self, repo, capsys):
|
||||||
|
assert ec.main(["check", "--directory", str(repo)]) == 2
|
||||||
|
assert "::error::[configuration] give --base" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_an_unknown_base_is_a_configuration_error(self, repo, capsys):
|
||||||
|
assert ec.main(["check", "--directory", str(repo), "--base", "nope"]) == 2
|
||||||
|
assert "failed: fatal" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_a_missing_directory_is_a_configuration_error(self, tmp_path, capsys):
|
||||||
|
assert ec.main(["check", "--directory", str(tmp_path / "x"), "--scope", "all"]) == 2
|
||||||
|
assert "does not exist" in capsys.readouterr().out
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("flags", [["--threshold", "0"], ["--threshold", "1.5"], ["--min-words", "0"]])
|
||||||
|
def test_bad_numbers_are_a_configuration_error(self, tmp_path, capsys, flags):
|
||||||
|
assert ec.main(["check", "--directory", str(tmp_path), "--scope", "all", *flags]) == 2
|
||||||
|
assert "threshold" in capsys.readouterr().out
|
||||||
|
|
||||||
|
def test_langid_missing_is_a_configuration_error(self, repo, monkeypatch, capsys):
|
||||||
|
commit_file(repo, "new.py", b"# Denne funktion beregner totalen for kurven og gemmer resultatet\n")
|
||||||
|
monkeypatch.setattr(ec, "_identifier", None)
|
||||||
|
monkeypatch.setitem(__import__("sys").modules, "langid.langid", None)
|
||||||
|
assert ec.main(["check", "--directory", str(repo), "--base", "main"]) == 2
|
||||||
|
assert "langid is not installed" in capsys.readouterr().out
|
||||||
@@ -0,0 +1,288 @@
|
|||||||
|
"""Offline tests for tools/english_comments_report.py and .gitea/workflows/english-comments.yml.
|
||||||
|
|
||||||
|
The Gitea API is a fake that records the writes, so no host is contacted.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import urllib.error
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from workflow_source import REPO_ROOT, load_script
|
||||||
|
|
||||||
|
report = load_script(REPO_ROOT / "tools" / "english_comments_report.py", "english_comments_report")
|
||||||
|
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "english-comments.yml"
|
||||||
|
TOKEN = "t" * 40
|
||||||
|
FINDING = {
|
||||||
|
"path": "src/a.py",
|
||||||
|
"line": 4,
|
||||||
|
"end_line": 4,
|
||||||
|
"language": "da",
|
||||||
|
"probability": 0.93,
|
||||||
|
"text": "Denne funktion beregner totalen",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeGitea:
|
||||||
|
"""Lists the comments of one pull request and records the writes."""
|
||||||
|
|
||||||
|
def __init__(self, comments=()):
|
||||||
|
self.comments = list(comments)
|
||||||
|
self.writes = []
|
||||||
|
|
||||||
|
def __call__(self, url, method="GET", headers=None, body=None):
|
||||||
|
assert headers["Authorization"] == f"token {TOKEN}"
|
||||||
|
if method == "GET":
|
||||||
|
return self.comments
|
||||||
|
self.writes.append((method, url.split("/repos/o/r")[-1], body["body"]))
|
||||||
|
return {"id": 99}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def env(tmp_path, monkeypatch):
|
||||||
|
findings = tmp_path / "findings.json"
|
||||||
|
findings.write_text(json.dumps([FINDING]), encoding="utf-8")
|
||||||
|
values = {
|
||||||
|
"GITEA_API_URL": "https://git.example/api/v1/",
|
||||||
|
"SOURCE_REPOSITORY": "o/r",
|
||||||
|
"PR_NUMBER": "7",
|
||||||
|
"PR_AUTHOR": "anna",
|
||||||
|
"REPORT_TOKEN": TOKEN,
|
||||||
|
"FINDINGS_FILE": str(findings),
|
||||||
|
"RUN_URL": "https://git.example/o/r/actions/runs/5",
|
||||||
|
}
|
||||||
|
for key, value in values.items():
|
||||||
|
monkeypatch.setenv(key, value)
|
||||||
|
monkeypatch.setattr("builtins.print", lambda *a, **k: None)
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def write_findings(path, findings):
|
||||||
|
path.write_text(json.dumps(findings), encoding="utf-8")
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_finding_writes_one_comment_that_mentions_the_author(env):
|
||||||
|
gitea = FakeGitea()
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
[(method, path, body)] = gitea.writes
|
||||||
|
assert (method, path) == ("POST", "/issues/7/comments")
|
||||||
|
assert report.MARKER in body and body.startswith(report.MARKER)
|
||||||
|
assert "@anna" in body and "`src/a.py`" in body and "| 4 |" in body
|
||||||
|
assert "Denne funktion beregner totalen" in body
|
||||||
|
assert "[The run](https://git.example/o/r/actions/runs/5)" in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_second_run_updates_the_comment_instead_of_adding_one(env):
|
||||||
|
old = {"id": 11, "body": f"{report.MARKER}\nsomething older"}
|
||||||
|
gitea = FakeGitea([{"id": 3, "body": "hello"}, old])
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
[(method, path, _)] = gitea.writes
|
||||||
|
assert (method, path) == ("PATCH", "/issues/comments/11")
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unchanged_comment_is_not_written_again(env):
|
||||||
|
body = report.failing_body("anna", [FINDING], "https://git.example/o/r/actions/runs/5")
|
||||||
|
gitea = FakeGitea([{"id": 11, "body": body}])
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
assert gitea.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_passing_run_marks_the_comment_resolved_without_a_mention(env):
|
||||||
|
write_findings(env, [])
|
||||||
|
gitea = FakeGitea([{"id": 11, "body": f"{report.MARKER}\n@anna failing"}])
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
[(method, path, body)] = gitea.writes
|
||||||
|
assert (method, path) == ("PATCH", "/issues/comments/11")
|
||||||
|
assert "passes" in body and "@anna" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_passing_run_with_no_earlier_comment_writes_nothing(env):
|
||||||
|
write_findings(env, [])
|
||||||
|
gitea = FakeGitea()
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
assert gitea.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_comments_of_other_authors_are_not_touched(env):
|
||||||
|
gitea = FakeGitea([{"id": 3, "body": "no marker here"}])
|
||||||
|
report.main(request=gitea)
|
||||||
|
assert [w[0] for w in gitea.writes] == ["POST"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_all_pages_of_comments_are_searched(env):
|
||||||
|
page = [{"id": n, "body": "x"} for n in range(report.PAGE_SIZE)]
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def paged(url, method="GET", headers=None, body=None):
|
||||||
|
if method != "GET":
|
||||||
|
calls.append((method, url))
|
||||||
|
return None
|
||||||
|
number = int(url.rsplit("page=", 1)[1])
|
||||||
|
return page if number == 1 else [{"id": 900, "body": report.MARKER}]
|
||||||
|
|
||||||
|
assert report.main(request=paged) == 0
|
||||||
|
assert calls == [("PATCH", "https://git.example/api/v1/repos/o/r/issues/comments/900")]
|
||||||
|
|
||||||
|
|
||||||
|
def test_no_token_writes_nothing_and_does_not_fail(env, monkeypatch):
|
||||||
|
monkeypatch.setenv("REPORT_TOKEN", "")
|
||||||
|
gitea = FakeGitea()
|
||||||
|
assert report.main(request=gitea) == 0
|
||||||
|
assert gitea.writes == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_a_missing_findings_file_means_the_check_did_not_run(env, monkeypatch):
|
||||||
|
monkeypatch.setenv("FINDINGS_FILE", str(env.parent / "absent.json"))
|
||||||
|
assert report.main(request=FakeGitea()) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unreadable_findings_file_is_a_configuration_error(env, capsys):
|
||||||
|
env.write_text("not json", encoding="utf-8")
|
||||||
|
assert report.main(request=FakeGitea()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("name", ["GITEA_API_URL", "PR_NUMBER", "PR_AUTHOR"])
|
||||||
|
def test_a_missing_setting_is_a_configuration_error(env, monkeypatch, name):
|
||||||
|
monkeypatch.delenv(name)
|
||||||
|
assert report.main(request=FakeGitea()) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_text_in_a_comment_cannot_mention_someone_or_break_the_table():
|
||||||
|
body = report.failing_body("anna", [{**FINDING, "text": "ping @bob `x` | y\nz"}])
|
||||||
|
assert "@bob" not in body.replace("@bob", "")
|
||||||
|
assert "\\|" in body and "\n| " in body and "`x`" not in body
|
||||||
|
|
||||||
|
|
||||||
|
def test_long_lists_are_cut(env):
|
||||||
|
many = [{**FINDING, "line": n} for n in range(report.MAX_ROWS + 5)]
|
||||||
|
body = report.failing_body("anna", many)
|
||||||
|
assert "...and 5 more" in body
|
||||||
|
assert body.count("`src/a.py`") == report.MAX_ROWS
|
||||||
|
|
||||||
|
|
||||||
|
class FakeResponse(io.BytesIO):
|
||||||
|
def __enter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
def __exit__(self, *args):
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_json_sends_the_body_and_reads_the_answer(monkeypatch):
|
||||||
|
seen = {}
|
||||||
|
|
||||||
|
def fake_urlopen(request, timeout):
|
||||||
|
seen["method"] = request.get_method()
|
||||||
|
seen["data"] = request.data
|
||||||
|
return FakeResponse(b'{"id": 5}')
|
||||||
|
|
||||||
|
monkeypatch.setattr(report.urllib.request, "urlopen", fake_urlopen)
|
||||||
|
assert report.request_json("https://h/x", "POST", {"A": "b"}, {"body": "t"}) == {"id": 5}
|
||||||
|
assert seen == {"method": "POST", "data": b'{"body": "t"}'}
|
||||||
|
|
||||||
|
|
||||||
|
def test_request_json_returns_none_for_an_empty_answer(monkeypatch):
|
||||||
|
monkeypatch.setattr(report.urllib.request, "urlopen", lambda request, timeout: FakeResponse(b""))
|
||||||
|
assert report.request_json("https://h/x") is None
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("code", "category"), [(401, "credentials"), (403, "permissions"), (500, "api")]
|
||||||
|
)
|
||||||
|
def test_http_errors_have_a_category(monkeypatch, code, category):
|
||||||
|
def fail(request, timeout):
|
||||||
|
raise urllib.error.HTTPError(request.full_url, code, "reason", {}, None)
|
||||||
|
|
||||||
|
monkeypatch.setattr(report.urllib.request, "urlopen", fail)
|
||||||
|
with pytest.raises(report.ReportError, match=rf"\[{category}\]"):
|
||||||
|
report.request_json("https://h/x?token=secret")
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_unreachable_host_is_an_api_error(monkeypatch):
|
||||||
|
def fail(request, timeout):
|
||||||
|
raise urllib.error.URLError("down")
|
||||||
|
|
||||||
|
monkeypatch.setattr(report.urllib.request, "urlopen", fail)
|
||||||
|
with pytest.raises(report.ReportError, match=r"\[api\]"):
|
||||||
|
report.request_json("https://h/x")
|
||||||
|
|
||||||
|
|
||||||
|
def test_an_api_error_is_printed_without_the_token_and_fails_the_step(env, monkeypatch, capsys):
|
||||||
|
monkeypatch.undo() # restore print
|
||||||
|
for key, value in {
|
||||||
|
"GITEA_API_URL": "https://git.example/api/v1",
|
||||||
|
"SOURCE_REPOSITORY": "o/r",
|
||||||
|
"PR_NUMBER": "7",
|
||||||
|
"PR_AUTHOR": "anna",
|
||||||
|
"REPORT_TOKEN": TOKEN,
|
||||||
|
"FINDINGS_FILE": str(env),
|
||||||
|
}.items():
|
||||||
|
monkeypatch.setenv(key, value)
|
||||||
|
|
||||||
|
def refuse(url, method="GET", headers=None, body=None):
|
||||||
|
raise report.ReportError("permissions", "the token may not comment")
|
||||||
|
|
||||||
|
assert report.main(request=refuse) == 1
|
||||||
|
out = capsys.readouterr().out
|
||||||
|
assert "::error::[permissions]" in out and TOKEN not in out
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def document():
|
||||||
|
return yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkflow:
|
||||||
|
|
||||||
|
def test_is_a_reusable_workflow_with_the_contract_inputs(self, document):
|
||||||
|
call = document[True]["workflow_call"] # PyYAML reads `on` as True
|
||||||
|
assert set(call["inputs"]) == {
|
||||||
|
"scope", "exclude", "allowed-words-file", "threshold", "min-words",
|
||||||
|
"langid-version", "numpy-version", "tools-ref",
|
||||||
|
}
|
||||||
|
assert call["inputs"]["threshold"]["default"] == "0.6"
|
||||||
|
assert call["inputs"]["min-words"]["default"] == "3"
|
||||||
|
assert call["inputs"]["scope"]["default"] == "changed"
|
||||||
|
assert call["secrets"]["REPORT_TOKEN"]["required"] is False
|
||||||
|
|
||||||
|
def test_permissions_are_explicit_and_small(self, document):
|
||||||
|
permissions = document["jobs"]["english-comments"]["permissions"]
|
||||||
|
assert permissions == {"contents": "read", "issues": "write", "pull-requests": "write"}
|
||||||
|
|
||||||
|
def test_checks_out_the_full_history_for_the_merge_base(self, document):
|
||||||
|
first = document["jobs"]["english-comments"]["steps"][0]
|
||||||
|
assert first["with"]["fetch-depth"] == 0
|
||||||
|
|
||||||
|
def test_the_check_does_not_end_the_job_before_the_author_is_told(self, document):
|
||||||
|
steps = {s.get("name"): s for s in document["jobs"]["english-comments"]["steps"]}
|
||||||
|
assert steps["Check the comments"]["continue-on-error"] is True
|
||||||
|
tell = steps["Tell the author"]
|
||||||
|
assert tell["continue-on-error"] is True and "always()" in tell["if"]
|
||||||
|
assert "steps.check.outcome == 'failure'" in steps["Fail the run when a comment is not in English"]["if"]
|
||||||
|
|
||||||
|
def test_the_author_step_asks_for_the_pull_request_not_for_the_event_name(self, document):
|
||||||
|
# in a called workflow gitea.event_name is workflow_call (seen on the instance, MIL-013 task 8)
|
||||||
|
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
|
||||||
|
assert "event_name" not in tell["if"]
|
||||||
|
assert "gitea.event.pull_request.number != ''" in tell["if"]
|
||||||
|
|
||||||
|
def test_the_link_to_the_run_uses_the_run_id(self, document):
|
||||||
|
# the URL of a run on the instance holds its id, not its number (MIL-013 task 8)
|
||||||
|
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
|
||||||
|
assert tell["env"]["RUN_URL"].endswith("/actions/runs/${{ gitea.run_id }}")
|
||||||
|
|
||||||
|
def test_inputs_reach_the_scripts_as_environment_variables_only(self, document):
|
||||||
|
for step in document["jobs"]["english-comments"]["steps"]:
|
||||||
|
assert "inputs." not in step.get("run", "")
|
||||||
|
assert "secrets." not in step.get("run", "")
|
||||||
|
|
||||||
|
def test_the_token_falls_back_to_the_token_of_the_run(self, document):
|
||||||
|
tell = next(s for s in document["jobs"]["english-comments"]["steps"] if s.get("name") == "Tell the author")
|
||||||
|
assert "secrets.REPORT_TOKEN || gitea.token" in tell["env"]["REPORT_TOKEN"]
|
||||||
|
|
||||||
|
def test_uses_gitea_contexts_only(self):
|
||||||
|
text = WORKFLOW.read_text(encoding="utf-8")
|
||||||
|
assert "gitea.event.pull_request.user.login" in text
|
||||||
|
assert "github." not in text.replace("TirSystem/github-action", "")
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
"""Offline tests for tools/lint-action-scripts.sh (MIL-012 task 6).
|
||||||
|
|
||||||
|
The script runs shellcheck and shfmt on a folder; the tests give it a clean
|
||||||
|
script, a script with a shellcheck problem and a badly formatted one, so a
|
||||||
|
seeded error is shown to fail the run.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
from workflow_source import REPO_ROOT
|
||||||
|
|
||||||
|
SCRIPT = REPO_ROOT / "tools" / "lint-action-scripts.sh"
|
||||||
|
WORKFLOW = REPO_ROOT / ".gitea" / "workflows" / "validate-workflows.yml"
|
||||||
|
BASH = shutil.which("bash")
|
||||||
|
# the tools are in the virtual environment of the interpreter that runs the tests
|
||||||
|
SEARCH_PATH = os.pathsep.join([str(Path(sys.executable).parent), os.environ.get("PATH", "")])
|
||||||
|
SHELLCHECK = shutil.which("shellcheck", path=SEARCH_PATH)
|
||||||
|
SHFMT = shutil.which("shfmt", path=SEARCH_PATH)
|
||||||
|
HAVE_TOOLS = BASH and SHELLCHECK and SHFMT
|
||||||
|
|
||||||
|
CLEAN = '#!/usr/bin/env bash\nmain() {\n echo "$1"\n}\n\nmain "$@"\n'
|
||||||
|
SHELLCHECK_PROBLEM = '#!/usr/bin/env bash\nmain() {\n echo $1\n}\n\nmain "$@"\n'
|
||||||
|
BADLY_FORMATTED = '#!/usr/bin/env bash\nmain() {\n echo "$1"\n}\n\nmain "$@"\n'
|
||||||
|
|
||||||
|
|
||||||
|
def lint(folder: Path) -> subprocess.CompletedProcess:
|
||||||
|
# shfmt reads .editorconfig from the folder upwards; a temporary folder is
|
||||||
|
# outside this repository, so it gets a copy of the repository's file
|
||||||
|
if folder != REPO_ROOT / "actions":
|
||||||
|
shutil.copy(REPO_ROOT / ".editorconfig", folder / ".editorconfig")
|
||||||
|
env = {**os.environ, "SHELLCHECK": str(SHELLCHECK), "SHFMT": str(SHFMT)}
|
||||||
|
return subprocess.run(
|
||||||
|
[BASH, str(SCRIPT), str(folder)],
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False,
|
||||||
|
cwd=REPO_ROOT,
|
||||||
|
env=env,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.skipif(not HAVE_TOOLS, reason="bash, shellcheck and shfmt are needed")
|
||||||
|
class TestLint:
|
||||||
|
def test_a_clean_script_passes(self, tmp_path):
|
||||||
|
(tmp_path / "ok.sh").write_bytes(CLEAN.encode()) # LF on every platform
|
||||||
|
result = lint(tmp_path)
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
assert "clean" in result.stdout
|
||||||
|
|
||||||
|
def test_a_shellcheck_problem_fails(self, tmp_path):
|
||||||
|
(tmp_path / "bad.sh").write_bytes(SHELLCHECK_PROBLEM.encode()) # LF on every platform
|
||||||
|
result = lint(tmp_path)
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[lint] shellcheck" in result.stdout
|
||||||
|
|
||||||
|
def test_a_badly_formatted_script_fails(self, tmp_path):
|
||||||
|
(tmp_path / "ugly.sh").write_bytes(BADLY_FORMATTED.encode()) # LF on every platform
|
||||||
|
result = lint(tmp_path)
|
||||||
|
assert result.returncode == 1
|
||||||
|
assert "::error::[lint] shfmt" in result.stdout
|
||||||
|
|
||||||
|
def test_no_scripts_is_not_a_failure(self, tmp_path):
|
||||||
|
result = lint(tmp_path)
|
||||||
|
assert result.returncode == 0
|
||||||
|
assert "nothing was checked" in result.stdout
|
||||||
|
|
||||||
|
def test_the_scripts_of_the_actions_are_clean(self):
|
||||||
|
result = lint(REPO_ROOT / "actions")
|
||||||
|
assert result.returncode == 0, result.stdout + result.stderr
|
||||||
|
|
||||||
|
|
||||||
|
class TestWorkflow:
|
||||||
|
def test_validate_workflows_runs_the_lint_with_pinned_tools(self):
|
||||||
|
document = yaml.safe_load(WORKFLOW.read_text(encoding="utf-8"))
|
||||||
|
commands = [step.get("run", "") for step in document["jobs"]["validate"]["steps"]]
|
||||||
|
assert any("tools/lint-action-scripts.sh" in command for command in commands)
|
||||||
|
requirements = (REPO_ROOT / "tests" / "requirements.txt").read_text(encoding="utf-8")
|
||||||
|
assert "shellcheck-py==" in requirements
|
||||||
|
assert "shfmt-py==" in requirements
|
||||||
+25
-2
@@ -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,406 @@
|
|||||||
|
"""Find comments in source code that are not written in English (MIL-013, ADR-0012).
|
||||||
|
|
||||||
|
Run by .gitea/workflows/english-comments.yml. It reads the comments and docstrings
|
||||||
|
of the source files of the languages of ADR-0010 and judges each comment line
|
||||||
|
with ``langid`` (restricted to a few candidate languages) after removing
|
||||||
|
everything that is not a word: paths, options, identifiers, numbers and the words
|
||||||
|
of the allowed-words file. When no line of a block of consecutive comment lines
|
||||||
|
is flagged, the block is judged as a whole, so a sentence cut over short lines is
|
||||||
|
not missed. A text with fewer than ``--min-words`` words left is not judged.
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
python tools/english_comments.py check [--directory DIR] [--scope changed|all]
|
||||||
|
[--base REF] [--exclude PATTERNS] [--allowed-words FILE]
|
||||||
|
[--threshold P] [--min-words N] [--output FILE]
|
||||||
|
|
||||||
|
With ``--scope changed`` (the default) only the comments on lines that the
|
||||||
|
current branch added since ``--base`` (a git ref, for example ``origin/main``)
|
||||||
|
are checked, so a pull request is not blamed for a comment it did not write;
|
||||||
|
``--scope all`` checks every tracked file. A comment block that contains ``english-comments: ignore`` is
|
||||||
|
skipped (a quotation, a name).
|
||||||
|
|
||||||
|
Every finding is printed as ``::error file=PATH,line=N::...`` and as ``PATH:N:
|
||||||
|
looks Danish (0.93): "text"``; ``--output`` writes them as JSON for the
|
||||||
|
reporting step. Exit codes: 0 no finding, 1 at least one finding, 2 a setting
|
||||||
|
or a missing tool (printed as ``::error::[configuration] ...``).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import ast
|
||||||
|
import importlib.util
|
||||||
|
import io
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import tokenize
|
||||||
|
from dataclasses import asdict, dataclass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
DEFAULT_THRESHOLD = 0.6
|
||||||
|
DEFAULT_MIN_WORDS = 3
|
||||||
|
# a single line of a longer block is judged more strictly than a whole block (ADR-0012)
|
||||||
|
LINE_THRESHOLD = 0.7
|
||||||
|
LINE_MIN_WORDS = 4
|
||||||
|
MAX_FILE_BYTES = 1_000_000
|
||||||
|
CANDIDATES = ["en", "da", "no", "sv", "de", "fr", "ms", "id", "nl"]
|
||||||
|
LANGUAGE_NAMES = {
|
||||||
|
"da": "Danish",
|
||||||
|
"no": "Norwegian",
|
||||||
|
"sv": "Swedish",
|
||||||
|
"de": "German",
|
||||||
|
"fr": "French",
|
||||||
|
"ms": "Malay",
|
||||||
|
"id": "Indonesian",
|
||||||
|
"nl": "Dutch",
|
||||||
|
}
|
||||||
|
IGNORE_MARKER = re.compile(r"english-comments:\s*ignore", re.IGNORECASE)
|
||||||
|
# comments that configure a tool are not prose
|
||||||
|
_PRAGMA = re.compile(
|
||||||
|
r"^(noqa|type:|pragma|shellcheck|fmt:|pylint|ruff:|mypy:|pyright:|eslint|prettier|"
|
||||||
|
r"-\*-|vim:|coding[:=]|nosec|isort:|nolint|clang-format|cppcheck|lint:)",
|
||||||
|
re.IGNORECASE,
|
||||||
|
)
|
||||||
|
_WORD = re.compile(r"^[^\W\d_]+(?:'[^\W\d_]+)?$")
|
||||||
|
_NOT_WORD_CHARS = re.compile(r"[\d_/\{}\[\]\"`=<>|$@#%&+^~*]")
|
||||||
|
_HUNK = re.compile(r"^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@")
|
||||||
|
|
||||||
|
|
||||||
|
class CheckError(RuntimeError):
|
||||||
|
"""A setting or tool problem (exit code 2)."""
|
||||||
|
|
||||||
|
|
||||||
|
def _load_sibling(name: str):
|
||||||
|
path = Path(__file__).resolve().parent / f"{name}.py"
|
||||||
|
spec = importlib.util.spec_from_file_location(name, path)
|
||||||
|
assert spec is not None and spec.loader is not None
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
count_loc = _load_sibling("count_loc")
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class Finding:
|
||||||
|
"""A comment block that is probably not English."""
|
||||||
|
|
||||||
|
path: str
|
||||||
|
line: int
|
||||||
|
end_line: int
|
||||||
|
language: str
|
||||||
|
probability: float
|
||||||
|
text: str
|
||||||
|
|
||||||
|
|
||||||
|
def _clean(text: str) -> str:
|
||||||
|
"""Remove comment decoration (``*``, ``/``, ``#``, ``-``, ``=``) from both ends."""
|
||||||
|
text = re.sub(r"^[/*!#<>=\-\s]+", "", text.strip())
|
||||||
|
return re.sub(r"[*/\s]+$", "", text).strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _python_comments(source: str) -> list[tuple[int, str]]:
|
||||||
|
"""Comments (tokenizer) and docstring lines (ast) of a Python file."""
|
||||||
|
found: list[tuple[int, str]] = []
|
||||||
|
for token in tokenize.generate_tokens(io.StringIO(source).readline):
|
||||||
|
if token.type == tokenize.COMMENT:
|
||||||
|
found.append((token.start[0], _clean(token.string)))
|
||||||
|
for node in ast.walk(ast.parse(source)):
|
||||||
|
if isinstance(node, (ast.Module, ast.ClassDef, ast.FunctionDef, ast.AsyncFunctionDef)):
|
||||||
|
first = node.body[0] if node.body else None
|
||||||
|
if (
|
||||||
|
isinstance(first, ast.Expr)
|
||||||
|
and isinstance(first.value, ast.Constant)
|
||||||
|
and isinstance(first.value.value, str)
|
||||||
|
):
|
||||||
|
for offset, line in enumerate(first.value.value.splitlines()):
|
||||||
|
found.append((first.value.lineno + offset, _clean(line)))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def _scanned_comments(text: str, language) -> list[tuple[int, str]]:
|
||||||
|
"""Comments of any other language, from the scanner of the code counter."""
|
||||||
|
state = count_loc._State()
|
||||||
|
found: list[tuple[int, str]] = []
|
||||||
|
lines = text.replace("\r\n", "\n").replace("\r", "\n").split("\n")
|
||||||
|
for number, line in enumerate(lines, start=1):
|
||||||
|
pieces: list[str] = []
|
||||||
|
count_loc._scan_line(line, language, state, pieces)
|
||||||
|
if number == 1 and pieces and pieces[0].startswith("!"):
|
||||||
|
pieces = pieces[1:] # a shebang line is not a comment
|
||||||
|
joined = " ".join(_clean(piece) for piece in pieces).strip()
|
||||||
|
if joined:
|
||||||
|
found.append((number, joined))
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def extract(path: str, text: str) -> list[tuple[int, str]]:
|
||||||
|
"""Return (line, text) for every comment line of a source file, sorted by line.
|
||||||
|
|
||||||
|
A file of a language the code counter does not know has no comments here.
|
||||||
|
"""
|
||||||
|
language = count_loc.language_of(path)
|
||||||
|
if language is None:
|
||||||
|
return []
|
||||||
|
if language.name == "Python":
|
||||||
|
try:
|
||||||
|
found = _python_comments(text)
|
||||||
|
except (SyntaxError, tokenize.TokenError):
|
||||||
|
found = _scanned_comments(text, language)
|
||||||
|
else:
|
||||||
|
found = _scanned_comments(text, language)
|
||||||
|
return sorted((n, t) for n, t in found if t and not _PRAGMA.match(t))
|
||||||
|
|
||||||
|
|
||||||
|
def groups(lines: list[tuple[int, str]]) -> list[list[tuple[int, str]]]:
|
||||||
|
"""Group comment lines on consecutive line numbers: one group is one comment block."""
|
||||||
|
found: list[list[tuple[int, str]]] = []
|
||||||
|
for number, text in lines:
|
||||||
|
if found and found[-1][-1][0] in (number, number - 1):
|
||||||
|
found[-1].append((number, text))
|
||||||
|
else:
|
||||||
|
found.append([(number, text)])
|
||||||
|
return found
|
||||||
|
|
||||||
|
|
||||||
|
def words(text: str, allowed: frozenset[str] = frozenset()) -> list[str]:
|
||||||
|
"""The words of a comment: no URLs, paths, options, identifiers or allowed words."""
|
||||||
|
out: list[str] = []
|
||||||
|
tokens = text.split()
|
||||||
|
if tokens and tokens[0].endswith(":") and tokens[0][:-1].isalpha():
|
||||||
|
tokens = tokens[1:] # a label such as "Usage:" or "Note:" is not prose
|
||||||
|
for token in tokens:
|
||||||
|
if len(token) > 1 and token[0] == "." and token[1].isalpha():
|
||||||
|
continue # a dotfile, for example one of a linter
|
||||||
|
token = token.strip("().,;:!?")
|
||||||
|
# a token with code characters (quotes, braces, `=`, `/`, `_`, digits) is not a word
|
||||||
|
if not token or _NOT_WORD_CHARS.search(token) or "::" in token:
|
||||||
|
continue
|
||||||
|
if token.startswith("-") or "." in token or (len(token) > 1 and token.isupper()):
|
||||||
|
continue # an option, a file name, an acronym or a constant
|
||||||
|
if re.search(r"[a-z][A-Z]", token):
|
||||||
|
continue
|
||||||
|
for part in token.split("-"):
|
||||||
|
if len(part) > 1 and _WORD.match(part) and part.lower() not in allowed:
|
||||||
|
out.append(part)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
_identifier = None
|
||||||
|
|
||||||
|
|
||||||
|
def _identifier_model():
|
||||||
|
global _identifier
|
||||||
|
if _identifier is None:
|
||||||
|
try:
|
||||||
|
from langid.langid import LanguageIdentifier, model
|
||||||
|
except ImportError:
|
||||||
|
raise CheckError("langid is not installed (pip install langid==1.1.6).") from None
|
||||||
|
_identifier = LanguageIdentifier.from_modelstring(model, norm_probs=True)
|
||||||
|
_identifier.set_languages(CANDIDATES)
|
||||||
|
return _identifier
|
||||||
|
|
||||||
|
|
||||||
|
def judge(
|
||||||
|
text: str,
|
||||||
|
allowed: frozenset[str] = frozenset(),
|
||||||
|
threshold: float = DEFAULT_THRESHOLD,
|
||||||
|
min_words: int = DEFAULT_MIN_WORDS,
|
||||||
|
) -> tuple[str, float] | None:
|
||||||
|
"""Return (language code, probability) when the text is not English, else None."""
|
||||||
|
kept = words(text, allowed)
|
||||||
|
if len(kept) < min_words:
|
||||||
|
return None
|
||||||
|
code, probability = _identifier_model().classify(" ".join(kept))
|
||||||
|
if code != "en" and probability >= threshold:
|
||||||
|
return code, float(probability)
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def check_text(
|
||||||
|
path: str,
|
||||||
|
text: str,
|
||||||
|
lines: set[int] | None = None,
|
||||||
|
allowed: frozenset[str] = frozenset(),
|
||||||
|
threshold: float = DEFAULT_THRESHOLD,
|
||||||
|
min_words: int = DEFAULT_MIN_WORDS,
|
||||||
|
) -> list[Finding]:
|
||||||
|
"""Findings of one file; with ``lines`` only the blocks that touch those line numbers."""
|
||||||
|
findings: list[Finding] = []
|
||||||
|
for group in groups(extract(path, text)):
|
||||||
|
body = " ".join(line for _, line in group)
|
||||||
|
if IGNORE_MARKER.search(body):
|
||||||
|
continue
|
||||||
|
found: list[Finding] = []
|
||||||
|
if len(group) > 1:
|
||||||
|
# each line on its own, so an English line cannot hide a Danish one next to it
|
||||||
|
found = [
|
||||||
|
Finding(path, number, number, verdict[0], round(verdict[1], 2), line)
|
||||||
|
for number, line in group
|
||||||
|
if (
|
||||||
|
verdict := judge(
|
||||||
|
line, allowed, max(threshold, LINE_THRESHOLD), max(min_words, LINE_MIN_WORDS)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
if not found and (verdict := judge(body, allowed, threshold, min_words)):
|
||||||
|
# the block as a whole, so a sentence cut over short lines is not missed
|
||||||
|
found = [Finding(path, group[0][0], group[-1][0], verdict[0], round(verdict[1], 2), body)]
|
||||||
|
findings += [
|
||||||
|
f
|
||||||
|
for f in found
|
||||||
|
if lines is None or any(f.line <= n <= f.end_line for n in lines)
|
||||||
|
]
|
||||||
|
return findings
|
||||||
|
|
||||||
|
|
||||||
|
def read_allowed_words(path: str | None) -> frozenset[str]:
|
||||||
|
"""Words, one per line, ``#`` starts a comment; the file may be missing only when not named."""
|
||||||
|
if not path:
|
||||||
|
return frozenset()
|
||||||
|
try:
|
||||||
|
content = Path(path).read_text(encoding="utf-8")
|
||||||
|
except OSError:
|
||||||
|
raise CheckError(f"allowed-words file '{path}' cannot be read.") from None
|
||||||
|
return frozenset(
|
||||||
|
line.split("#", 1)[0].strip().lower()
|
||||||
|
for line in content.splitlines()
|
||||||
|
if line.split("#", 1)[0].strip()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _git(directory: Path, *args: str) -> str:
|
||||||
|
result = subprocess.run(
|
||||||
|
["git", "-c", "core.quotepath=off", *args],
|
||||||
|
cwd=directory,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
encoding="utf-8",
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise CheckError(f"git {' '.join(args[:2])} failed: {result.stderr.strip()[:200]}")
|
||||||
|
return result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def changed_lines(directory: Path, base: str) -> dict[str, set[int]]:
|
||||||
|
"""Added or changed line numbers per file since the merge base with ``base``."""
|
||||||
|
out = _git(directory, "diff", "-U0", "--no-color", "--diff-filter=ACMR", f"{base}...HEAD")
|
||||||
|
result: dict[str, set[int]] = {}
|
||||||
|
current: set[int] | None = None
|
||||||
|
for line in out.splitlines():
|
||||||
|
if line.startswith("+++ "):
|
||||||
|
name = line[4:]
|
||||||
|
current = result.setdefault(name[2:], set()) if name.startswith("b/") else None
|
||||||
|
continue
|
||||||
|
hunk = _HUNK.match(line)
|
||||||
|
if hunk and current is not None:
|
||||||
|
start = int(hunk.group(1))
|
||||||
|
size = int(hunk.group(2)) if hunk.group(2) is not None else 1
|
||||||
|
current.update(range(start, start + size))
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def tracked_files(directory: Path) -> list[str]:
|
||||||
|
return [name for name in _git(directory, "ls-files").splitlines() if name]
|
||||||
|
|
||||||
|
|
||||||
|
def _excluded(path: str, excludes) -> bool:
|
||||||
|
parts = path.split("/")
|
||||||
|
if any(part in count_loc.DEFAULT_EXCLUDED_DIRS for part in parts[:-1]):
|
||||||
|
return True
|
||||||
|
return excludes.matches(parts[-1], path)
|
||||||
|
|
||||||
|
|
||||||
|
def check_repository(
|
||||||
|
directory: Path,
|
||||||
|
base: str | None,
|
||||||
|
check_all: bool,
|
||||||
|
patterns: list[str],
|
||||||
|
allowed: frozenset[str],
|
||||||
|
threshold: float,
|
||||||
|
min_words: int,
|
||||||
|
) -> tuple[list[Finding], int]:
|
||||||
|
"""Return (findings, number of files read)."""
|
||||||
|
excludes = count_loc.Excludes(patterns)
|
||||||
|
if check_all:
|
||||||
|
selection: dict[str, set[int] | None] = {name: None for name in tracked_files(directory)}
|
||||||
|
else:
|
||||||
|
if not base:
|
||||||
|
raise CheckError("give --base (a git ref such as origin/main) or --all.")
|
||||||
|
selection = dict(changed_lines(directory, base))
|
||||||
|
findings: list[Finding] = []
|
||||||
|
read = 0
|
||||||
|
for name in sorted(selection):
|
||||||
|
if count_loc.language_of(name) is None or _excluded(name, excludes):
|
||||||
|
continue
|
||||||
|
target = directory / name
|
||||||
|
if not target.is_file() or target.stat().st_size > MAX_FILE_BYTES:
|
||||||
|
continue
|
||||||
|
data = target.read_bytes()
|
||||||
|
if not count_loc._is_text(data):
|
||||||
|
continue
|
||||||
|
read += 1
|
||||||
|
text = data.decode("utf-8", errors="replace")
|
||||||
|
findings += check_text(name, text, selection[name], allowed, threshold, min_words)
|
||||||
|
return findings, read
|
||||||
|
|
||||||
|
|
||||||
|
def describe(finding: Finding) -> str:
|
||||||
|
name = LANGUAGE_NAMES.get(finding.language, finding.language)
|
||||||
|
shown = finding.text if len(finding.text) <= 120 else finding.text[:117] + "..."
|
||||||
|
return f'looks {name} ({finding.probability}): "{shown}"'
|
||||||
|
|
||||||
|
|
||||||
|
def _split(value: str) -> list[str]:
|
||||||
|
return [item.strip() for item in re.split(r"[,\n]", value) if item.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def main(argv: list[str] | None = None) -> int:
|
||||||
|
parser = argparse.ArgumentParser(description="Check that source code comments are in English.")
|
||||||
|
parser.add_argument("command", choices=["check"])
|
||||||
|
parser.add_argument("--directory", default=".")
|
||||||
|
parser.add_argument("--base", default="")
|
||||||
|
parser.add_argument("--scope", choices=["changed", "all"], default="changed")
|
||||||
|
parser.add_argument("--exclude", default="")
|
||||||
|
parser.add_argument("--allowed-words", default="")
|
||||||
|
parser.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD)
|
||||||
|
parser.add_argument("--min-words", type=int, default=DEFAULT_MIN_WORDS)
|
||||||
|
parser.add_argument("--output", default="")
|
||||||
|
args = parser.parse_args(argv)
|
||||||
|
try:
|
||||||
|
if not 0.0 < args.threshold <= 1.0 or args.min_words < 1:
|
||||||
|
raise CheckError("threshold must be above 0 and at most 1, and min-words at least 1.")
|
||||||
|
directory = Path(args.directory)
|
||||||
|
if not directory.is_dir():
|
||||||
|
raise CheckError(f"directory '{args.directory}' does not exist.")
|
||||||
|
allowed = read_allowed_words(args.allowed_words or None)
|
||||||
|
findings, files = check_repository(
|
||||||
|
directory,
|
||||||
|
args.base or None,
|
||||||
|
args.scope == "all",
|
||||||
|
_split(args.exclude),
|
||||||
|
allowed,
|
||||||
|
args.threshold,
|
||||||
|
args.min_words,
|
||||||
|
)
|
||||||
|
except CheckError as error:
|
||||||
|
print(f"::error::[configuration] {error}")
|
||||||
|
return 2
|
||||||
|
for finding in findings:
|
||||||
|
message = describe(finding)
|
||||||
|
print(f"::error file={finding.path},line={finding.line}::[english-comments] {message}")
|
||||||
|
print(f"{finding.path}:{finding.line}: {message}")
|
||||||
|
if args.output:
|
||||||
|
Path(args.output).write_text(
|
||||||
|
json.dumps([asdict(f) for f in findings], indent=2, ensure_ascii=False) + "\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
summary = f"{len(findings)} comment(s) not in English in {files} file(s) checked."
|
||||||
|
print(summary if findings else f"All comments are in English: {files} file(s) checked.")
|
||||||
|
return 1 if findings else 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -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())
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Lint the shell scripts of the composite actions (MIL-012 task 6): shellcheck,
|
||||||
|
# and shfmt in the format .editorconfig sets (two spaces, indented case labels).
|
||||||
|
# validate-workflows.yml runs it on every pull request and push to main.
|
||||||
|
#
|
||||||
|
# Usage: lint-action-scripts.sh [<directory>] (default: actions)
|
||||||
|
# Env: SHELLCHECK, SHFMT the commands to run (default: from the PATH)
|
||||||
|
# Exit: 0 every script is clean (or there is none); 1 at least one finding or
|
||||||
|
# a missing tool
|
||||||
|
# Needs: bash, find, shellcheck, shfmt
|
||||||
|
|
||||||
|
SHELLCHECK="${SHELLCHECK:-shellcheck}"
|
||||||
|
SHFMT="${SHFMT:-shfmt}"
|
||||||
|
|
||||||
|
main() {
|
||||||
|
set -u
|
||||||
|
local dir="${1:-actions}" failed=0 tool
|
||||||
|
for tool in "$SHELLCHECK" "$SHFMT"; do
|
||||||
|
command -v "$tool" >/dev/null 2>&1 || {
|
||||||
|
echo "::error::[environment] $tool is not installed."
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
done
|
||||||
|
local files=()
|
||||||
|
while IFS= read -r file; do
|
||||||
|
files+=("$file")
|
||||||
|
done < <(find "$dir" -name '*.sh' -type f | sort)
|
||||||
|
if [ "${#files[@]}" -eq 0 ]; then
|
||||||
|
echo "::notice::No shell scripts under '$dir'; nothing was checked."
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
"$SHELLCHECK" "${files[@]}" || {
|
||||||
|
echo "::error::[lint] shellcheck found problems."
|
||||||
|
failed=1
|
||||||
|
}
|
||||||
|
"$SHFMT" -d "${files[@]}" || {
|
||||||
|
echo "::error::[lint] shfmt would change a script; run shfmt -w."
|
||||||
|
failed=1
|
||||||
|
}
|
||||||
|
[ "$failed" -eq 0 ] && echo "Checked ${#files[@]} script(s): clean."
|
||||||
|
exit "$failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
main "$@"
|
||||||
Reference in New Issue
Block a user