Skip to content

feat: Regex filters for replication (phase 2) - #753

Open
Vad1mo wants to merge 8 commits into
feat/regex-selector-phase1from
feat/regex-filters-phase2
Open

Vad1mo wants to merge 8 commits into
feat/regex-selector-phase1from
feat/regex-filters-phase2

Conversation

@Vad1mo

@Vad1mo Vad1mo commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

PR 2 of the stack, base is phase 1 (#752). Proposal: goharbor/community#298.

Phase 1 added the regex selector kind to retention and immutability. This phase brings the same engine to the replication filters.

What changes

A replication filter gains an optional kind, valid on the name, tag and label filters and rejected on the resource one, exactly like decoration:

{ "type": "name", "value": "library/(app|api)-.*", "kind": "regex" }

kind is empty or doublestar for everything that exists today, so no stored policy changes meaning.

One engine, in src/lib/pattern

src/lib/pattern already held the kind literals and the doublestar/regex matching of the proxy cache repository filter (goharbor/harbor#23527), so the shared engine goes there rather than in a selector package the replication filters would have to reach into. This PR adds CompileRegex, ValidateRegex and a cached Matcher next to it:

  • CompileRegex is phase 1's compile-and-anchor step, cap included, moved verbatim. The pattern is compiled bare before it is wrapped, so one that would escape the anchoring is rejected rather than silently matching on a prefix: foo)|(?:bar is invalid on its own, yet wrapping it produces a valid unanchored alternation.
  • Matcher carries the caching the selector had — a filter applied to thousands of candidates compiles once, not once per candidate — and makes it available to any caller that has a kind.
  • Phase 1's regex selector now delegates: Kind, MaxPatternLength, Compile and Validate are aliases of the shared ones and it holds a Matcher instead of its own sync.Once. src/pkg/reg uses the same Matcher, so there is no second implementation anywhere.

Match, ValidateKind and ValidateRepositoryFilter keep their behavior and the proxy cache middleware is untouched. Two differences remain between them and the new helpers, both left for the follow-up that moves the proxy onto the cached Matcher: Match compiles twice per call and has no length cap, and it trims and pre-validates its doublestar patterns while the selectors and replication filters do not.

Wiring

  • The Matcher reaches every place a filter pattern is evaluated (pkg/reg/filter, and the aliacr, dockerhub, gitlab, tencentcr and volcenginecr adapters).
  • util.IsSpecificPathForKind / IsSpecificPathComponentForKind refuse to reverse a regex pattern into an explicit repository or namespace list. Glob syntax says nothing about a regular expression, so without this an adapter would take lib(a|b) for a literal namespace and fetch the wrong thing. All the IsSpecificPath* callers were converted, not only the ones that also call Match: native, jfrog, githubcr and harbor/base derive repository or project lists the same way. githubcr only supports specific repository names, so a regex name filter is refused there.
  • The stored filter JSON already has a kind field: on a pre-1.10 entry, one without a type, it names the old repository/tag/label filter. parseFilters therefore reads it as a pattern engine only on typed entries.

Untagged artifacts

A replication tag filter has no untagged flag, so the rule is the one doublestar already follows: an artifact without tags is kept exactly when the pattern matches the empty tag. ** keeps it, v* drops it, and an anchored regex behaves identically for free — .* matches the empty string, v.* does not. The engines agree on the same policy, which is what the proposal promises; the paired cases are in artifact_test.go.

(The "never run the expression against an empty string" rule belongs to the retention and immutability selectors of phase 1, where an untagged flag governs and evaluating the pattern against "" would bypass it. Nothing in replication has that flag.)

One asymmetry that stays: gitlab case folding

The gitlab adapter compares repository paths and tags case insensitively by lowercasing both sides. Lowercasing a regex would rewrite \D into \d and [A-Z] into [a-z], so only doublestar patterns are folded. (?i) is available for the regex case.

Portal

Each pattern filter row gets a Doublestar / RegEx select, i18n keys in all ten languages (German for de-de, English elsewhere), and an info tooltip that follows the selected engine. The engine is left out of the payload while it is the default, so switching nothing changes nothing. The dialog moves to the large modal size: the filter row now carries four controls and no longer fits the old one. That needed two things beyond the clrModalSize binding, both found by measuring a live render: a 2021 rule in this component pinned div.modal-dialog to 32rem through ::ng-deep and outranked Clarity's .modal-lg on specificity, so the declared size was inert; and Clarity sets a 20px root, which makes the row's rem-based controls a quarter wider than they read, so the widest row measures 531px against the 430px the row had been given. The override is gone (this component has a single clr-modal and nothing inside it renders a dialog of its own).

The tag and label value inputs also grow from 6rem to 10rem: 6rem fits a glob like 1.0* but shows only a fragment of ^v\d+\.\d+\.\d+$, which is the point of the feature. Those two rows carry a decoration select the name row does not, so their content reaches 611px against the 642px the large modal leaves beside the label column, and the row is 620px. Matching the name field's 12rem would not fit — the row would want 651px. The class is defined in this component and Angular scopes it here, so the export-cve dialog that uses the same class name is untouched.

The value input strips whitespace only while a row is on doublestar: whitespace carries no meaning in a glob and the strip forgives a pasted a, b, but a space is a literal character in a regex, so ^(v1|v2) rc$ and [a-z ]+ were being corrupted as the user typed. This matches the same fix in the retention and immutability dialogs on phase 1.

Known and inherited: .filterSelect is a fixed pixel width, as it has been since 2021. Below roughly an 800px viewport the Clarity modal goes fluid while the row does not, so it would clip — the same failure the previous 430px had, at a different breakpoint. Making the row fluid is a change to a shared dialog that this stack does not carry.

Follow-up, deliberately not in this PR

regexp.Regexp.LiteralPrefix() could recover a literal prefix from a regex (library/.*library/) and give the adapters back a narrowing hint, the way IsSpecificPath does for globs. Today a regex name filter costs a full catalog walk, which is the same cost as a ** glob, so this is an optimization rather than a correctness gap. It needs its own design and its own tests.

Testing

  • go test ./lib/pattern/... ./lib/selector/... ./pkg/reg/... ./pkg/retention/... ./pkg/immutable/... ./controller/replication/... ./controller/immutable/... ./server/v2.0/handler/ — pass, so the retention, immutability and proxy paths still behave after the refactor
  • golangci-lint run ./lib/pattern/... ./lib/selector/... ./pkg/reg/... ./controller/replication/... ./server/v2.0/handler/... — 0 issues
  • task test:lint:api — 0 errors (286 pre-existing warnings)
  • portal: ng build --aot, ng lint clean, ng test --include='**/replication/**/*.spec.ts' — 16 pass, bun run i18n:check adds no missing key (23 pre-existing gaps in ru-ru, unchanged)

src/lib/pattern/regex_test.go covers the shared engine: anchoring, the anchoring-escape pattern, the 512 character cap counted in characters rather than bytes, an inline flag staying scoped to the wrapped group, compile-once caching and the cached compile error. The rest cover Filter.Validate (kind combinations, resource + kind rejected, the anchoring-escape pattern, the 512 character cap), the repository and artifact filters under regex (anchoring, excludes, untagged in both engines, labels), a gitlab fetch driven by regex name and tag filters, IsSpecificPath* refusing a regex, the handler conversion, and the round trip of kind through the stored policy including the legacy-kind case.

src/server/v2.0/handler/replication_test.go was already unformatted on main; since this PR adds a case to it, the file is gofmt'd here, which is most of its diff.

Release Notes

Replication filters can now be written as regular expressions. Each name, tag and label filter of a replication rule carries a pattern engine, Doublestar (the default, unchanged) or RegEx, selectable in the rule dialog and set through the kind field of the API. A regular expression has to match the whole repository name, tag or label, and is validated when the rule is saved.

@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 59 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: 79594add-d0c1-429c-a47f-f91501411559

📥 Commits

Reviewing files that changed from the base of the PR and between 1567ce6 and 5daba00.

📒 Files selected for processing (47)
  • api/v2.0/swagger.yaml
  • src/controller/replication/model/model.go
  • src/controller/replication/model/model_test.go
  • src/lib/pattern/regex.go
  • src/lib/pattern/regex_test.go
  • src/lib/selector/selectors/regexp/selector.go
  • src/lib/selector/selectors/regexp/selector_test.go
  • src/pkg/reg/adapter/aliacr/adapter.go
  • src/pkg/reg/adapter/dockerhub/adapter.go
  • src/pkg/reg/adapter/dockerhub/adapter_test.go
  • src/pkg/reg/adapter/githubcr/adapter.go
  • src/pkg/reg/adapter/gitlab/adapter.go
  • src/pkg/reg/adapter/gitlab/adapter_test.go
  • src/pkg/reg/adapter/harbor/base/adapter.go
  • src/pkg/reg/adapter/jfrog/adapter.go
  • src/pkg/reg/adapter/native/adapter.go
  • src/pkg/reg/adapter/tencentcr/artifact_registry.go
  • src/pkg/reg/adapter/tencentcr/artifact_registry_test.go
  • src/pkg/reg/adapter/volcenginecr/artifact_registry.go
  • src/pkg/reg/adapter/volcenginecr/volccr.go
  • src/pkg/reg/adapter/volcenginecr/volccr_test.go
  • src/pkg/reg/filter/artifact.go
  • src/pkg/reg/filter/artifact_test.go
  • src/pkg/reg/filter/repository.go
  • src/pkg/reg/filter/repository_test.go
  • src/pkg/reg/model/policy.go
  • src/pkg/reg/model/policy_test.go
  • src/pkg/reg/util/pattern.go
  • src/pkg/reg/util/pattern_test.go
  • src/portal/src/app/base/left-side-nav/replication/replication.ts
  • src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.html
  • src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.scss
  • src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.spec.ts
  • src/portal/src/app/base/left-side-nav/replication/replication/create-edit-rule/create-edit-rule.component.ts
  • src/portal/src/app/shared/services/interface.ts
  • src/portal/src/i18n/lang/de-de-lang.json
  • src/portal/src/i18n/lang/en-us-lang.json
  • src/portal/src/i18n/lang/es-es-lang.json
  • src/portal/src/i18n/lang/fr-fr-lang.json
  • src/portal/src/i18n/lang/ko-kr-lang.json
  • src/portal/src/i18n/lang/pt-br-lang.json
  • src/portal/src/i18n/lang/ru-ru-lang.json
  • src/portal/src/i18n/lang/tr-tr-lang.json
  • src/portal/src/i18n/lang/zh-cn-lang.json
  • src/portal/src/i18n/lang/zh-tw-lang.json
  • src/server/v2.0/handler/replication.go
  • src/server/v2.0/handler/replication_test.go

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found and verified against the latest diff

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/portal/src/i18n/lang/ko-kr-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/ko-kr-lang.json:58">
P2: The new regex-filter keys added to the Korean locale carry English values, so Korean UI users will see English text for the pattern-engine select and the regex tooltips while every other string in this file is Korean. Translate the six added keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX at lines 58-60 and PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX at lines 627-629) instead of dropping in the en-US strings.</violation>
</file>

<file name="src/pkg/reg/adapter/gitlab/adapter.go">

<violation number="1" location="src/pkg/reg/adapter/gitlab/adapter.go:244">
P2: When a GitLab policy contains an invalid regex that bypassed write-time validation, `Matcher.Match` returns an error but this loop discards it and continues. Return the matcher error through `existPatterns` and `FetchArtifacts` (and do the same for tag matching) so replication fails visibly instead of silently omitting content.</violation>
</file>

<file name="src/portal/src/i18n/lang/zh-cn-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/zh-cn-lang.json:58">
P3: The Simplified Chinese locale file now serves these new strings in English (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX), so Chinese users will see English tooltips and selectors. Translate the new values to Simplified Chinese, mirroring de-de-lang.json which already localized the same keys.</violation>
</file>

<file name="src/portal/src/i18n/lang/zh-tw-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/zh-tw-lang.json:58">
P3: The new UI strings added to the Traditional Chinese locale file are in English, not translated to zh-TW. This file is otherwise fully localized (the adjacent NAME_FILTER/TAG_FILTER/LABEL_FILTER at lines 55-57 are Chinese), so Traditional Chinese users will see untranslated English text for the new RegEx pattern controls and tooltips: NAME_FILTER_REGEX (58), TAG_FILTER_REGEX (59), LABEL_FILTER_REGEX (60), PATTERN_ENGINE (627), plus the REPLICATION section copies at 1352-1354. Translate these keys (PATTERN_ENGINE → "模式引擎"; ENGINE_DOUBLESTAR/ENGINE_REGEX can stay as-is) and provide zh-TW tooltip text for the three _REGEX keys.</violation>
</file>

<file name="src/portal/src/i18n/lang/pt-br-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/pt-br-lang.json:58">
P2: The new regex-filter i18n keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) were added to the pt-br file with English values. Every surrounding string in this Portuguese file is translated, so pt-BR users will see English tooltips and the "Pattern engine" label for the new replication feature. Translate these values to Portuguese (e.g. NAME_FILTER_REGEX: "Filtrar o nome do recurso com uma expressão regular que deve corresponder ao nome completo.", PATTERN_ENGINE: "Mecanismo de padrão").</violation>
</file>

