DEVX-146: feat: add --include-roles and --exclude-roles to distribute_molecule
Co-authored-by: oblachno Admin <admin@oblachno.oblachno.fyi>
This commit is contained in:
committed by
emil User
parent
0df79fed53
commit
6601d90bee
@@ -0,0 +1,12 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Don't attribute human qualities to software or hardware ('%s')."
|
||||||
|
link: https://developers.google.com/style/anthropomorphism
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# Limited to the two verbs the guide itself names. Broader lists (wants, knows,
|
||||||
|
# thinks) can't tell a software subject from a human one: on a 950-file corpus
|
||||||
|
# they produced 8 false positives ('the customer wants', 'your audience knows')
|
||||||
|
# for every 2 real ones.
|
||||||
|
tokens:
|
||||||
|
- sees
|
||||||
|
- tells
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
extends: existence
|
extends: existence
|
||||||
message: "'%s' should be in lowercase."
|
message: "'%s' should be in lowercase."
|
||||||
link: 'https://developers.google.com/style/colons'
|
link: 'https://developers.google.com/style/colons'
|
||||||
nonword: true
|
|
||||||
level: warning
|
level: warning
|
||||||
scope: sentence
|
scope: sentence
|
||||||
|
# The match is the word itself, not ': X', and `nonword` is off. Both are
|
||||||
|
# required for a project Vocab to work: Vale compares accept.txt entries
|
||||||
|
# against the matched text, and `nonword: true` opts out of that entirely.
|
||||||
|
# So a proper noun after a colon can be exempted by adding it to accept.txt.
|
||||||
|
# The guide's other exemption, notice labels, is handled by the lookbehinds;
|
||||||
|
# headings are already excluded by `scope: sentence`. See issue #20.
|
||||||
tokens:
|
tokens:
|
||||||
- '(?<!:[^ ]+?):\s[A-Z]'
|
- '(?<!Note: )(?<!Caution: )(?<!Warning: )(?<!Success: )(?<=:\s)[A-Z]\w+'
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ level: error
|
|||||||
nonword: true
|
nonword: true
|
||||||
tokens:
|
tokens:
|
||||||
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
|
- '\d{1,2}(?:\.|/)\d{1,2}(?:\.|/)\d{4}'
|
||||||
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)|May|Jun(?:e)|Jul(?:y)|Aug(?:ust)|Sep(?:tember)?|Oct(?:ober)|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
|
- '\d{1,2} (?:Jan(?:uary)?|Feb(?:ruary)?|Mar(?:ch)?|Apr(?:il)?|May|Jun(?:e)?|Jul(?:y)?|Aug(?:ust)?|Sep(?:tember)?|Oct(?:ober)?|Nov(?:ember)?|Dec(?:ember)?) \d{4}'
|
||||||
|
|||||||
@@ -0,0 +1,14 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the unverifiable claim '%s'."
|
||||||
|
link: https://developers.google.com/style/excessive-claims
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'never', 'always', and 'ensure', but in technical writing
|
||||||
|
# those are usually legitimate instructions ('never commit secrets') rather than
|
||||||
|
# product claims: they accounted for 125 of 142 hits on a 950-file corpus.
|
||||||
|
# 'best practices' is a fixed term, not a superlative.
|
||||||
|
tokens:
|
||||||
|
- 'best(?! practices?)'
|
||||||
|
- simplest
|
||||||
|
- fastest
|
||||||
|
- guarantees?
|
||||||
@@ -3,11 +3,13 @@ message: "Avoid first-person pronouns such as '%s'."
|
|||||||
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
link: 'https://developers.google.com/style/pronouns#personal-pronouns'
|
||||||
ignorecase: true
|
ignorecase: true
|
||||||
level: warning
|
level: warning
|
||||||
nonword: true
|
# The 'I' tokens use lookaround rather than consuming the surrounding
|
||||||
|
# whitespace. Matching ' I ' made the alert span cover both spaces, which shows
|
||||||
|
# up as a too-wide underline in editors, and read as "such as ' I '". Dropping
|
||||||
|
# `nonword` also lets a project Vocab apply, which it can't when set. See PR #50.
|
||||||
tokens:
|
tokens:
|
||||||
- (?:^|\s)I\s
|
- '(?<=^|\s)I(?=[\s,])'
|
||||||
- (?:^|\s)I,\s
|
- "\\bI'm\\b"
|
||||||
- \bI'm\b
|
|
||||||
- \bme\b
|
- \bme\b
|
||||||
- \bmy\b
|
- \bmy\b
|
||||||
- \bmine\b
|
- \bmine\b
|
||||||
|
|||||||
@@ -4,8 +4,11 @@ link: "https://developers.google.com/style/capitalization#capitalization-in-titl
|
|||||||
level: warning
|
level: warning
|
||||||
scope: heading
|
scope: heading
|
||||||
match: $sentence
|
match: $sentence
|
||||||
indicators:
|
# No `indicators: [":"]` here. That makes Vale require a capital after a colon,
|
||||||
- ":"
|
# which is the Microsoft convention this rule was originally copied from. This
|
||||||
|
# guide says the opposite: "the first word after a colon is generally
|
||||||
|
# lowercase" (developers.google.com/style/colons), and Colons.yml enforces
|
||||||
|
# exactly that. See issue #58.
|
||||||
exceptions:
|
exceptions:
|
||||||
- Azure
|
- Azure
|
||||||
- CLI
|
- CLI
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid the jargon '%s'."
|
||||||
|
link: https://developers.google.com/style/jargon
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also cites 'solution', 'support', and 'workload' as overloaded
|
||||||
|
# terms, but those have ordinary technical meanings and accounted for every hit
|
||||||
|
# on a 950-file corpus, so only the unambiguous figurative terms are listed.
|
||||||
|
tokens:
|
||||||
|
- break-glass
|
||||||
|
- camel ?case
|
||||||
|
- out-of-the-box
|
||||||
|
- swim ?lane
|
||||||
@@ -6,6 +6,10 @@ level: error
|
|||||||
nonword: true
|
nonword: true
|
||||||
action:
|
action:
|
||||||
name: replace
|
name: replace
|
||||||
|
# The delimiter is a lookahead so the replacement doesn't swallow the comma or
|
||||||
|
# space that follows (issue #18). `$` is included so the abbreviation is still
|
||||||
|
# caught at the end of a heading, table cell, or block, which accounted for 8
|
||||||
|
# of 10 occurrences on a 950-file corpus.
|
||||||
swap:
|
swap:
|
||||||
'\b(?:eg|e\.g\.)(?=[\s,;])': for example
|
'\b(?:eg|e\.g\.)(?=[\s,;]|$)': for example
|
||||||
'\b(?:ie|i\.e\.)(?=[\s,;])': that is
|
'\b(?:ie|i\.e\.)(?=[\s,;]|$)': that is
|
||||||
|
|||||||
@@ -3,5 +3,26 @@ message: "Use the Oxford comma in '%s'."
|
|||||||
link: 'https://developers.google.com/style/commas'
|
link: 'https://developers.google.com/style/commas'
|
||||||
scope: sentence
|
scope: sentence
|
||||||
level: warning
|
level: warning
|
||||||
|
nonword: true
|
||||||
|
# List items may be several words long, not just one. Four guards keep the
|
||||||
|
# false-positive rate down:
|
||||||
|
#
|
||||||
|
# 1. The comma can't be the one closing a fronted subordinate clause
|
||||||
|
# ('When your alarm rings, you turn it off and tumble out of bed.') --
|
||||||
|
# that comma separates clauses, not list items. Only the first comma of
|
||||||
|
# such a sentence is exempt, so 'When it rains, apples, pears or bananas
|
||||||
|
# get wet.' is still caught.
|
||||||
|
# 2. The item can't open with a clause-introducer (', which ...',
|
||||||
|
# ', specifically ...').
|
||||||
|
# 3. The item can't open with a subject pronoun followed by a verb, which
|
||||||
|
# marks a compound predicate rather than a list ('..., you walk to the
|
||||||
|
# fridge and get a snack.'). A pronoun directly followed by 'and'/'or'
|
||||||
|
# is a real list item, so ', you and me.' still matches.
|
||||||
|
# 4. Neither item may contain an auxiliary verb, which is another compound
|
||||||
|
# predicate signal (', it has some downsides and is officially
|
||||||
|
# discouraged.').
|
||||||
|
#
|
||||||
|
# The trailing anchor allows end-of-scope so list fragments ('Apples, pears
|
||||||
|
# or bananas') are still caught.
|
||||||
tokens:
|
tokens:
|
||||||
- '(?:[^,]+,){1,}\s\w+\s(?:and|or)'
|
- '(?<!^(?i:when|whenever|while|if|unless|until|although|though|because|since|after|before|once|whereas|whether|as)\b[^,]{0,80}),\s(?!(?:which|who|whom|whose|that|where|when|while|because|since|although|though|if|unless|so|but|and|or|however|therefore|thus|specifically|especially|namely|then|take|see|note|consider|make|use|either|neither)\b)(?!(?i:i|you|we|they|he|she|it)\s+(?!(?:and|or)\b))(?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+ (?:and|or) (?:(?!\b(?:is|are|was|were|has|have|had|be|been|being|will|would|can|could|should|may|might|must|do|does|did)\b)\w+ ){0,4}\w+(?:[.?!]|$)'
|
||||||
|
|||||||
@@ -3,5 +3,13 @@ message: "Use parentheses judiciously."
|
|||||||
link: 'https://developers.google.com/style/parentheses'
|
link: 'https://developers.google.com/style/parentheses'
|
||||||
nonword: true
|
nonword: true
|
||||||
level: suggestion
|
level: suggestion
|
||||||
|
# `[^)]` rather than `.+`: a greedy match ran from the first '(' on a line to
|
||||||
|
# the last ')', so 'Text (one) and more (two).' produced a single alert
|
||||||
|
# covering everything between them. See issue #30.
|
||||||
|
# A bare 3-5 letter acronym is skipped: Acronyms.yml requires acronyms to be
|
||||||
|
# defined as 'Spelled Out Term (ACRONYM)', so flagging those parentheses would
|
||||||
|
# put the two rules in direct conflict. The acronym has to be the whole
|
||||||
|
# parenthetical — '(NASA rocket program)' is an ordinary aside and still
|
||||||
|
# flags. Length matches the {3,5} in Acronyms.yml. See PR #59.
|
||||||
tokens:
|
tokens:
|
||||||
- '\(.+\)'
|
- '\((?![A-Z]{3,5}\))[^)]+\)'
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
extends: existence
|
||||||
|
message: "Avoid time-based words like '%s' in product documentation."
|
||||||
|
link: https://developers.google.com/style/timeless-documentation
|
||||||
|
level: suggestion
|
||||||
|
ignorecase: true
|
||||||
|
# The guide also names 'now' and 'new', but both have common senses that aren't
|
||||||
|
# time-anchored ('create a new project'): adding them took a 950-file corpus of
|
||||||
|
# technical documentation from 14 hits to 117. 'recently' is left out too — every
|
||||||
|
# hit in that corpus was the UI idiom 'recently used'.
|
||||||
|
tokens:
|
||||||
|
- currently
|
||||||
|
- latest
|
||||||
|
- soon
|
||||||
@@ -4,5 +4,7 @@ link: "https://developers.google.com/style/units-of-measure"
|
|||||||
nonword: true
|
nonword: true
|
||||||
level: error
|
level: error
|
||||||
tokens:
|
tokens:
|
||||||
- \b\d+(?:B|kB|MB|GB|TB)
|
- '\b\d+(?:B|kB|MB|GB|TB)\b'
|
||||||
- \b\d+(?:ns|ms|s|min|h|d)
|
- '\b\d+(?:ns|ms|min|h|d)\b'
|
||||||
|
# Seconds are split out so a decade ('1990s') isn't read as a unit.
|
||||||
|
- '\b\d+s\b(?<!\b(?:19|20)\d\ds\b)'
|
||||||
|
|||||||
@@ -2,79 +2,28 @@ extends: substitution
|
|||||||
message: "Use '%s' instead of '%s'."
|
message: "Use '%s' instead of '%s'."
|
||||||
link: "https://developers.google.com/style/word-list"
|
link: "https://developers.google.com/style/word-list"
|
||||||
level: warning
|
level: warning
|
||||||
|
# Case matters here: each key's own capitalization is what's being corrected,
|
||||||
|
# so ignorecase would make these match their own replacements. The rest of the
|
||||||
|
# word list lives in WordListCase.yml.
|
||||||
ignorecase: false
|
ignorecase: false
|
||||||
action:
|
action:
|
||||||
name: replace
|
name: replace
|
||||||
swap:
|
swap:
|
||||||
"(?:API Console|dev|developer) key": API key
|
|
||||||
"(?:cell ?phone|smart ?phone)": phone|mobile phone
|
|
||||||
"(?:dev|developer|APIs) console": API console
|
|
||||||
"(?:e-mail|Email|E-mail)": email
|
|
||||||
"(?:file ?path|path ?name)": path
|
|
||||||
"(?:kill|terminate|abort)": stop|exit|cancel|end
|
|
||||||
"(?:OAuth ?2|Oauth)": OAuth 2.0
|
|
||||||
"(?:ok|Okay)": OK|okay
|
|
||||||
"(?:WiFi|wifi)": Wi-Fi
|
|
||||||
'[\.]+apk': APK
|
|
||||||
'3\-D': 3D
|
|
||||||
'Google (?:I\-O|IO)': Google I/O
|
|
||||||
"tap (?:&|and) hold": touch & hold
|
|
||||||
"un(?:check|select)": clear
|
|
||||||
above: preceding
|
|
||||||
account name: username
|
|
||||||
action bar: app bar
|
|
||||||
admin: administrator
|
|
||||||
Ajax: AJAX
|
Ajax: AJAX
|
||||||
a\.k\.a|aka: or|also known as
|
|
||||||
Android device: Android-powered device
|
Android device: Android-powered device
|
||||||
android: Android
|
android: Android
|
||||||
API explorer: APIs Explorer
|
API explorer: APIs Explorer
|
||||||
application: app
|
|
||||||
approx\.: approximately
|
|
||||||
authN: authentication
|
authN: authentication
|
||||||
authZ: authorization
|
authZ: authorization
|
||||||
autoupdate: automatically update
|
|
||||||
cellular data: mobile data
|
|
||||||
cellular network: mobile network
|
|
||||||
chapter: documents|pages|sections
|
|
||||||
check box: checkbox
|
|
||||||
CLI: command-line tool
|
CLI: command-line tool
|
||||||
click on: click|click in
|
|
||||||
Cloud: Google Cloud Platform|GCP
|
Cloud: Google Cloud Platform|GCP
|
||||||
Container Engine: Kubernetes Engine
|
Container Engine: Kubernetes Engine
|
||||||
content type: media type
|
|
||||||
curated roles: predefined roles
|
|
||||||
data are: data is
|
|
||||||
Developers Console: Google API Console|API Console
|
Developers Console: Google API Console|API Console
|
||||||
disabled?: turn off|off
|
|
||||||
ephemeral IP address: ephemeral external IP address
|
|
||||||
fewer data: less data
|
|
||||||
file name: filename
|
|
||||||
firewalls: firewall rules
|
|
||||||
functionality: capability|feature
|
|
||||||
Google account: Google Account
|
Google account: Google Account
|
||||||
Google accounts: Google Accounts
|
Google accounts: Google Accounts
|
||||||
Googling: search with Google
|
Googling: search with Google
|
||||||
grayed-out: unavailable
|
|
||||||
HTTPs: HTTPS
|
HTTPs: HTTPS
|
||||||
in order to: to
|
|
||||||
ingest: import|load
|
|
||||||
k8s: Kubernetes
|
k8s: Kubernetes
|
||||||
long press: touch & hold
|
|
||||||
network IP address: internal IP address
|
|
||||||
omnibox: address bar
|
|
||||||
open-source: open source
|
|
||||||
overview screen: recents screen
|
|
||||||
regex: regular expression
|
|
||||||
SHA1: SHA-1|HAS-SHA1
|
SHA1: SHA-1|HAS-SHA1
|
||||||
sign into: sign in to
|
|
||||||
sign-?on: single sign-on
|
|
||||||
static IP address: static external IP address
|
|
||||||
stylesheet: style sheet
|
|
||||||
synch: sync
|
|
||||||
tablename: table name
|
|
||||||
tablet: device
|
|
||||||
touch: tap
|
|
||||||
url: URL
|
url: URL
|
||||||
vs\.: versus
|
|
||||||
World Wide Web: web
|
World Wide Web: web
|
||||||
|
|||||||
@@ -0,0 +1,68 @@
|
|||||||
|
extends: substitution
|
||||||
|
message: "Use '%s' instead of '%s'."
|
||||||
|
link: "https://developers.google.com/style/word-list"
|
||||||
|
level: warning
|
||||||
|
# The case-insensitive half of the word list, so sentence-initial use is caught
|
||||||
|
# ('Touch the screen', not only 'touch the screen'). Entries that must stay
|
||||||
|
# case-sensitive are in WordList.yml.
|
||||||
|
ignorecase: true
|
||||||
|
action:
|
||||||
|
name: replace
|
||||||
|
swap:
|
||||||
|
"(?:API Console|dev|developer) key": API key
|
||||||
|
"(?:cell ?phone|smart ?phone)": phone|mobile phone
|
||||||
|
"(?:dev|developer|APIs) console": API console
|
||||||
|
"(?:e-mail|Email|E-mail)": email
|
||||||
|
"(?:file ?path|path ?name)": path
|
||||||
|
"(?:kill|terminate|abort)": stop|exit|cancel|end
|
||||||
|
# Longest form first: with the shortest alternative leading, 'OAuth 2' matched
|
||||||
|
# only 'OAuth', so applying the suggestion produced 'OAuth 2.0 2'. The rule is
|
||||||
|
# already case-insensitive, so the inline (?i) is redundant. See issue #41.
|
||||||
|
'\bOauth2\.0\b|\bOAuth ?2\b(?!\.0)|\bOauth\b(?! ?2)': OAuth 2.0
|
||||||
|
"(?:ok|Okay)": OK|okay
|
||||||
|
"(?:WiFi|wifi)": Wi-Fi
|
||||||
|
'[\.]+apk': APK
|
||||||
|
'3\-D': 3D
|
||||||
|
'Google (?:I\-O|IO)': Google I/O
|
||||||
|
"tap (?:&|and) hold": touch & hold
|
||||||
|
"un(?:check|select)": clear
|
||||||
|
above: preceding
|
||||||
|
account name: username
|
||||||
|
action bar: app bar
|
||||||
|
admin: administrator
|
||||||
|
a\.k\.a|aka: or|also known as
|
||||||
|
application: app
|
||||||
|
approx\.: approximately
|
||||||
|
autoupdate: automatically update
|
||||||
|
cellular data: mobile data
|
||||||
|
cellular network: mobile network
|
||||||
|
chapter: documents|pages|sections
|
||||||
|
check box: checkbox
|
||||||
|
click on: click|click in
|
||||||
|
content type: media type
|
||||||
|
curated roles: predefined roles
|
||||||
|
data are: data is
|
||||||
|
disabled?: turn off|off
|
||||||
|
ephemeral IP address: ephemeral external IP address
|
||||||
|
fewer data: less data
|
||||||
|
file name: filename
|
||||||
|
firewalls: firewall rules
|
||||||
|
functionality: capability|feature
|
||||||
|
grayed-out: unavailable
|
||||||
|
in order to: to
|
||||||
|
ingest: import|load
|
||||||
|
long press: touch & hold
|
||||||
|
network IP address: internal IP address
|
||||||
|
omnibox: address bar
|
||||||
|
open-source: open source
|
||||||
|
overview screen: recents screen
|
||||||
|
regex: regular expression
|
||||||
|
sign into: sign in to
|
||||||
|
'(?<!single )sign-?on': single sign-on
|
||||||
|
static IP address: static external IP address
|
||||||
|
stylesheet: style sheet
|
||||||
|
synch: sync
|
||||||
|
tablename: table name
|
||||||
|
tablet: device
|
||||||
|
'touch(?! ?(?:&|and) hold)': tap
|
||||||
|
vs\.: versus
|
||||||
@@ -12,7 +12,7 @@ opinionated CI/CD pipeline: conventional commits, automated versioning via
|
|||||||
git-cliff, squash-merge automation, Vikunja task tracking, wiki sync, and
|
git-cliff, squash-merge automation, Vikunja task tracking, wiki sync, and
|
||||||
quality badges.
|
quality badges.
|
||||||
|
|
||||||
> An open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
> An open source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ parallel test distribution, and more into a single installable package.
|
|||||||
It was extracted from the [GRM](https://git.oblachno.oblachno.fyi/oblachno-oss/grm)
|
It was extracted from the [GRM](https://git.oblachno.oblachno.fyi/oblachno-oss/grm)
|
||||||
project to be reusable across all oblachno-oss repositories.
|
project to be reusable across all oblachno-oss repositories.
|
||||||
|
|
||||||
> An open-source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
> An open source project from **Oblachno** (облачно means *cloudy* in Bulgarian).
|
||||||
|
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/actions)
|
||||||
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
[](https://git.oblachno.oblachno.fyi/oblachno-oss/devx/src/branch/master/LICENSE)
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ unblocked auto-merge across all three repos.
|
|||||||
### 2. Double-Prefix Detection (MEDIUM impact)
|
### 2. Double-Prefix Detection (MEDIUM impact)
|
||||||
|
|
||||||
`check_auto_merge_ready.py` now detects and rejects Vikunja task titles
|
`check_auto_merge_ready.py` now detects and rejects Vikunja task titles
|
||||||
that include the identifier prefix (for example, "DEVX-127: Fix...").
|
that include the identifier prefix (for example, "DEVX-127: Fix").
|
||||||
The validator adds the prefix automatically, so a double prefix would
|
The validator adds the prefix automatically, so a double prefix would
|
||||||
fail validation.
|
fail validation.
|
||||||
|
|
||||||
|
|||||||
@@ -87,11 +87,11 @@ overridden via environment variables with the `DEVX_` prefix. Provides:
|
|||||||
|
|
||||||
- `GITEA_API_URL` / `VIKUNJA_API_URL` — API endpoints
|
- `GITEA_API_URL` / `VIKUNJA_API_URL` — API endpoints
|
||||||
- `REPO_OWNER` — repository owner (must be set per-project)
|
- `REPO_OWNER` — repository owner (must be set per-project)
|
||||||
- `TASK_PREFIX` / `TASK_ID_RE` — task ID prefix and regex (for example, `DEVX-N`)
|
- `TASK_PREFIX` / `TASK_ID_RE` — task ID prefix and regular expression (for example, `DEVX-N`)
|
||||||
- `VIKUNJA_PROJECT_ID` — Vikunja project for task tracking
|
- `VIKUNJA_PROJECT_ID` — Vikunja project for task tracking
|
||||||
- `DEFAULT_TIMEOUT`, `DEFAULT_PER_PAGE` — HTTP client defaults
|
- `DEFAULT_TIMEOUT`, `DEFAULT_PER_PAGE` — HTTP client defaults
|
||||||
- `MAX_RETRIES`, `RETRY_BACKOFF_BASE`, `RETRY_STATUS_CODES` — retry config
|
- `MAX_RETRIES`, `RETRY_BACKOFF_BASE`, `RETRY_STATUS_CODES` — retry config
|
||||||
- `CONVENTIONAL_RE` — conventional commit format regex
|
- `CONVENTIONAL_RE` — conventional commit format regular expression
|
||||||
|
|
||||||
### `exceptions.py`
|
### `exceptions.py`
|
||||||
|
|
||||||
@@ -109,7 +109,7 @@ wraps user-facing strings for translation.
|
|||||||
|
|
||||||
Projects can extend translations by setting `DEVX_TRANSLATIONS_PATH` to a
|
Projects can extend translations by setting `DEVX_TRANSLATIONS_PATH` to a
|
||||||
custom JSON file. Keys from the project's file are merged on top of devx's
|
custom JSON file. Keys from the project's file are merged on top of devx's
|
||||||
built-in translations, allowing projects to override or add keys without
|
built-in translations, allowing projects to override, or add keys without
|
||||||
modifying the package.
|
modifying the package.
|
||||||
|
|
||||||
### `api_clients.py`
|
### `api_clients.py`
|
||||||
@@ -171,7 +171,7 @@ from `devx.api_clients`, `devx.config`, `devx.gitea_cli`, and `devx.i18n`.
|
|||||||
|
|
||||||
Automated release using git-cliff. Calculates the next semver version from
|
Automated release using git-cliff. Calculates the next semver version from
|
||||||
conventional commits since the last tag, updates `__version__` in
|
conventional commits since the last tag, updates `__version__` in
|
||||||
`__init__.py` and `CHANGELOG.md`, runs lint and tests to verify the release
|
`__init__.py` and `CHANGELOG.md`, runs lint, and tests to verify the release
|
||||||
is healthy, commits with `release: vX.Y.Z [skip ci]`, creates an annotated
|
is healthy, commits with `release: vX.Y.Z [skip ci]`, creates an annotated
|
||||||
tag, and pushes both to master.
|
tag, and pushes both to master.
|
||||||
|
|
||||||
@@ -288,7 +288,7 @@ Click commands from `cli.py` and verifies each has documentation in
|
|||||||
### `discover_runners.py`
|
### `discover_runners.py`
|
||||||
|
|
||||||
Discovers available Gitea Actions runners at three levels: repository,
|
Discovers available Gitea Actions runners at three levels: repository,
|
||||||
organization, and instance (admin). Falls back to the `MOLECULE_RUNNERS` repo
|
organization, and instance (administrator). Falls back to the `MOLECULE_RUNNERS` repo
|
||||||
variable or `DEFAULT_MAX_RUNNERS` (3). Outputs runner count or a JSON index
|
variable or `DEFAULT_MAX_RUNNERS` (3). Outputs runner count or a JSON index
|
||||||
array for use as a dynamic matrix in Gitea Actions.
|
array for use as a dynamic matrix in Gitea Actions.
|
||||||
|
|
||||||
@@ -330,7 +330,7 @@ Supports `--tool` to install specific tools and `--list` to show status.
|
|||||||
Runs unit tests and enforces execution-time budgets. Two quality gates:
|
Runs unit tests and enforces execution-time budgets. Two quality gates:
|
||||||
total suite time must not exceed `--max-seconds` (default: 10s), and no
|
total suite time must not exceed `--max-seconds` (default: 10s), and no
|
||||||
individual test may exceed `--max-single-seconds` (default: 0.5s, 0 to
|
individual test may exceed `--max-single-seconds` (default: 0.5s, 0 to
|
||||||
disable). Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0`.
|
off). Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0`.
|
||||||
|
|
||||||
### `check_test_isolation.py`
|
### `check_test_isolation.py`
|
||||||
|
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ devx ci detect-release-commit
|
|||||||
|
|
||||||
Discover available Gitea Actions runners for dynamic job distribution.
|
Discover available Gitea Actions runners for dynamic job distribution.
|
||||||
Queries the Gitea API for registered runners at repository, organization, and
|
Queries the Gitea API for registered runners at repository, organization, and
|
||||||
instance (admin) levels. Falls back to `MOLECULE_RUNNERS` repo variable or
|
instance (administrator) levels. Falls back to `MOLECULE_RUNNERS` repo variable or
|
||||||
`DEFAULT_MAX_RUNNERS` (3).
|
`DEFAULT_MAX_RUNNERS` (3).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -373,7 +373,7 @@ Run unit tests and enforce execution-time budgets. Two quality gates:
|
|||||||
|
|
||||||
- **Total suite time** must not exceed `--max-seconds` (default: 10s)
|
- **Total suite time** must not exceed `--max-seconds` (default: 10s)
|
||||||
- **Per-test time** — no individual test may exceed `--max-single-seconds`
|
- **Per-test time** — no individual test may exceed `--max-single-seconds`
|
||||||
(default: 0.5s, 0 to disable)
|
(default: 0.5s, 0 to turn off)
|
||||||
|
|
||||||
Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0` so pytest emits
|
Runs `make test-unit` with `PYTEST_ADDOPTS=--durations=0` so pytest emits
|
||||||
per-test timing lines.
|
per-test timing lines.
|
||||||
@@ -419,7 +419,7 @@ devx tools check-test-isolation --src-dir src/
|
|||||||
|
|
||||||
Pytest plugin options (automatic when devx is installed):
|
Pytest plugin options (automatic when devx is installed):
|
||||||
|
|
||||||
- `--no-test-isolation` — disable static analysis and runtime subprocess audit
|
- `--no-test-isolation` — turn off static analysis and runtime subprocess audit
|
||||||
- `--test-isolation-max-loop N` — max iterations per loop (default: 100)
|
- `--test-isolation-max-loop N` — max iterations per loop (default: 100)
|
||||||
|
|
||||||
### `devx tools configure-repo`
|
### `devx tools configure-repo`
|
||||||
@@ -464,7 +464,7 @@ devx tools generate-cliff-config --prefix GRM --force # overwrite existing
|
|||||||
Options:
|
Options:
|
||||||
- `--prefix <prefix>` — task ID prefix (default: `DEVX_TASK_PREFIX` env var
|
- `--prefix <prefix>` — task ID prefix (default: `DEVX_TASK_PREFIX` env var
|
||||||
or `DEVX`)
|
or `DEVX`)
|
||||||
- `--output <file>` — output file path (default: `cliff.toml`)
|
- `--output <file>` — output path (default: `cliff.toml`)
|
||||||
- `--force` — overwrite existing file
|
- `--force` — overwrite existing file
|
||||||
|
|
||||||
### `devx tools install-checkmake`
|
### `devx tools install-checkmake`
|
||||||
@@ -585,7 +585,7 @@ devx tools check-alert-rules \
|
|||||||
|
|
||||||
Options:
|
Options:
|
||||||
- `--template-path <path>` — path to templates directory (required)
|
- `--template-path <path>` — path to templates directory (required)
|
||||||
- `--template-name <name>` — template file name (default: `alert-rules.yml.j2`)
|
- `--template-name <name>` — template filename (default: `alert-rules.yml.j2`)
|
||||||
- `--var key=value` — template variables (repeatable)
|
- `--var key=value` — template variables (repeatable)
|
||||||
|
|
||||||
## Molecule Commands
|
## Molecule Commands
|
||||||
|
|||||||
@@ -104,21 +104,36 @@ def discover_scenarios(root: Path | None = None) -> list[str]:
|
|||||||
return sorted(scenarios)
|
return sorted(scenarios)
|
||||||
|
|
||||||
|
|
||||||
def discover_multi_role_scenarios(roles_root: Path | None = None) -> list[tuple[str, str]]:
|
def discover_multi_role_scenarios(
|
||||||
|
roles_root: Path | None = None,
|
||||||
|
include_roles: list[str] | None = None,
|
||||||
|
exclude_roles: list[str] | None = None,
|
||||||
|
) -> list[tuple[str, str]]:
|
||||||
"""Discover (role, scenario) pairs across all roles under *roles_root*.
|
"""Discover (role, scenario) pairs across all roles under *roles_root*.
|
||||||
|
|
||||||
Scans ``roles_root/*/molecule/*/`` for scenario directories, skipping
|
Scans ``roles_root/*/molecule/*/`` for scenario directories, skipping
|
||||||
``common`` and directories starting with ``_``. Returns a sorted list of
|
``common`` and directories starting with ``_``. Returns a sorted list of
|
||||||
``(role_name, scenario_name)`` tuples.
|
``(role_name, scenario_name)`` tuples.
|
||||||
|
|
||||||
|
If *include_roles* is given, only roles whose name is in the list are
|
||||||
|
returned. If *exclude_roles* is given, roles whose name is in the list
|
||||||
|
are skipped. Both filters are case-insensitive.
|
||||||
"""
|
"""
|
||||||
if roles_root is None:
|
if roles_root is None:
|
||||||
roles_root = DEFAULT_ROLES_ROOT
|
roles_root = DEFAULT_ROLES_ROOT
|
||||||
if not roles_root.is_dir():
|
if not roles_root.is_dir():
|
||||||
raise click.ClickException(_("Roles directory not found: {path}", path=str(roles_root)))
|
raise click.ClickException(_("Roles directory not found: {path}", path=str(roles_root)))
|
||||||
|
include_set = {r.lower() for r in include_roles} if include_roles else None
|
||||||
|
exclude_set = {r.lower() for r in exclude_roles} if exclude_roles else None
|
||||||
pairs: list[tuple[str, str]] = []
|
pairs: list[tuple[str, str]] = []
|
||||||
for role_dir in sorted(roles_root.iterdir()):
|
for role_dir in sorted(roles_root.iterdir()):
|
||||||
if not role_dir.is_dir():
|
if not role_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
|
role_name = role_dir.name
|
||||||
|
if include_set is not None and role_name.lower() not in include_set:
|
||||||
|
continue
|
||||||
|
if exclude_set is not None and role_name.lower() in exclude_set:
|
||||||
|
continue
|
||||||
mol_dir = role_dir / "molecule"
|
mol_dir = role_dir / "molecule"
|
||||||
if not mol_dir.is_dir():
|
if not mol_dir.is_dir():
|
||||||
continue
|
continue
|
||||||
@@ -348,6 +363,24 @@ def _write_github_env(key: str, value: str) -> None:
|
|||||||
help="JSON file with custom platform list (each entry: name, image, command). "
|
help="JSON file with custom platform list (each entry: name, image, command). "
|
||||||
"Overrides the default platform matrix. Useful for projects with custom test images.",
|
"Overrides the default platform matrix. Useful for projects with custom test images.",
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
"--include-roles",
|
||||||
|
"include_roles",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated list of role names to include (multi-role mode only). "
|
||||||
|
"Only scenarios from these roles are distributed. Case-insensitive. "
|
||||||
|
"Example: --include-roles docker_base,crowdsec,disk_cleanup,app_hardening",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--exclude-roles",
|
||||||
|
"exclude_roles",
|
||||||
|
type=str,
|
||||||
|
default=None,
|
||||||
|
help="Comma-separated list of role names to exclude (multi-role mode only). "
|
||||||
|
"Scenarios from these roles are skipped. Case-insensitive. "
|
||||||
|
"Example: --exclude-roles docker_base,crowdsec,disk_cleanup,app_hardening",
|
||||||
|
)
|
||||||
def cli(
|
def cli(
|
||||||
runner_index: int | None,
|
runner_index: int | None,
|
||||||
max_runners: int,
|
max_runners: int,
|
||||||
@@ -358,11 +391,18 @@ def cli(
|
|||||||
molecule_root: Path | None,
|
molecule_root: Path | None,
|
||||||
roles_root: Path | None,
|
roles_root: Path | None,
|
||||||
platforms_file: Path | None,
|
platforms_file: Path | None,
|
||||||
|
include_roles: str | None,
|
||||||
|
exclude_roles: str | None,
|
||||||
) -> None:
|
) -> None:
|
||||||
platforms = load_platforms(platforms_file)
|
platforms = load_platforms(platforms_file)
|
||||||
|
# Parse role filters
|
||||||
|
include_list = [r.strip() for r in include_roles.split(",")] if include_roles else None
|
||||||
|
exclude_list = [r.strip() for r in exclude_roles.split(",")] if exclude_roles else None
|
||||||
# Multi-role mode: discover (role, scenario) pairs across all roles
|
# Multi-role mode: discover (role, scenario) pairs across all roles
|
||||||
if roles_root is not None:
|
if roles_root is not None:
|
||||||
role_scenarios = discover_multi_role_scenarios(roles_root)
|
role_scenarios = discover_multi_role_scenarios(
|
||||||
|
roles_root, include_roles=include_list, exclude_roles=exclude_list
|
||||||
|
)
|
||||||
if list_all:
|
if list_all:
|
||||||
for role, scenario in role_scenarios:
|
for role, scenario in role_scenarios:
|
||||||
click.echo(f"{role}|{scenario}")
|
click.echo(f"{role}|{scenario}")
|
||||||
|
|||||||
@@ -311,6 +311,61 @@ class TestDiscoverMultiRole:
|
|||||||
with pytest.raises(click.ClickException):
|
with pytest.raises(click.ClickException):
|
||||||
discover_multi_role_scenarios()
|
discover_multi_role_scenarios()
|
||||||
|
|
||||||
|
def test_include_roles_filters_to_subset(self, tmp_path: Path) -> None:
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
for scenario in ["default"]:
|
||||||
|
(roles / "docker_base" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "crowdsec" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "app_container" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
result = discover_multi_role_scenarios(roles, include_roles=["docker_base", "crowdsec"])
|
||||||
|
assert ("docker_base", "default") in result
|
||||||
|
assert ("crowdsec", "default") in result
|
||||||
|
assert ("app_container", "default") not in result
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
|
def test_include_roles_case_insensitive(self, tmp_path: Path) -> None:
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
(roles / "Docker_Base" / "molecule" / "default").mkdir(parents=True)
|
||||||
|
(roles / "other" / "molecule" / "default").mkdir(parents=True)
|
||||||
|
result = discover_multi_role_scenarios(roles, include_roles=["docker_base"])
|
||||||
|
assert ("Docker_Base", "default") in result
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
def test_exclude_roles_skips_subset(self, tmp_path: Path) -> None:
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
for scenario in ["default"]:
|
||||||
|
(roles / "docker_base" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "crowdsec" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "app_container" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
result = discover_multi_role_scenarios(roles, exclude_roles=["docker_base", "crowdsec"])
|
||||||
|
assert ("docker_base", "default") not in result
|
||||||
|
assert ("crowdsec", "default") not in result
|
||||||
|
assert ("app_container", "default") in result
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
def test_exclude_roles_case_insensitive(self, tmp_path: Path) -> None:
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
(roles / "Docker_Base" / "molecule" / "default").mkdir(parents=True)
|
||||||
|
(roles / "other" / "molecule" / "default").mkdir(parents=True)
|
||||||
|
result = discover_multi_role_scenarios(roles, exclude_roles=["docker_base"])
|
||||||
|
assert ("Docker_Base", "default") not in result
|
||||||
|
assert ("other", "default") in result
|
||||||
|
assert len(result) == 1
|
||||||
|
|
||||||
|
def test_include_and_exclude_combined(self, tmp_path: Path) -> None:
|
||||||
|
roles = tmp_path / "roles"
|
||||||
|
for scenario in ["default"]:
|
||||||
|
(roles / "docker_base" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "crowdsec" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
(roles / "app_container" / "molecule" / scenario).mkdir(parents=True)
|
||||||
|
result = discover_multi_role_scenarios(
|
||||||
|
roles, include_roles=["docker_base", "crowdsec", "app_container"], exclude_roles=["crowdsec"]
|
||||||
|
)
|
||||||
|
assert ("docker_base", "default") in result
|
||||||
|
assert ("crowdsec", "default") not in result
|
||||||
|
assert ("app_container", "default") in result
|
||||||
|
assert len(result) == 2
|
||||||
|
|
||||||
def test_default_roles_root_constant(self) -> None:
|
def test_default_roles_root_constant(self) -> None:
|
||||||
assert Path("ansible/roles") == DEFAULT_ROLES_ROOT
|
assert Path("ansible/roles") == DEFAULT_ROLES_ROOT
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user