diff --git a/README.md b/README.md index 18f12631..aed9dd1e 100644 --- a/README.md +++ b/README.md @@ -278,13 +278,15 @@ remote: bundle: # dmt lint remote : documentation: rules: - changelog: + readme: impact: warn release: # ... the same command, /release: module: rules: release-layout: impact: error + changelog-valid: + impact: error ``` A `remote` section has the same shape as `global.linters-settings` — a linter diff --git a/internal/metrics/repository.go b/internal/metrics/repository.go index 1ac58b96..2a422ec1 100644 --- a/internal/metrics/repository.go +++ b/internal/metrics/repository.go @@ -17,55 +17,13 @@ limitations under the License. package metrics import ( - "path/filepath" "strings" - "gopkg.in/ini.v1" - - "github.com/deckhouse/deckhouse/pkg/log" - - "github.com/deckhouse/dmt/internal/fsutils" + "github.com/deckhouse/dmt/pkg" ) func getRepositoryAddress(dir string) string { - configFile := getGitConfigFile(dir) - if configFile == "" { - return "" - } - - cfg, err := ini.Load(configFile) - if err != nil { - log.Error("Failed to load config file", log.Err(err)) - return "" - } - - sec, err := cfg.GetSection("remote \"origin\"") - if err != nil { - log.Error("Failed to get remote origin", log.Err(err)) - return "" - } - - repositoryULR := sec.Key("url").String() - - return convertToHTTPS(repositoryULR) -} - -func getGitConfigFile(dir string) string { - for { - if fsutils.IsDir(filepath.Join(dir, ".git")) && - fsutils.IsFile(filepath.Join(dir, ".git", "config")) { - return filepath.Join(dir, ".git", "config") - } - - parent := filepath.Dir(dir) - if dir == parent || parent == "" { - break - } - - dir = parent - } - - return "" + return convertToHTTPS(pkg.RepositoryOriginURL(dir)) } func convertToHTTPS(repoURL string) string { diff --git a/internal/modules/module.go b/internal/modules/module.go index 024df21b..522bf755 100644 --- a/internal/modules/module.go +++ b/internal/modules/module.go @@ -367,7 +367,6 @@ func mapDocumentationRules(linterSettings *pkg.LintersSettings, configSettings * // render), so it defaults to error via fallbackImpact — unlike the style/soft // markdownlint and size rules above. A per-rule impact in config still overrides. rules.FrontMatterRule.SetLevel(globalRules.FrontMatterRule.Impact, fallbackImpact) - rules.ChangelogRule.SetLevel(globalRules.ChangelogRule.Impact, fallbackImpact) } func mapModuleRules(linterSettings *pkg.LintersSettings, configSettings *config.LintersSettings, globalConfig *global.Linters) { @@ -388,6 +387,8 @@ func mapModuleRules(linterSettings *pkg.LintersSettings, configSettings *config. rules.ReleaseLayoutRule.SetLevel(globalRules.ReleaseLayoutRule.Impact, fallbackImpact) rules.BundleLayoutRule.SetLevel(globalRules.BundleLayoutRule.Impact, fallbackImpact) rules.HelmignoreCoverageRule.SetLevel(globalRules.HelmignoreCoverageRule.Impact, fallbackImpact) + rules.HasChangelogRule.SetLevel(globalRules.HasChangelogRule.Impact, fallbackImpact) + rules.ChangelogValidRule.SetLevel(globalRules.ChangelogValidRule.Impact, fallbackImpact) } // mapTemplatesRules configures Templates linter rules diff --git a/pkg/config.go b/pkg/config.go index 53a2fdef..9e1afd34 100644 --- a/pkg/config.go +++ b/pkg/config.go @@ -1,8 +1,5 @@ package pkg -// TODO: THINK ABOUT HOW TO ENDURE -var IgnoreDeckhouseReposList = []string{"deckhouse", "deckhouse-test-1", "deckhouse-test-2"} - type LinterConfig struct { Impact *Level } @@ -90,7 +87,6 @@ type DocumentationLinterRules struct { MarkdownlintRule RuleConfig SizeRule RuleConfig FrontMatterRule RuleConfig - ChangelogRule RuleConfig } type NoCyrillicLinterConfig struct { @@ -253,6 +249,8 @@ type ModuleLinterRules struct { ReleaseLayoutRule RuleConfig BundleLayoutRule RuleConfig HelmignoreCoverageRule RuleConfig + HasChangelogRule RuleConfig + ChangelogValidRule RuleConfig } type OSSRuleSettings struct { Disable bool diff --git a/pkg/config/global/global.go b/pkg/config/global/global.go index 7c329076..539bd70f 100644 --- a/pkg/config/global/global.go +++ b/pkg/config/global/global.go @@ -100,7 +100,6 @@ type DocumentationRules struct { MarkdownlintRule RuleConfig `mapstructure:"markdownlint"` SizeRule RuleConfig `mapstructure:"size"` FrontMatterRule RuleConfig `mapstructure:"front-matter"` - ChangelogRule RuleConfig `mapstructure:"changelog"` } type OpenAPILinterConfig struct { @@ -133,6 +132,8 @@ type ModuleLinterRules struct { ReleaseLayoutRule RuleConfig `mapstructure:"release-layout"` BundleLayoutRule RuleConfig `mapstructure:"bundle-layout"` HelmignoreCoverageRule RuleConfig `mapstructure:"helmignore-coverage"` + HasChangelogRule RuleConfig `mapstructure:"has-changelog"` + ChangelogValidRule RuleConfig `mapstructure:"changelog-valid"` } type TemplatesLinterConfig struct { diff --git a/pkg/linters/docs/documentation.go b/pkg/linters/docs/documentation.go index e8f1babf..09df54b1 100644 --- a/pkg/linters/docs/documentation.go +++ b/pkg/linters/docs/documentation.go @@ -59,7 +59,6 @@ func (l *Documentation) rules() []pkg.Rule { rules.NewMarkdownRule(m, errorList.WithMaxLevel(l.cfg.Rules.MarkdownlintRule.GetLevel())), rules.NewSizeRule(m, errorList.WithMaxLevel(l.cfg.Rules.SizeRule.GetLevel())), rules.NewFrontMatterRule(m, errorList.WithMaxLevel(l.cfg.Rules.FrontMatterRule.GetLevel())), - rules.NewChangelogRule(m, errorList.WithMaxLevel(l.cfg.Rules.ChangelogRule.GetLevel())), } } diff --git a/pkg/linters/docs/rules/changelog.go b/pkg/linters/docs/rules/changelog.go deleted file mode 100644 index 70e900cf..00000000 --- a/pkg/linters/docs/rules/changelog.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2025 Flant JSC -// Licensed under the Apache License, Version 2.0 - -package rules - -import ( - "context" - "os" - "path/filepath" - - "github.com/deckhouse/dmt/pkg" - "github.com/deckhouse/dmt/pkg/errors" -) - -const ( - ChangelogRuleName = "changelog" -) - -func NewChangelogRule(m pkg.Module, errorList *errors.LintRuleErrorsList) *ChangelogRule { - return &ChangelogRule{ - RuleMeta: pkg.RuleMeta{ - Name: ChangelogRuleName, - }, - module: m, - errorList: errorList.WithRule(ChangelogRuleName), - } -} - -type ChangelogRule struct { - pkg.RuleMeta - - module pkg.Module - errorList *errors.LintRuleErrorsList -} - -var _ pkg.Rule = (*ChangelogRule)(nil) - -func (r *ChangelogRule) Check(_ context.Context) { - path := filepath.Join(r.module.GetPath(), "changelog.yaml") - errorList := r.errorList.WithFilePath(path) - - info, err := os.Stat(path) - - switch { - case os.IsNotExist(err): - errorList.Error("changelog.yaml file is missing") - case err != nil: - errorList.WithValue(err.Error()).Error("failed to check changelog.yaml file") - case info.Size() == 0: - errorList.Error("changelog.yaml file is empty") - } -} diff --git a/pkg/linters/module/README.md b/pkg/linters/module/README.md index cc442d31..7dba2f87 100644 --- a/pkg/linters/module/README.md +++ b/pkg/linters/module/README.md @@ -12,6 +12,8 @@ The Module linter includes the following validation rules: | Rule | Description | Configurable | |------|-------------|--------------| +| [**has-changelog**](#has-changelog) | Validates that a non-empty `changelog.yaml` is carried | ✅ Yes | +| [**changelog-valid**](#changelog-valid) | Validates that `changelog.yaml` parses | ✅ Yes | | [**definition-file**](#definition-file) | Validates `module.yaml` structure, accessibility, and update sections | ✅ Yes | | [**oss**](#oss) | Validates open-source software attribution in `oss.yaml` | ✅ Yes | | [**conversions**](#conversions) | Validates OpenAPI conversion files and documentation | ✅ Yes | @@ -29,6 +31,37 @@ The Module linter includes the following validation rules: ## Rule Details +### Has-changelog + +Validates that a published image carries a `changelog.yaml` and that it is not empty. + +**Purpose:** the changelog is what a release is described by. An image that ships without one publishes a release with nothing to show for it, and the gap is invisible until someone reads the release page. + +**Checks:** + +- ✅ `changelog.yaml` exists in the package root +- ✅ It is not zero bytes + +**Scope:** `release` and `bundle` — the published images are where a changelog has to be, and the source tree is not checked for one. Whether it parses is checked by [changelog-valid](#changelog-valid), which runs in the same two scopes. + +The layout rules already report the file missing in both scopes, so this rule's own contribution is the empty-file case. + +A module built into the Deckhouse monorepo needs no exemption here: it publishes no images of its own — it rides the platform release, and its changes are described by the repo-level `CHANGELOG/` — so neither scope ever runs over it. + +--- + +### Changelog-valid + +Validates the `changelog.yaml` a published image ships. + +**Purpose:** Deckhouse reads this file to render the release notes for a version. A file it cannot parse means the release lands with no notes at all, and nothing upstream of the registry catches it. + +**Scope:** `release` and `bundle` — both published images carry `changelog.yaml`. + +A missing `changelog.yaml` is not this rule's finding: the layout rules own presence and [has-changelog](#has-changelog) owns emptiness, and this rule stays quiet when the file is absent — the same split [definition-file](#definition-file) and [package-yaml](#package-yaml) follow. + +--- + ### Definition file Validates the `module.yaml` configuration file structure and content. diff --git a/pkg/linters/module/module.go b/pkg/linters/module/module.go index 0fd1be18..b303325b 100644 --- a/pkg/linters/module/module.go +++ b/pkg/linters/module/module.go @@ -82,6 +82,8 @@ func (l *Module) rules() []pkg.Rule { rules.NewReleaseLayoutRule(m, level(cfg.Rules.ReleaseLayoutRule)), rules.NewBundleLayoutRule(m, level(cfg.Rules.BundleLayoutRule)), rules.NewHelmignoreCoverageRule(m, level(cfg.Rules.HelmignoreCoverageRule)), + rules.NewHasChangelogRule(m, level(cfg.Rules.HasChangelogRule)), + rules.NewChangelogValidRule(m, level(cfg.Rules.ChangelogValidRule)), } } diff --git a/pkg/linters/module/rules/changelog_valid.go b/pkg/linters/module/rules/changelog_valid.go new file mode 100644 index 00000000..be90633e --- /dev/null +++ b/pkg/linters/module/rules/changelog_valid.go @@ -0,0 +1,76 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "context" + "os" + "path/filepath" + + "sigs.k8s.io/yaml" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + ChangelogValidRuleName = "changelog-valid" + ChangelogFilename = "changelog.yaml" +) + +// ChangelogValidRule reports a changelog.yaml the release image ships that cannot be parsed. +type ChangelogValidRule struct { + pkg.RuleMeta + + module pkg.Module + errorList *errors.LintRuleErrorsList +} + +var _ pkg.Rule = (*ChangelogValidRule)(nil) + +func NewChangelogValidRule(m pkg.Module, errorList *errors.LintRuleErrorsList) *ChangelogValidRule { + return &ChangelogValidRule{ + RuleMeta: pkg.RuleMeta{Name: ChangelogValidRuleName}, + module: m, + errorList: errorList.WithRule(ChangelogValidRuleName), + } +} + +func (r *ChangelogValidRule) Check(_ context.Context) { + root := r.module.GetPath() + if root == "" { + return + } + + errorList := r.errorList.WithFilePath(ChangelogFilename) + + raw, err := os.ReadFile(filepath.Join(root, ChangelogFilename)) + if err != nil { + if os.IsNotExist(err) { + return + } + + errorList.Errorf("Cannot read %s: %s", ChangelogFilename, err) + + return + } + + var value any + if err := yaml.Unmarshal(raw, &value); err != nil { + errorList.Warnf("invalid YAML in %s:\n%s", ChangelogFilename, err) + } +} diff --git a/pkg/linters/module/rules/changelog_valid_test.go b/pkg/linters/module/rules/changelog_valid_test.go new file mode 100644 index 00000000..28ff7118 --- /dev/null +++ b/pkg/linters/module/rules/changelog_valid_test.go @@ -0,0 +1,98 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestChangelogYAMLRule(t *testing.T) { + for _, tc := range []struct { + name string + content string + write bool + wantErr bool + }{ + { + name: "a changelog the builder wrote", + write: true, + content: "log-shipper:\n features:\n - summary: a feature\n pull_request: https://example.invalid/1\n", + }, + { + // Syntax only, so a file that parses but holds nothing recognisable is + // deliberately clean here. + name: "valid YAML of an unexpected shape", + write: true, + content: "just a string\n", + }, + { + name: "empty file", + write: true, + content: "", + }, + { + // release-layout owns presence; this rule must stay quiet, which is what + // lets a scope whose image carries no changelog ask for it. + name: "no changelog at all", + write: false, + }, + { + name: "broken YAML", + write: true, + content: "log-shipper:\n features:\n - summary: a feature\n \tpull_request: 1\n", + wantErr: true, + }, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + if tc.write { + require.NoError(t, os.WriteFile( + filepath.Join(root, ChangelogFilename), []byte(tc.content), 0o600)) + } + + errorList := errors.NewLintRuleErrorsList() + NewChangelogValidRule(moduleAt(t, root), errorList).Check(context.Background()) + + errs := errorList.GetErrors() + if !tc.wantErr { + assert.Empty(t, errs) + + return + } + + require.Len(t, errs, 1) + assert.Contains(t, errs[0].Text, "invalid YAML in changelog.yaml") + }) + } +} + +// A module with no path is what a scope hands a rule when nothing was unpacked; the +// rule must not read the working directory instead. +func TestChangelogYAMLRuleWithoutAPath(t *testing.T) { + errorList := errors.NewLintRuleErrorsList() + NewChangelogValidRule(moduleAt(t, ""), errorList).Check(context.Background()) + + assert.Empty(t, errorList.GetErrors()) +} diff --git a/pkg/linters/module/rules/has_changelog.go b/pkg/linters/module/rules/has_changelog.go new file mode 100644 index 00000000..0f9f128f --- /dev/null +++ b/pkg/linters/module/rules/has_changelog.go @@ -0,0 +1,52 @@ +// Copyright 2025 Flant JSC +// Licensed under the Apache License, Version 2.0 + +package rules + +import ( + "context" + "os" + "path/filepath" + + "github.com/deckhouse/dmt/pkg" + "github.com/deckhouse/dmt/pkg/errors" +) + +const ( + HasChangelogRuleName = "has-changelog" +) + +func NewHasChangelogRule(m pkg.Module, errorList *errors.LintRuleErrorsList) *HasChangelogRule { + return &HasChangelogRule{ + RuleMeta: pkg.RuleMeta{ + Name: HasChangelogRuleName, + }, + module: m, + errorList: errorList.WithRule(HasChangelogRuleName), + } +} + +// HasChangelogRule reports a published image that carries no changelog, or carries an empty one. +type HasChangelogRule struct { + pkg.RuleMeta + + module pkg.Module + errorList *errors.LintRuleErrorsList +} + +var _ pkg.Rule = (*HasChangelogRule)(nil) + +func (r *HasChangelogRule) Check(_ context.Context) { + errorList := r.errorList.WithFilePath(ChangelogFilename) + + info, err := os.Stat(filepath.Join(r.module.GetPath(), ChangelogFilename)) + + switch { + case os.IsNotExist(err): + errorList.Warn("changelog.yaml file is missing") + case err != nil: + errorList.WithValue(err.Error()).Warn("failed to check changelog.yaml file") + case info.Size() == 0: + errorList.Warn("changelog.yaml file is empty") + } +} diff --git a/pkg/linters/module/rules/has_changelog_test.go b/pkg/linters/module/rules/has_changelog_test.go new file mode 100644 index 00000000..fe880198 --- /dev/null +++ b/pkg/linters/module/rules/has_changelog_test.go @@ -0,0 +1,74 @@ +/* +Copyright 2026 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package rules + +import ( + "context" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/deckhouse/dmt/pkg/errors" +) + +func TestHasChangelogRule(t *testing.T) { + for _, tc := range []struct { + name string + content string + write bool + want string + }{ + { + name: "a changelog the module wrote", + write: true, + content: "features:\n General:\n - a feature\n", + }, + { + name: "no changelog at all", + want: "changelog.yaml file is missing", + }, + { + name: "an empty changelog", + write: true, + want: "changelog.yaml file is empty", + }, + } { + t.Run(tc.name, func(t *testing.T) { + root := t.TempDir() + if tc.write { + require.NoError(t, os.WriteFile( + filepath.Join(root, ChangelogFilename), []byte(tc.content), DefaultFilePerm)) + } + + errorList := errors.NewLintRuleErrorsList() + NewHasChangelogRule(moduleAt(t, root), errorList).Check(context.Background()) + + errs := errorList.GetErrors() + if tc.want == "" { + assert.Empty(t, errs) + + return + } + + require.Len(t, errs, 1) + assert.Equal(t, tc.want, errs[0].Text) + }) + } +} diff --git a/pkg/linters/module/rules/layout.go b/pkg/linters/module/rules/layout.go index a81487e1..dd3c38bf 100644 --- a/pkg/linters/module/rules/layout.go +++ b/pkg/linters/module/rules/layout.go @@ -62,15 +62,15 @@ func NewReleaseLayoutRule(m pkg.Module, errorList *errors.LintRuleErrorsList) *L // packaged module — chart, templates and docs included. // // The list is what a published bundle actually holds, which is not what its source -// tree holds: changelog.yaml and version.json ship in the sibling release image, and -// the ignore file the package carries is .helmignore, not .gitignore. +// tree holds: version.json ships in the sibling release image, and the ignore file +// the package carries is .helmignore, not .gitignore. // // It is the intersection of eight published CE bundles, not every path they carry — // crds/, hooks/, monitoring/ and .werf/ appear in some and not others, so requiring // any of them would fail the modules that legitimately have nothing to put there. func NewBundleLayoutRule(m pkg.Module, errorList *errors.LintRuleErrorsList) *LayoutRule { return newLayoutRule(BundleLayoutRuleName, m, errorList, - []string{".helmignore", "Chart.yaml", "images_digests.json", "module.yaml"}, + []string{".helmignore", "Chart.yaml", ChangelogFilename, "images_digests.json", "module.yaml"}, []string{"charts", "docs", "openapi", "templates"}, ) } diff --git a/pkg/linters/module/rules/layout_test.go b/pkg/linters/module/rules/layout_test.go index 46b5b53a..e641f85c 100644 --- a/pkg/linters/module/rules/layout_test.go +++ b/pkg/linters/module/rules/layout_test.go @@ -37,7 +37,7 @@ func TestLayoutRules(t *testing.T) { // sds-replicated-volume, csi-nfs, console, commander-agent, observability, // secrets-store-integration) all carry these, and differ only in the optional // crds/, hooks/, monitoring/ and .werf/. - bundleFiles := []string{".helmignore", "Chart.yaml", "images_digests.json", "module.yaml"} + bundleFiles := []string{".helmignore", "Chart.yaml", "changelog.yaml", "images_digests.json", "module.yaml"} bundleDirs := []string{"charts", "docs", "openapi", "templates"} t.Run("release layout is complete", func(t *testing.T) { diff --git a/pkg/linters/module/rules/module_yaml.go b/pkg/linters/module/rules/module_yaml.go index 7a595d15..6697c4fa 100644 --- a/pkg/linters/module/rules/module_yaml.go +++ b/pkg/linters/module/rules/module_yaml.go @@ -27,13 +27,9 @@ import ( "strings" "github.com/Masterminds/semver/v3" - "gopkg.in/ini.v1" "k8s.io/utils/ptr" "sigs.k8s.io/yaml" - "github.com/deckhouse/deckhouse/pkg/log" - - "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/pkg" "github.com/deckhouse/dmt/pkg/errors" ) @@ -149,71 +145,6 @@ type ModuleUpdateVersion struct { To string `json:"to"` } -func getModuleNameFromRepository(dir string) string { - configFile := getGitConfigFile(dir) - if configFile == "" { - return "" - } - - cfg, err := ini.Load(configFile) - if err != nil { - log.Error("Failed to load config file", log.Err(err)) - return "" - } - - sec, err := cfg.GetSection("remote \"origin\"") - if err != nil { - log.Error("Failed to get remote origin", log.Err(err)) - return "" - } - - repositoryURL := sec.Key("url").String() - - return convertURLToModuleName(repositoryURL) -} - -func getGitConfigFile(dir string) string { - for { - if fsutils.IsDir(filepath.Join(dir, ".git")) && - fsutils.IsFile(filepath.Join(dir, ".git", "config")) { - return filepath.Join(dir, ".git", "config") - } - - parent := filepath.Dir(dir) - if dir == parent || parent == "" { - break - } - - dir = parent - } - - return "" -} - -// convertURLToModuleName converts a repository URL to a module name. -// It handles both SSH and HTTPS formats. -// Examples: -// git@github.com:deckhouse/dmt.git -// https://github.com/deckhouse/dmt -// It returns the last part of the URL as the module name. -// For example, for the URL "git@github.com:deckhouse/dmt.git", it will return "dmt". -func convertURLToModuleName(repoURL string) string { - // Remove the protocol part if it exists - repoURL = strings.TrimPrefix(repoURL, "https://") - repoURL = strings.TrimPrefix(repoURL, "git@") - - // Remove the ".git" suffix if it exists - repoURL = strings.TrimSuffix(repoURL, ".git") - - // Split by '/' and return the last part - parts := strings.Split(repoURL, "/") - if len(parts) == 0 { - return "" - } - - return parts[len(parts)-1] -} - var _ pkg.Rule = (*DefinitionFileRule)(nil) func (r *DefinitionFileRule) Check(_ context.Context) { @@ -290,12 +221,8 @@ func (r *DefinitionFileRule) Check(_ context.Context) { // TODO: refactor this maxLevel := ptr.To(pkg.Error) - moduleNameFromRepo := getModuleNameFromRepository(modulePath) - for _, repo := range pkg.IgnoreDeckhouseReposList { - if moduleNameFromRepo == repo { - maxLevel = ptr.To(pkg.Warn) - break - } + if pkg.IsDeckhouseRepo(modulePath) { + maxLevel = ptr.To(pkg.Warn) } // ru description is not required diff --git a/pkg/linters/module/rules/requirements.go b/pkg/linters/module/rules/requirements.go index aa795577..620a4c07 100644 --- a/pkg/linters/module/rules/requirements.go +++ b/pkg/linters/module/rules/requirements.go @@ -279,11 +279,8 @@ func (*RequirementsRegistry) validateComponentRequirement(checkName string, req switch req.ComponentType { case ComponentDeckhouse: // Skip deckhouse version check for repositories in the ignore list - moduleNameFromRepo := getModuleNameFromRepository(modulePath) - for _, repo := range pkg.IgnoreDeckhouseReposList { - if moduleNameFromRepo == repo { - return - } + if pkg.IsDeckhouseRepo(modulePath) { + return } if module.Requirements == nil || module.Requirements.Deckhouse == "" { diff --git a/pkg/linters/templates/rules/registry.go b/pkg/linters/templates/rules/registry.go index 1062a9c9..11aaa005 100644 --- a/pkg/linters/templates/rules/registry.go +++ b/pkg/linters/templates/rules/registry.go @@ -21,12 +21,6 @@ import ( "context" "fmt" "os" - "path/filepath" - "strings" - - "gopkg.in/ini.v1" - - "github.com/deckhouse/deckhouse/pkg/log" "github.com/deckhouse/dmt/internal/fsutils" "github.com/deckhouse/dmt/internal/modules" @@ -66,12 +60,9 @@ func (r *RegistryRule) Check(_ context.Context) { return } - moduleNameFromRepo := getModuleNameFromRepository(md.GetPath()) - for _, repo := range pkg.IgnoreDeckhouseReposList { - if moduleNameFromRepo == repo { - // Skip registry secret check - return - } + // Skip registry secret check for a module built into the Deckhouse monorepo. + if pkg.IsDeckhouseRepo(md.GetPath()) { + return } moduleName := md.GetName() @@ -96,68 +87,3 @@ func (r *RegistryRule) Check(_ context.Context) { } } } - -func getModuleNameFromRepository(dir string) string { - configFile := getGitConfigFile(dir) - if configFile == "" { - return "" - } - - cfg, err := ini.Load(configFile) - if err != nil { - log.Error("Failed to load config file", log.Err(err)) - return "" - } - - sec, err := cfg.GetSection("remote \"origin\"") - if err != nil { - log.Error("Failed to get remote origin", log.Err(err)) - return "" - } - - repositoryURL := sec.Key("url").String() - - return convertURLToModuleName(repositoryURL) -} - -func getGitConfigFile(dir string) string { - for { - if fsutils.IsDir(filepath.Join(dir, ".git")) && - fsutils.IsFile(filepath.Join(dir, ".git", "config")) { - return filepath.Join(dir, ".git", "config") - } - - parent := filepath.Dir(dir) - if dir == parent || parent == "" { - break - } - - dir = parent - } - - return "" -} - -// convertURLToModuleName converts a repository URL to a module name. -// It handles both SSH and HTTPS formats. -// Examples: -// git@github.com:deckhouse/dmt.git -// https://github.com/deckhouse/dmt -// It returns the last part of the URL as the module name. -// For example, for the URL "git@github.com:deckhouse/dmt.git", it will return "dmt". -func convertURLToModuleName(repoURL string) string { - // Remove the protocol part if it exists - repoURL = strings.TrimPrefix(repoURL, "https://") - repoURL = strings.TrimPrefix(repoURL, "git@") - - // Remove the ".git" suffix if it exists - repoURL = strings.TrimSuffix(repoURL, ".git") - - // Split by '/' and return the last part - parts := strings.Split(repoURL, "/") - if len(parts) == 0 { - return "" - } - - return parts[len(parts)-1] -} diff --git a/pkg/linters/templates/rules/registry_test.go b/pkg/linters/templates/rules/registry_test.go index b1862ffc..ad79a33c 100644 --- a/pkg/linters/templates/rules/registry_test.go +++ b/pkg/linters/templates/rules/registry_test.go @@ -295,260 +295,3 @@ data: }) } } - -func TestConvertURLToModuleName(t *testing.T) { - tests := []struct { - name string - repoURL string - expected string - }{ - { - name: "SSH URL with .git suffix", - repoURL: "git@github.com:deckhouse/test-module.git", - expected: "test-module", - }, - { - name: "HTTPS URL with .git suffix", - repoURL: "https://github.com/deckhouse/test-module.git", - expected: "test-module", - }, - { - name: "HTTPS URL without .git suffix", - repoURL: "https://github.com/deckhouse/test-module", - expected: "test-module", - }, - { - name: "SSH URL without .git suffix", - repoURL: "git@github.com:deckhouse/test-module", - expected: "test-module", - }, - { - name: "complex path", - repoURL: "https://github.com/deckhouse/ee/modules/test-module.git", - expected: "test-module", - }, - { - name: "empty URL", - repoURL: "", - expected: "", - }, - { - name: "URL with trailing slash", - repoURL: "https://github.com/deckhouse/test-module/", - expected: "", - }, - { - name: "single component URL", - repoURL: "test-module", - expected: "test-module", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result := convertURLToModuleName(tt.repoURL) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestGetGitConfigFile(t *testing.T) { - tests := []struct { - name string - dir string - expected string - setupFunc func(string) func() - }{ - { - name: "git config exists in current directory", - dir: "/tmp/test-module", - expected: "/tmp/test-module/.git/config", - setupFunc: func(dir string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte("test"), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "git config exists in parent directory", - dir: "/tmp/test-module/subdir", - expected: "/tmp/test-module/.git/config", - setupFunc: func(dir string) func() { - // Create parent directory with git config - parentDir := filepath.Dir(dir) - err := os.MkdirAll(filepath.Join(parentDir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(parentDir, ".git", "config"), []byte("test"), 0600) - require.NoError(t, err) - - // Create subdirectory - err = os.MkdirAll(dir, 0755) - require.NoError(t, err) - - return func() { - os.RemoveAll(parentDir) - } - }, - }, - { - name: "no git config found", - dir: "/tmp/test-module", - expected: "", - setupFunc: func(dir string) func() { - err := os.MkdirAll(dir, 0755) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "git directory exists but no config file", - dir: "/tmp/test-module", - expected: "", - setupFunc: func(dir string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cleanup := tt.setupFunc(tt.dir) - defer cleanup() - - result := getGitConfigFile(tt.dir) - assert.Equal(t, tt.expected, result) - }) - } -} - -func TestGetModuleNameFromRepository(t *testing.T) { - tests := []struct { - name string - dir string - gitConfig string - expected string - setupFunc func(string, string) func() - }{ - { - name: "valid git config with SSH URL", - dir: "/tmp/test-module", - gitConfig: `[remote "origin"] - url = git@github.com:deckhouse/test-module.git`, - expected: "test-module", - setupFunc: func(dir, config string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(config), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "valid git config with HTTPS URL", - dir: "/tmp/test-module", - gitConfig: `[remote "origin"] - url = https://github.com/deckhouse/test-module.git`, - expected: "test-module", - setupFunc: func(dir, config string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(config), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "valid git config with HTTPS URL without .git suffix", - dir: "/tmp/test-module", - gitConfig: `[remote "origin"] - url = https://github.com/deckhouse/test-module`, - expected: "test-module", - setupFunc: func(dir, config string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(config), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "no git directory", - dir: "/tmp/test-module", - expected: "", - setupFunc: func(dir, _ string) func() { - err := os.MkdirAll(dir, 0755) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "invalid git config", - dir: "/tmp/test-module", - gitConfig: `[remote "origin"] - url = invalid-url`, - expected: "invalid-url", - setupFunc: func(dir, config string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(config), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - { - name: "deckhouse module URL", - dir: "/tmp/deckhouse", - gitConfig: `[remote "origin"] - url = git@github.com:deckhouse/deckhouse.git`, - expected: "deckhouse", - setupFunc: func(dir, config string) func() { - err := os.MkdirAll(filepath.Join(dir, ".git"), 0755) - require.NoError(t, err) - err = os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(config), 0600) - require.NoError(t, err) - - return func() { - os.RemoveAll(dir) - } - }, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - cleanup := tt.setupFunc(tt.dir, tt.gitConfig) - defer cleanup() - - result := getModuleNameFromRepository(tt.dir) - assert.Equal(t, tt.expected, result) - }) - } -} diff --git a/pkg/repository.go b/pkg/repository.go new file mode 100644 index 00000000..38b683d9 --- /dev/null +++ b/pkg/repository.go @@ -0,0 +1,106 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pkg + +import ( + "path/filepath" + "slices" + "strings" + + "gopkg.in/ini.v1" + + "github.com/deckhouse/deckhouse/pkg/log" + + "github.com/deckhouse/dmt/internal/fsutils" +) + +// RepositoryOriginURL returns the `remote "origin"` URL of the git checkout dir sits in, +// or "" when there is no checkout, no origin, or the config cannot be read. Every caller +// that needs to know which repository a module was linted from starts here. +func RepositoryOriginURL(dir string) string { + configFile := getGitConfigFile(dir) + if configFile == "" { + return "" + } + + cfg, err := ini.Load(configFile) + if err != nil { + log.Error("Failed to load config file", log.Err(err)) + return "" + } + + sec, err := cfg.GetSection("remote \"origin\"") + if err != nil { + log.Error("Failed to get remote origin", log.Err(err)) + return "" + } + + return sec.Key("url").String() +} + +// IsDeckhouseRepo reports a module linted from inside the Deckhouse monorepo, by the name +// of the repository it sits in. Such a module is built and released by the platform rather +// than on its own, so the rules that describe what a standalone module publishes — and the +// requirements only a standalone module can state — do not apply to it. +func IsDeckhouseRepo(dir string) bool { + // TODO: THINK ABOUT HOW TO ENDURE + ignoreDeckhouseReposList := []string{"deckhouse", "deckhouse-test-1", "deckhouse-test-2"} + + return slices.Contains(ignoreDeckhouseReposList, convertURLToModuleName(RepositoryOriginURL(dir))) +} + +func getGitConfigFile(dir string) string { + for { + if fsutils.IsDir(filepath.Join(dir, ".git")) && + fsutils.IsFile(filepath.Join(dir, ".git", "config")) { + return filepath.Join(dir, ".git", "config") + } + + parent := filepath.Dir(dir) + if dir == parent || parent == "" { + break + } + + dir = parent + } + + return "" +} + +// convertURLToModuleName converts a repository URL to a module name. +// It handles both SSH and HTTPS formats. +// Examples: +// git@github.com:deckhouse/dmt.git +// https://github.com/deckhouse/dmt +// It returns the last part of the URL as the module name. +// For example, for the URL "git@github.com:deckhouse/dmt.git", it will return "dmt". +func convertURLToModuleName(repoURL string) string { + // Remove the protocol part if it exists + repoURL = strings.TrimPrefix(repoURL, "https://") + repoURL = strings.TrimPrefix(repoURL, "git@") + + // Remove the ".git" suffix if it exists + repoURL = strings.TrimSuffix(repoURL, ".git") + + // Split by '/' and return the last part + parts := strings.Split(repoURL, "/") + if len(parts) == 0 { + return "" + } + + return parts[len(parts)-1] +} diff --git a/pkg/repository_test.go b/pkg/repository_test.go new file mode 100644 index 00000000..d68acec2 --- /dev/null +++ b/pkg/repository_test.go @@ -0,0 +1,153 @@ +/* +Copyright 2025 Flant JSC + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package pkg + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestConvertURLToModuleName(t *testing.T) { + for _, tt := range []struct { + name string + repoURL string + expected string + }{ + {name: "SSH URL with .git suffix", repoURL: "git@github.com:deckhouse/test-module.git", expected: "test-module"}, + {name: "HTTPS URL with .git suffix", repoURL: "https://github.com/deckhouse/test-module.git", expected: "test-module"}, + {name: "HTTPS URL without .git suffix", repoURL: "https://github.com/deckhouse/test-module", expected: "test-module"}, + {name: "SSH URL without .git suffix", repoURL: "git@github.com:deckhouse/test-module", expected: "test-module"}, + {name: "complex path", repoURL: "https://github.com/deckhouse/ee/modules/test-module.git", expected: "test-module"}, + {name: "empty URL", repoURL: "", expected: ""}, + {name: "URL with trailing slash", repoURL: "https://github.com/deckhouse/test-module/", expected: ""}, + {name: "single component URL", repoURL: "test-module", expected: "test-module"}, + } { + t.Run(tt.name, func(t *testing.T) { + assert.Equal(t, tt.expected, convertURLToModuleName(tt.repoURL)) + }) + } +} + +func TestGetGitConfigFile(t *testing.T) { + t.Run("git config exists in current directory", func(t *testing.T) { + dir := t.TempDir() + writeGitConfig(t, dir, "test") + + assert.Equal(t, filepath.Join(dir, ".git", "config"), getGitConfigFile(dir)) + }) + + t.Run("git config exists in parent directory", func(t *testing.T) { + parent := t.TempDir() + writeGitConfig(t, parent, "test") + + sub := filepath.Join(parent, "subdir") + require.NoError(t, os.MkdirAll(sub, 0o755)) + + assert.Equal(t, filepath.Join(parent, ".git", "config"), getGitConfigFile(sub)) + }) + + t.Run("no git config found", func(t *testing.T) { + assert.Empty(t, getGitConfigFile(t.TempDir())) + }) + + t.Run("git directory exists but no config file", func(t *testing.T) { + dir := t.TempDir() + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755)) + + assert.Empty(t, getGitConfigFile(dir)) + }) +} + +func TestRepositoryOriginURL(t *testing.T) { + for _, tt := range []struct { + name string + gitConfig string + noGitDir bool + expected string + }{ + { + name: "SSH URL", + gitConfig: "[remote \"origin\"]\n\turl = git@github.com:deckhouse/test-module.git", + expected: "git@github.com:deckhouse/test-module.git", + }, + { + name: "HTTPS URL", + gitConfig: "[remote \"origin\"]\n\turl = https://github.com/deckhouse/test-module.git", + expected: "https://github.com/deckhouse/test-module.git", + }, + { + // Not a URL at all — reading the config is this function's job, judging + // what it holds is the caller's. + name: "origin that is not a URL", + gitConfig: "[remote \"origin\"]\n\turl = invalid-url", + expected: "invalid-url", + }, + { + name: "no origin section", + gitConfig: "[core]\n\tbare = false", + expected: "", + }, + { + name: "no git directory", + noGitDir: true, + expected: "", + }, + } { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + if !tt.noGitDir { + writeGitConfig(t, dir, tt.gitConfig) + } + + assert.Equal(t, tt.expected, RepositoryOriginURL(dir)) + }) + } +} + +func TestIsDeckhouseRepo(t *testing.T) { + for _, tt := range []struct { + name string + originURL string + expected bool + }{ + {name: "the monorepo itself", originURL: "git@github.com:deckhouse/deckhouse.git", expected: true}, + {name: "a fork of the monorepo", originURL: "https://github.com/someone/deckhouse.git", expected: true}, + {name: "a test monorepo from the ignore list", originURL: "git@github.com:deckhouse/deckhouse-test-1.git", expected: true}, + {name: "a standalone module in the same org", originURL: "git@github.com:deckhouse/log-shipper.git", expected: false}, + {name: "no origin at all", originURL: "", expected: false}, + } { + t.Run(tt.name, func(t *testing.T) { + dir := t.TempDir() + if tt.originURL != "" { + writeGitConfig(t, dir, "[remote \"origin\"]\n\turl = "+tt.originURL) + } + + assert.Equal(t, tt.expected, IsDeckhouseRepo(dir)) + }) + } +} + +func writeGitConfig(t *testing.T, dir, content string) { + t.Helper() + + require.NoError(t, os.MkdirAll(filepath.Join(dir, ".git"), 0o755)) + require.NoError(t, os.WriteFile(filepath.Join(dir, ".git", "config"), []byte(content), 0o600)) +} diff --git a/pkg/scopes/bundle.go b/pkg/scopes/bundle.go index c60f782f..af214986 100644 --- a/pkg/scopes/bundle.go +++ b/pkg/scopes/bundle.go @@ -29,10 +29,6 @@ import ( // bundleRules is the rule membership of the bundle scope: the bundle image // (:) holds the packaged module — chart, templates, docs and digests — -// so bundle-layout asks for the whole of that shape. The changelog rule is not part -// of it: changelog.yaml ships in the release image, and release-layout is what makes -// its absence a finding there. -// // What it deliberately does not ask for is anything under the templates or container // linters. A bundle carries rendered-looking directories but the module behind this // scope comes from modules.NewRemoteModule, whose object store is nil; those linters @@ -41,6 +37,8 @@ var bundleRules = map[string]set.Set{ moduleLinter.ID: set.New( modulerules.BundleLayoutRuleName, modulerules.HelmignoreCoverageRuleName, + modulerules.HasChangelogRuleName, + modulerules.ChangelogValidRuleName, ), docs.ID: set.New( docsrules.ReadmeRuleName, diff --git a/pkg/scopes/release.go b/pkg/scopes/release.go index c83c3a63..ef72c049 100644 --- a/pkg/scopes/release.go +++ b/pkg/scopes/release.go @@ -31,16 +31,18 @@ import ( // and for nothing that needs a chart or a rendered object — the module behind this // scope is built by modules.NewRemoteModule and has neither. // -// release-layout is what makes a missing file a finding. definition-file and -// package-yaml validate the contents of module.yaml and package.yaml and stay quiet -// when the file is absent, which is why the presence check is a rule of its own. -// package.yaml is not in the layout list on purpose: it is validated when the image -// ships one, and its absence is not an error. +// release-layout is what makes a missing file a finding. definition-file, +// package-yaml and changelog-valid validate the contents of module.yaml, package.yaml +// and changelog.yaml and stay quiet when the file is absent, which is why the presence +// check is a rule of its own. package.yaml is not in the layout list on purpose: it is +// validated when the image ships one, and its absence is not an error. var releaseRules = map[string]set.Set{ moduleLinter.ID: set.New( modulerules.ReleaseLayoutRuleName, modulerules.DefinitionFileRuleName, modulerules.PackageYAMLRuleName, + modulerules.HasChangelogRuleName, + modulerules.ChangelogValidRuleName, ), } diff --git a/pkg/scopes/scopes_test.go b/pkg/scopes/scopes_test.go index a64eac9f..be892fc2 100644 --- a/pkg/scopes/scopes_test.go +++ b/pkg/scopes/scopes_test.go @@ -91,7 +91,7 @@ func TestRemoteScopesRunOverAnUnpackedImage(t *testing.T) { scope: Bundle, // The root of a real published bundle — see layout_test.go for where it // comes from. Deriving it from bundleRules would test nothing. - files: []string{".helmignore", "Chart.yaml", "images_digests.json", "module.yaml"}, + files: []string{".helmignore", "Chart.yaml", "changelog.yaml", "images_digests.json", "module.yaml"}, dirs: []string{"charts", "docs", "openapi", "templates"}, }, {scope: Bundle, wantErr: true}, diff --git a/test/e2e/testdata/module/enabled-script-deprecated/module/.helmignore b/test/e2e/testdata/module/enabled-script-deprecated/module/.helmignore index 9fa482f0..f6562537 100644 --- a/test/e2e/testdata/module/enabled-script-deprecated/module/.helmignore +++ b/test/e2e/testdata/module/enabled-script-deprecated/module/.helmignore @@ -4,3 +4,4 @@ crds/ docs/ module.yaml enabled +changelog.yaml diff --git a/test/e2e/testdata/module/enabled-script-deprecated/module/changelog.yaml b/test/e2e/testdata/module/enabled-script-deprecated/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/enabled-script-deprecated/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/enabled-script-empty/module/.helmignore b/test/e2e/testdata/module/enabled-script-empty/module/.helmignore index 9fa482f0..f6562537 100644 --- a/test/e2e/testdata/module/enabled-script-empty/module/.helmignore +++ b/test/e2e/testdata/module/enabled-script-empty/module/.helmignore @@ -4,3 +4,4 @@ crds/ docs/ module.yaml enabled +changelog.yaml diff --git a/test/e2e/testdata/module/enabled-script-empty/module/changelog.yaml b/test/e2e/testdata/module/enabled-script-empty/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/enabled-script-empty/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/helmignore-coverage-uncovered/module/.helmignore b/test/e2e/testdata/module/helmignore-coverage-uncovered/module/.helmignore index 7ccdc1e9..6cc615cd 100644 --- a/test/e2e/testdata/module/helmignore-coverage-uncovered/module/.helmignore +++ b/test/e2e/testdata/module/helmignore-coverage-uncovered/module/.helmignore @@ -2,3 +2,4 @@ hooks/ openapi/ docs/ module.yaml +changelog.yaml diff --git a/test/e2e/testdata/module/helmignore-coverage-uncovered/module/changelog.yaml b/test/e2e/testdata/module/helmignore-coverage-uncovered/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/helmignore-coverage-uncovered/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/.helmignore b/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/.helmignore new file mode 100644 index 00000000..72587bc1 --- /dev/null +++ b/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/.helmignore @@ -0,0 +1,5 @@ +hooks +openapi +docs +module.yaml +changelog.yaml diff --git a/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/changelog.yaml b/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/helmignore-dir-covered-without-slash/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/oss-version-not-semver-excluded/module/.helmignore b/test/e2e/testdata/module/oss-version-not-semver-excluded/module/.helmignore index afb7ba62..637e79b0 100644 --- a/test/e2e/testdata/module/oss-version-not-semver-excluded/module/.helmignore +++ b/test/e2e/testdata/module/oss-version-not-semver-excluded/module/.helmignore @@ -6,3 +6,4 @@ openapi/ module.yaml oss.yaml .dmtlint.yaml +changelog.yaml diff --git a/test/e2e/testdata/module/oss-version-not-semver-excluded/module/changelog.yaml b/test/e2e/testdata/module/oss-version-not-semver-excluded/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/oss-version-not-semver-excluded/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/oss-version-not-semver/module/.helmignore b/test/e2e/testdata/module/oss-version-not-semver/module/.helmignore index 48e9533f..f845c196 100644 --- a/test/e2e/testdata/module/oss-version-not-semver/module/.helmignore +++ b/test/e2e/testdata/module/oss-version-not-semver/module/.helmignore @@ -5,3 +5,4 @@ images/ openapi/ module.yaml oss.yaml +changelog.yaml diff --git a/test/e2e/testdata/module/oss-version-not-semver/module/changelog.yaml b/test/e2e/testdata/module/oss-version-not-semver/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/oss-version-not-semver/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1 diff --git a/test/e2e/testdata/module/package-consistency-fix/module/changelog.yaml b/test/e2e/testdata/module/package-consistency-fix/module/changelog.yaml new file mode 100644 index 00000000..5f04efa2 --- /dev/null +++ b/test/e2e/testdata/module/package-consistency-fix/module/changelog.yaml @@ -0,0 +1,4 @@ +test-module: + fixes: + - summary: a fix + pull_request: https://example.invalid/1