<file name="src/portal/src/i18n/lang/tr-tr-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/tr-tr-lang.json:58">
P3: The newly added user-facing strings (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) are left untranslated in English in the Turkish locale, even though the surrounding TOOLTIP/REPLICATION keys are translated into Turkish. Non-Turkish users will see these controls and tooltips in English, which is inconsistent with the rest of the dialog (e.g. NAME_FILTER/TAG_FILTER right above are Turkish). The German locale in this same PR (de-de-lang.json:58,627-629) did translate the same keys, so translating them here is expected.</violation>
</file>

<file name="src/portal/src/i18n/lang/es-es-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/es-es-lang.json:58">
P3: The new keys added to the Spanish file (es-es) are untranslated English strings, so Spanish-language portal users will see English for the regex filter tooltips (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX) and the pattern-engine dropdown (PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX). The neighboring NAME_FILTER/TAG_FILTER keys are Spanish, and the German file de-de-lang.json:58-60 receives German text for these same keys, so Spanish translations are expected here. Provide Spanish translations for all six added values.</violation>
</file>

<file name="src/pkg/reg/model/policy.go">

<violation number="1" location="src/pkg/reg/model/policy.go:94">
P2: When a label filter is loaded from a stored policy, parseFilters rewrites Value to []string, but Filter.Validate's label branch asserts f.Value.([]any). A kind=regex label filter configured at request time validates (JSON unmarshals to []any), but once re-loaded from storage and re-validated it fails the type assertion before the new regex validation runs. Normalize label Value to one type across validation and the parse/round-trip path (or assert []string in Validate), and add a stored->validated round-trip test for a regex label filter.</violation>
</file>

<file name="api/v2.0/swagger.yaml">

<violation number="1" location="api/v2.0/swagger.yaml:7751">
P3: The new `kind` field is documented as accepting exactly `doublestar` or `regex`, but the spec defines no `enum`. Backend validation in `model.Filter.Validate` rejects other values case-sensitively at write time, so downstream consumers (harbor-cli, terraform provider, SDKs generated from this spec) get no client-side constraint and can easily send a value that the server silently 400s. Add the enum to make the allowed set explicit and self-documenting.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

"NAME_FILTER": "리소스 이름을 필터링합니다. 모두 일치시키려면 비워두거나 '**'을 입력하세요. 'library/**'는 'library' 아래의 리소스만 일치합니다. 더 많은 패턴에 대해서는 사용자 가이드를 참조하세요.",
"TAG_FILTER": "리소스의 태그/버전 부분을 필터링합니다. 모두 일치시키려면 비워 두거나 '**'를 사용하세요. '1.0*'은 '1.0'으로 시작하는 태그에만 일치합니다. 더 많은 패턴에 대해서는 사용자 가이드를 참조하세요.",
"LABEL_FILTER": "라벨에 따라 리소스를 필터링합니다.",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new regex-filter keys added to the Korean locale carry English values, so Korean UI users will see English text for the pattern-engine select and the regex tooltips while every other string in this file is Korean. Translate the six added keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX at lines 58-60 and PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX at lines 627-629) instead of dropping in the en-US strings.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/ko-kr-lang.json, line 58:

<comment>The new regex-filter keys added to the Korean locale carry English values, so Korean UI users will see English text for the pattern-engine select and the regex tooltips while every other string in this file is Korean. Translate the six added keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX at lines 58-60 and PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX at lines 627-629) instead of dropping in the en-US strings.</comment>

<file context>
@@ -55,6 +55,9 @@
         "NAME_FILTER": "리소스 이름을 필터링합니다. 모두 일치시키려면 비워두거나 '**'을 입력하세요. 'library/**'는 'library' 아래의 리소스만 일치합니다. 더 많은 패턴에 대해서는 사용자 가이드를 참조하세요.",
         "TAG_FILTER": "리소스의 태그/버전 부분을 필터링합니다. 모두 일치시키려면 비워 두거나 '**'를 사용하세요. '1.0*'은 '1.0'으로 시작하는 태그에만 일치합니다. 더 많은 패턴에 대해서는 사용자 가이드를 참조하세요.",
         "LABEL_FILTER": "라벨에 따라 리소스를 필터링합니다.",
+        "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

if ok, _ := util.Match(strings.ToLower(pathPattern), strings.ToLower(path)); ok {
if len(matchers) > 0 {
for _, matcher := range matchers {
if ok, _ := matcher.Match(path); ok {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a GitLab policy contains an invalid regex that bypassed write-time validation, Matcher.Match returns an error but this loop discards it and continues. Return the matcher error through existPatterns and FetchArtifacts (and do the same for tag matching) so replication fails visibly instead of silently omitting content.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pkg/reg/adapter/gitlab/adapter.go, line 244:

<comment>When a GitLab policy contains an invalid regex that bypassed write-time validation, `Matcher.Match` returns an error but this loop discards it and continues. Return the matcher error through `existPatterns` and `FetchArtifacts` (and do the same for tag matching) so replication fails visibly instead of silently omitting content.</comment>

<file context>
@@ -222,11 +237,11 @@ func (a *adapter) getProjectsByPattern(pattern string) ([]*Project, error) {
-			if ok, _ := util.Match(strings.ToLower(pathPattern), strings.ToLower(path)); ok {
+	if len(matchers) > 0 {
+		for _, matcher := range matchers {
+			if ok, _ := matcher.Match(path); ok {
 				correct = true
 				break
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as it is, for now. Discarding the match error is the shape the code already had: the previous line was if ok, _ := util.Match(...) and existPatterns has always swallowed it, so this PR is not introducing the behaviour, only carrying it. It is also unreachable through the API, since a pattern that fails to compile is rejected when the policy is saved, and a stored policy cannot acquire an invalid pattern afterwards.

Threading the error out of existPatterns and the tag loop is worth doing, but it changes the signature of a function on the fetch path for every kind, which belongs in its own change rather than in a feature PR where it would be indistinguishable from the feature. Noted as a follow-up.

"NAME_FILTER": "Filtrar por nome de recurso. Deixe vazio ou use '**' para ver todos. A expressão 'library/**' seleciona recursos dentro de 'library'. Para mais detalhes, confira a documentação.",
"TAG_FILTER": "Filtrar por tag de cada recurso. Deixe vazio ou use '**' para ver todas. A expressão '1.0*' seleciona todas as tags que começam com 1.0. Para mais detalhes, confira a documentação.",
"LABEL_FILTER": "Filtrar por marcadores.",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new regex-filter i18n keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) were added to the pt-br file with English values. Every surrounding string in this Portuguese file is translated, so pt-BR users will see English tooltips and the "Pattern engine" label for the new replication feature. Translate these values to Portuguese (e.g. NAME_FILTER_REGEX: "Filtrar o nome do recurso com uma expressão regular que deve corresponder ao nome completo.", PATTERN_ENGINE: "Mecanismo de padrão").

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/pt-br-lang.json, line 58:

<comment>The new regex-filter i18n keys (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) were added to the pt-br file with English values. Every surrounding string in this Portuguese file is translated, so pt-BR users will see English tooltips and the "Pattern engine" label for the new replication feature. Translate these values to Portuguese (e.g. NAME_FILTER_REGEX: "Filtrar o nome do recurso com uma expressão regular que deve corresponder ao nome completo.", PATTERN_ENGINE: "Mecanismo de padrão").</comment>

<file context>
@@ -55,6 +55,9 @@
         "NAME_FILTER": "Filtrar por nome de recurso. Deixe vazio ou use '**' para ver todos. A expressão 'library/**' seleciona recursos dentro de 'library'. Para mais detalhes, confira a documentação.",
         "TAG_FILTER": "Filtrar por tag de cada recurso. Deixe vazio ou use '**' para ver todas. A expressão '1.0*' seleciona todas as tags que começam com 1.0. Para mais detalhes, confira a documentação.",
         "LABEL_FILTER": "Filtrar por marcadores.",
+        "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

}
for _, label := range labels {
_, ok := label.(string)
value, ok := label.(string)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a label filter is loaded from a stored policy, parseFilters rewrites Value to []string, but Filter.Validate's label branch asserts f.Value.([]any). A kind=regex label filter configured at request time validates (JSON unmarshals to []any), but once re-loaded from storage and re-validated it fails the type assertion before the new regex validation runs. Normalize label Value to one type across validation and the parse/round-trip path (or assert []string in Validate), and add a stored->validated round-trip test for a regex label filter.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/pkg/reg/model/policy.go, line 94:

<comment>When a label filter is loaded from a stored policy, parseFilters rewrites Value to []string, but Filter.Validate's label branch asserts f.Value.([]any). A kind=regex label filter configured at request time validates (JSON unmarshals to []any), but once re-loaded from storage and re-validated it fails the type assertion before the new regex validation runs. Normalize label Value to one type across validation and the parse/round-trip path (or assert []string in Validate), and add a stored->validated round-trip test for a regex label filter.</comment>

<file context>
@@ -54,25 +70,35 @@ func (f *Filter) Validate() error {
 		}
 		for _, label := range labels {
-			_, ok := label.(string)
+			value, ok := label.(string)
 			if !ok {
 				return errors.New(nil).WithCode(errors.BadRequestCode).
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this. The type split is real but pre-existing: Validate's label branch has always asserted []any, because it only ever runs on API input, where the value arrives from JSON. parseFilters rewrites the value to []string on the way out of storage, and nothing re-validates a policy loaded from storage, so the assertion this describes is never reached.

The regex validation sits behind the same assertion and inherits exactly the same reachability, so it adds no new exposure. Normalising the two representations would mean touching Validate, parseFilters and every caller that consumes Filter.Value, which is a refactor of the filter model rather than part of adding a kind.

Comment thread src/server/v2.0/handler/replication.go Outdated
"NAME_FILTER": "篩選資源名稱。不填寫、或使用「**」可選取所有資源。「library/**」僅選取「library」下的資源。更多模式請參考使用者手冊。",
"TAG_FILTER": "篩選資源的標籤/版本。不填寫、或使用「**」可選取所有標籤/版本。「1.0*」僅選取以「1.0」開頭的標籤。更多模式請參考使用者手冊。",
"LABEL_FILTER": "根據標籤篩選資源。",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new UI strings added to the Traditional Chinese locale file are in English, not translated to zh-TW. This file is otherwise fully localized (the adjacent NAME_FILTER/TAG_FILTER/LABEL_FILTER at lines 55-57 are Chinese), so Traditional Chinese users will see untranslated English text for the new RegEx pattern controls and tooltips: NAME_FILTER_REGEX (58), TAG_FILTER_REGEX (59), LABEL_FILTER_REGEX (60), PATTERN_ENGINE (627), plus the REPLICATION section copies at 1352-1354. Translate these keys (PATTERN_ENGINE → "模式引擎"; ENGINE_DOUBLESTAR/ENGINE_REGEX can stay as-is) and provide zh-TW tooltip text for the three _REGEX keys.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/zh-tw-lang.json, line 58:

<comment>The new UI strings added to the Traditional Chinese locale file are in English, not translated to zh-TW. This file is otherwise fully localized (the adjacent NAME_FILTER/TAG_FILTER/LABEL_FILTER at lines 55-57 are Chinese), so Traditional Chinese users will see untranslated English text for the new RegEx pattern controls and tooltips: NAME_FILTER_REGEX (58), TAG_FILTER_REGEX (59), LABEL_FILTER_REGEX (60), PATTERN_ENGINE (627), plus the REPLICATION section copies at 1352-1354. Translate these keys (PATTERN_ENGINE → "模式引擎"; ENGINE_DOUBLESTAR/ENGINE_REGEX can stay as-is) and provide zh-TW tooltip text for the three _REGEX keys.</comment>

<file context>
@@ -55,6 +55,9 @@
         "NAME_FILTER": "篩選資源名稱。不填寫、或使用「**」可選取所有資源。「library/**」僅選取「library」下的資源。更多模式請參考使用者手冊。",
         "TAG_FILTER": "篩選資源的標籤/版本。不填寫、或使用「**」可選取所有標籤/版本。「1.0*」僅選取以「1.0」開頭的標籤。更多模式請參考使用者手冊。",
         "LABEL_FILTER": "根據標籤篩選資源。",
+        "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

"NAME_FILTER": "Kaynağın adını filtreleyin. Boş bırakın veya hepsine uyacak şekilde '**' kullanın. 'library / **' sadece 'library' altındaki kaynaklarla eşleşiyor. Daha fazla desen için lütfen kullanım kılavuzuna bakın.",
"TAG_FILTER": "Kaynakların etiket / sürüm bölümünü filtreleyin. Boş bırakın veya hepsine uyacak şekilde '**' kullanın. '1.0 *' sadece '1.0' ile başlayan etiketlerle eşleşir. Daha fazla desen için lütfen kullanım kılavuzuna bakın.",
"LABEL_FILTER": "Kaynakları etiketlere göre filtreleyin.",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The newly added user-facing strings (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) are left untranslated in English in the Turkish locale, even though the surrounding TOOLTIP/REPLICATION keys are translated into Turkish. Non-Turkish users will see these controls and tooltips in English, which is inconsistent with the rest of the dialog (e.g. NAME_FILTER/TAG_FILTER right above are Turkish). The German locale in this same PR (de-de-lang.json:58,627-629) did translate the same keys, so translating them here is expected.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/tr-tr-lang.json, line 58:

<comment>The newly added user-facing strings (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX, PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX) are left untranslated in English in the Turkish locale, even though the surrounding TOOLTIP/REPLICATION keys are translated into Turkish. Non-Turkish users will see these controls and tooltips in English, which is inconsistent with the rest of the dialog (e.g. NAME_FILTER/TAG_FILTER right above are Turkish). The German locale in this same PR (de-de-lang.json:58,627-629) did translate the same keys, so translating them here is expected.</comment>

<file context>
@@ -55,6 +55,9 @@
         "NAME_FILTER": "Kaynağın adını filtreleyin. Boş bırakın veya hepsine uyacak şekilde '**' kullanın. 'library / **' sadece 'library' altındaki kaynaklarla eşleşiyor. Daha fazla desen için lütfen kullanım kılavuzuna bakın.",
         "TAG_FILTER": "Kaynakların etiket / sürüm bölümünü filtreleyin. Boş bırakın veya hepsine uyacak şekilde '**' kullanın. '1.0 *' sadece '1.0' ile başlayan etiketlerle eşleşir. Daha fazla desen için lütfen kullanım kılavuzuna bakın.",
         "LABEL_FILTER": "Kaynakları etiketlere göre filtreleyin.",
+        "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

Comment thread src/controller/replication/model/model_test.go
"NAME_FILTER": "Filtrar el nombre del recurso. Dejar vacio o usar '**' para todos. 'library/**' solo busca recursos en 'library'. Para más patrones, por favor refierase a la guía de usuario.",
"TAG_FILTER": "Filtar parte tag/version de los recursos. Dejar vacio o usar '**' para todos. '1.0*' solo busca recursos con tags que comienzan con '1.0'. Para más patrones, por favor refierase a la guía de usuario.",
"LABEL_FILTER": "Filtrar los recursos por labels.",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new keys added to the Spanish file (es-es) are untranslated English strings, so Spanish-language portal users will see English for the regex filter tooltips (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX) and the pattern-engine dropdown (PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX). The neighboring NAME_FILTER/TAG_FILTER keys are Spanish, and the German file de-de-lang.json:58-60 receives German text for these same keys, so Spanish translations are expected here. Provide Spanish translations for all six added values.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/es-es-lang.json, line 58:

<comment>The new keys added to the Spanish file (es-es) are untranslated English strings, so Spanish-language portal users will see English for the regex filter tooltips (NAME_FILTER_REGEX, TAG_FILTER_REGEX, LABEL_FILTER_REGEX) and the pattern-engine dropdown (PATTERN_ENGINE, ENGINE_DOUBLESTAR, ENGINE_REGEX). The neighboring NAME_FILTER/TAG_FILTER keys are Spanish, and the German file de-de-lang.json:58-60 receives German text for these same keys, so Spanish translations are expected here. Provide Spanish translations for all six added values.</comment>

<file context>
@@ -55,6 +55,9 @@
         "NAME_FILTER": "Filtrar el nombre del recurso. Dejar vacio o usar '**' para todos. 'library/**' solo busca recursos en 'library'. Para más patrones, por favor refierase a la guía de usuario.",
         "TAG_FILTER": "Filtar parte tag/version de los recursos. Dejar vacio o usar '**' para todos. '1.0*' solo busca recursos con tags que comienzan con '1.0'. Para más patrones, por favor refierase a la guía de usuario.",
         "LABEL_FILTER": "Filtrar los recursos por labels.",
+        "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

Comment thread api/v2.0/swagger.yaml
decoration:
type: string
description: 'matches or excludes the result'
kind:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The new kind field is documented as accepting exactly doublestar or regex, but the spec defines no enum. Backend validation in model.Filter.Validate rejects other values case-sensitively at write time, so downstream consumers (harbor-cli, terraform provider, SDKs generated from this spec) get no client-side constraint and can easily send a value that the server silently 400s. Add the enum to make the allowed set explicit and self-documenting.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At api/v2.0/swagger.yaml, line 7751:

<comment>The new `kind` field is documented as accepting exactly `doublestar` or `regex`, but the spec defines no `enum`. Backend validation in `model.Filter.Validate` rejects other values case-sensitively at write time, so downstream consumers (harbor-cli, terraform provider, SDKs generated from this spec) get no client-side constraint and can easily send a value that the server silently 400s. Add the enum to make the allowed set explicit and self-documenting.</comment>

<file context>
@@ -7748,6 +7748,12 @@ definitions:
       decoration:
         type: string
         description: 'matches or excludes the result'
+      kind:
+        type: string
+        description: |-
</file context>

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deliberate. An enum in the spec makes every future kind a breaking change for generated clients: an SDK built against today's spec rejects a value a newer server accepts, and the failure surfaces in the client rather than at the API boundary that owns the rule. The allowed values are named in the description, and the server validates them, returning 400 with the offending value.

This also matches the neighbouring feature: the retention and immutability selector kind is free-form in the same spec, so adding an enum on only the replication filter would make the two inconsistent.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 existing issue remains and 1 new issue found across 25 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="src/portal/src/i18n/lang/fr-fr-lang.json">

<violation number="1" location="src/portal/src/i18n/lang/fr-fr-lang.json:59">
P3: The change to the French locale update leaves the TAG_FILTER_REGEX tooltip in English instead of translating it, so French-portal users still see an English description. The rest of this file is translated, and the PR's own de-de-lang.json translates this same key, so keeping fr-fr in English is an inconsistency. Translate the added sentence, e.g. "Un artefact sans tag n'est conservé que si l'expression correspond au tag vide, comme '.*' le fait."</violation>
</file>

Requires human review: Auto-approval blocked because this review re-detected 3 unresolved issues already reported by Cubic.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread src/lib/pattern/regex.go Outdated
Comment thread src/pkg/reg/filter/artifact.go
"TAG_FILTER": "Filtrer la partie tag/version des ressources. Laissez vide ou utilisez '**' pour tout inclure. '1.0*' n'inclut que les ressources qui commencent par '1.0'. Pour plus de patterns, référez-vous au guide d'utilisation.",
"LABEL_FILTER": "Filtrer les ressources en fonction des labels.",
"NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
"TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. An untagged artifact is kept only when the expression matches the empty tag, as '.*' does.",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The change to the French locale update leaves the TAG_FILTER_REGEX tooltip in English instead of translating it, so French-portal users still see an English description. The rest of this file is translated, and the PR's own de-de-lang.json translates this same key, so keeping fr-fr in English is an inconsistency. Translate the added sentence, e.g. "Un artefact sans tag n'est conservé que si l'expression correspond au tag vide, comme '.*' le fait."

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/portal/src/i18n/lang/fr-fr-lang.json, line 59:

<comment>The change to the French locale update leaves the TAG_FILTER_REGEX tooltip in English instead of translating it, so French-portal users still see an English description. The rest of this file is translated, and the PR's own de-de-lang.json translates this same key, so keeping fr-fr in English is an inconsistency. Translate the added sentence, e.g. "Un artefact sans tag n'est conservé que si l'expression correspond au tag vide, comme '.*' le fait."</comment>

<file context>
@@ -56,7 +56,7 @@
         "LABEL_FILTER": "Filtrer les ressources en fonction des labels.",
         "NAME_FILTER_REGEX": "Filter the name of the resource with a regular expression that has to match the full name. Leave empty to match all. 'library/.*' only matches resources under 'library'.",
-        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. Untagged artifacts never match a regular expression.",
+        "TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. An untagged artifact is kept only when the expression matches the empty tag, as '.*' does.",
         "LABEL_FILTER_REGEX": "Filter the resources according to labels, every expression has to match a full label name.",
         "RESOURCE_FILTER": "Filtrer le type de ressources.",
</file context>
Suggested change
"TAG_FILTER_REGEX": "Filter the tag/version part of the resources with a regular expression that has to match the full tag. Leave empty to match all. 'v\\d+\\.\\d+' only matches tags such as 'v1.0'. An untagged artifact is kept only when the expression matches the empty tag, as '.*' does.",
"TAG_FILTER_REGEX": "Filtrer la partie tag/version des ressources avec une expression régulière qui doit correspondre à tout le tag. Laissez vide pour tout inclure. 'v\\d+\\.\\d+' ne correspond qu'aux tags comme 'v1.0'. Un artefact sans tag n'est conservé que si l'expression correspond au tag vide, comme '.*' le fait.",

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not changing this, deliberately. German was hand-translated by a German speaker on the team; the other locales carry the English string rather than machine translations nobody here can review. These files are maintained by their language communities upstream, and an unreviewed translation is harder to correct later than an obvious English placeholder, because it looks finished. The key is in place, so a translator only has to fill in the value.

Side note: the tooltip text this thread quotes has since changed. TAG_FILTER_REGEX no longer says untagged artifacts never match a regular expression, because that behaviour was corrected: an untagged artifact is kept exactly when the pattern matches the empty tag, the same rule doublestar follows.

Comment thread src/lib/selector/selectors/regexp/selector_test.go Outdated
Vad1mo added 7 commits August 27, 2026 22:40
Adds an optional "kind" to the name, tag and label filters of a replication
policy. It selects the pattern engine for the filter value: "doublestar", the
default and the only behavior so far, or "regex", the anchored full string
matching of the selector added in phase 1.

The compile and anchor step is the one from
src/lib/selector/selectors/regexp, so a pattern that would escape the
anchoring, such as `foo)|(?:bar`, is rejected here as well, at write time.

util.Matcher carries the kind to every place a filter pattern is evaluated,
compiling a regex once per filter instead of once per candidate. The adapters
that reverse a glob into an explicit repository or namespace list
(IsSpecificPath, IsSpecificPathComponent) fall back to listing the catalog for
a regex, since the glob syntax says nothing about a regular expression: doing
otherwise would silently take "lib(a|b)" for a literal namespace.

Two deliberate asymmetries with doublestar: an untagged artifact is never
matched by running the expression against the empty string, and the gitlab
adapter, which compares paths case insensitively, leaves a regex unfolded
because lowercasing rewrites classes such as \D or [A-Z].

The stored filters already carry a "kind" field, which names the pre-1.10
repository/tag/label filter on entries without a type, so parseFilters only
reads it as a pattern engine on typed entries.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Each pattern filter of a replication rule (name, tag, label) gets a Doublestar
/ RegEx select next to its value, following the retention and immutability
dialogs of phase 1. The resource filter picks a type rather than matching a
pattern, so it has no engine.

The engine is left out of the payload while it is the default, which keeps the
policies of existing rules untouched until someone switches one to RegEx. The
info tooltip of a filter follows the selected engine.

The dialog grows to the large modal size: the filter row now holds four
controls and no longer fits the medium one.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
src/lib/pattern already holds the kind literals and the doublestar/regex
matching of the proxy cache repository filter (goharbor/harbor#23527), so it is
where the engine belongs rather than in a selector package the replication
filters would have to reach into.

CompileRegex and ValidateRegex are the phase-1 compile and anchor step, cap
included, moved verbatim: a pattern is compiled bare before it is wrapped, so
that one which would escape the anchoring is rejected instead of silently
matching on a prefix. Matcher adds the caching the selector had, so a filter
applied to thousands of candidates compiles once, and makes it available to any
caller that carries a kind.

The regex selector of phase 1 now delegates: its Kind, MaxPatternLength,
Compile and Validate are aliases of the shared ones, and it holds a Matcher
instead of its own sync.Once. The replication filters and the adapters take the
same Matcher, so src/pkg/reg no longer defines a second one.

Match, ValidateKind and ValidateRepositoryFilter keep their current behavior and
the proxy cache middleware is untouched: moving that path onto the cached
Matcher, and onto a single doublestar behavior, is a follow-up.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
A replication tag filter has no untagged flag: an artifact without tags is kept
exactly when the pattern matches the empty tag, which is why "**" keeps it and
"v*" drops it. An anchored regex gives the same rule for free, ".*" matches the
empty string and "v.*" does not, so the special case that refused to evaluate a
regex against "" only made the two engines disagree on the same policy.

The rule belongs to the retention and immutability selectors, where an untagged
flag governs and running the expression against "" would bypass it. Nothing here
does.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
The clrModalSize="lg" added with the engine select was inert: a rule in this
component pinned div.modal-dialog to 32rem through ::ng-deep, which outranks
Clarity's .modal-dialog.modal-lg on specificity. The dialog kept rendering at
its old width and the three pattern filter inputs were clipped, the tag and
label ones down to a barely usable sliver.

The override governed nothing else. This component holds a single clr-modal and
no child inside it renders a dialog of its own, so removing the rule lets the
declared size apply and leaves other components untouched.

The row width also had to grow. Clarity sets a 20px root, so the rem based
widths of the row are a quarter wider than they look: the widest row now
measures 531px against the 430px the row was given, which clipped its own
content whatever the dialog did. 560px clears the content and still leaves
about 60px inside the space the large modal offers next to the label column.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
The tag and label value inputs were 6rem, which fits a glob like "1.0*" but
shows only a fragment of an expression: "^v\d+\.\d+\.\d+$" is already wider
than the field. Both grow to 10rem, 200px at Clarity's 20px root, which holds
that expression at less than two thirds of the field.

The row grows with them. Those two rows carry a decoration select the name row
does not, so their content reaches 611px, against the 642px the large modal
leaves beside the label column; 620px covers the content and keeps the row
inside the dialog. Matching the name field's 12rem would not fit, the row would
want 651px.

The class is defined in this component and Angular scopes it here, so the
export-cve dialog that uses the same class name is unaffected.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Four findings from the review on #753.

An explicit kind of "doublestar" was echoed back in the response while the
identical filter created without a kind had the field omitted, so a client that
spells out the default saw a shape it never sent and read it as drift. Both
spellings now collapse to the same absent field.

Matcher.Match fell back to doublestar for any kind it did not recognise. No
stored filter can carry another kind today, between write-time validation and
the guard on the legacy kind field, but a pattern written for some other engine
would have selected the wrong repositories rather than failing, so an unknown
kind is now an error.

The round-trip test asserted the kind survived storage but never the pattern
itself, which is the part whose corruption would quietly change what a policy
selects.

TestCompileIsCached no longer tested caching once the compiled expression moved
into the matcher: the selector never reassigns that pointer, so the assertion
held whatever the cache did. The behaviour is covered by TestMatcherCompilesOnce
in lib/pattern, which can see the compiled expression, so the test is dropped
rather than left as a tautology.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@Vad1mo
Vad1mo force-pushed the feat/regex-filters-phase2 branch from eb088b0 to 06a99e9 Compare August 27, 2026 20:43
The filter value input stripped every whitespace character on each keystroke.
That is right for a doublestar pattern, where whitespace carries no meaning and
the strip forgives a pasted list like "a, b", but a space is a literal
character in a regex: "^(v1|v2) rc$" and "[a-z ]+" lost the space as the user
typed it, leaving a pattern that silently matched something else.

The strip now applies to a row only while that row is on doublestar, matching
the fix made for the retention and immutability dialogs.

Signed-off-by: Vadim Bauer <vb@container-registry.com>
Vad1mo added a commit that referenced this pull request Sep 10, 2026
…810)

Preview artifacts now follow what a pull request actually changes,
including across a GitHub stack: the seven images when image inputs
change, the chart when chart inputs change, always from the top of a
stack. Same change as container-registry/harbor-scanner-trivy#92,
applied to the inline seven-image workflow, plus the chart preview this
repo did not have.

## Problem

On a native stack the trigger's `paths` filter saw only each PR's own
slice: the top PR, the only one worth deploying, got no images when its
slice was docs-only, while every lower PR rebuilt all fourteen platform
images on each `gh stack push`. The chart had no preview at all; a chart
change could only be tried after a release.

## How it worked before

`pr-ci.yml` filtered with `on.pull_request.paths`, evaluated against the
PR's own diff; `build`, `merge`, `sign` and `pr-comment` each repeated
the same `if:` and ran for every position of a stack.

## How it works now

- A workflow publishes when the PR's diff against `main` touches one of
its inputs. The check is a job, not a `paths` trigger filter, so every
PR gets a status and the diff is the real one.
- Stacked PRs (`gh stack`): only the top PR publishes; its head is the
whole stack, so its ordinary `pr-<N>` artifacts are the stack artifacts.
Lower PRs are skipped at the gate. Plain PRs behave as before.
- The workflows also subscribe to the `stacked` activity type: GitHub
opens the PRs of a stack before it links them, so `opened` never carries
`pull_request.stack`; the link does. Verified on trivy stack #96:
linking fired both previews on the top with no push (gate logged
`event=stacked stack=96`), the bottom was skipped.
- Image and chart are independent: a chart-only PR gets a chart, an
app-only PR gets an image, both when both change, nothing when neither
does.
- `pr-ci.yml`: the 15-entry allowlist moves into a `changes` job; the
four jobs depend on it. The comment lists the stack members when
stacked.
- New `pr-chart.yml`: `<chart version>-pr.<N>` into
`PR_REGISTRY_PROJECT` with the `PR_REGISTRY_*` credentials, subcharts
from `Chart.lock`, Artifact Hub annotation against the release project
as on release, signed, sticky comment with the install command.

`build.yml` and `test.yml` are untouched: native stacks already trigger
them as if targeting `main`. Manually chained PRs such as #753 -> #754
are not a stack to GitHub; their bottom PR gets ordinary previews, the
ones above match no `branches` filter. `gh stack init` on those branches
is the fix.

## Verification

- `actionlint` and `zizmor` findings identical to `main` (the
`ubuntu-26.04` label and SC2086 notes are pre-existing); comment scripts
pass `node --check`.
- This PR changes `pr-ci.yml` and adds `pr-chart.yml`, so its own runs
exercise both plain paths: gate, 14 builds, 7 merges, sign, comment;
chart packaged, pushed, signed, commented.
- The stack path was verified on harbor-scanner-trivy with a 2-PR stack
(docs-only top built, bottom skipped, restack rebuilt only the top).


## Preview comments

Each preview comment names the commit it was built from, lists the
inputs that triggered the build, links the sibling preview comment when
one exists, and is rewritten with an outdated notice when a build is
skipped, fails, or the pull request is not the top of its stack. The
notice is delimited by markers, so a second one replaces the first and a
later successful build clears it. Ported from
container-registry/harbor-scanner-trivy#101.

---------

Signed-off-by: Vadim Bauer <vb@container-registry.com>
@bupd

bupd commented Sep 11, 2026

Copy link
Copy Markdown
Member

One process thing that I think matters more than anything in the diff: CI is not running on this PR.

Every substantive workflow in .github/workflows/ is gated on pull_request: branches: [main, "release-*"]. Because this targets feat/regex-selector-phase1, it gets 6 checks — DCO, labeler, typos, lint-actions, CodeRabbit, cubic — against #752's 40. No Go unit tests, no build, no Go lint, no UI lint. Same for #754, which has 5.

So the green tick here means spellcheck passed. Roughly two-thirds of the stack's Go code has never been compiled or tested by CI, and this is the largest diff in the stack (47 files) touching the subsystem with the least unit coverage of real registry behaviour. Each phase needs re-targeting to main and going fully green before it merges, rather than merging on the strength of the current status.

On the code

The engine work looks solid. pattern.CompileRegex caps patterns at 512 runes, compiles the bare pattern first so an anchoring escape like foo)|(?:bar is rejected, then wraps in \A(?:…)\z; Matcher holds a sync.Once compile cache so a policy over thousands of candidates compiles once. Go's RE2 gives linear matching, and I couldn't construct a compile bomb inside the 512-char cap — regexp/syntax rejects nested-repeat expansion, and the worst nesting I could build compiled in well under a millisecond. Write paths need project-admin anyway. No schema change anywhere in the stack, and nothing under make/migrations/ — good.

The one thing I'd want documented before merge

kind on a label filter changes matching strategy, not just syntax. In artifactLabelFilter.matchAll the doublestar/empty branch is exact set-membership (which is what the label filter has always done), while the regex branch is "each configured pattern must match some artifact label". So picking "Doublestar" gives exact matching and picking "RegEx" gives pattern matching — the engine dropdown means something different here than on name/tag. The swagger description ("The pattern engine used for the filter value… valid for the name, tag and label filters") doesn't convey that, and it's the kind of thing that produces a surprised bug report about replication copying the wrong repositories.

Smaller

  • After this PR src/lib/pattern exports both Match() (trims whitespace, pre-validates the glob, no length cap) and Matcher.Match() (no trim, no glob validation, cap applies to regex only). Same package, same apparent purpose, different results for " lib/** " or a malformed glob. You document the divergence and defer it, which is fair, but it's the most likely source of the next bug here.
  • Matcher.Match returns true for an empty pattern before the kind switch, so an unsupported kind with an empty pattern passes silently — inconsistent with the "no silent fallback to doublestar" contract in the method's own doc comment.
  • Response shape: an explicitly-stored "doublestar" now comes back as an absent field, so a client doing read-modify-write sees its explicit value disappear. Benign but observable.
  • The gitlab case-folding asymmetry (newCaseFoldingMatcher folds doublestar but not regex) is well-reasoned but user-visible and undocumented outside the PR body — a glob and its literal regex translation won't match the same GitLab repos.

Not tested

I did not run the Go suites on this head. Given CI runs none of them either, the most valuable next step is: re-target to main, then run go test ./src/pkg/reg/... and exercise a real replication rule with {"type":"name","value":"library/(app|api)-.*","kind":"regex"} against at least the native and harbor adapters, to confirm the IsSpecificPathForKind catalog-listing fallback engages instead of the glob reverse-engineering. That's the path where a wrong answer silently replicates the wrong repositories rather than erroring.

Not a 2.15.9 candidate — feature, plus an adapter-wide signature refactor.

@bupd bupd added prio/P2 No deadline; merges when the queue is clear state/blocked Waiting on another PR, a decision, or an external answer stacked Based on another open PR, not on main. Merge bottom-up labels Sep 14, 2026
@bupd
bupd added this pull request to stack #922 September 16, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

component/api component/portal prio/P2 No deadline; merges when the queue is clear stacked Based on another open PR, not on main. Merge bottom-up state/blocked Waiting on another PR, a decision, or an external answer tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants