diff --git a/.tektonlintrc.yaml b/.tektonlintrc.yaml index 087a332..d2c67f3 100644 --- a/.tektonlintrc.yaml +++ b/.tektonlintrc.yaml @@ -17,6 +17,7 @@ rules: # error | warning | off no-latest-image: warning prefer-beta: warning prefer-kebab-case: warning + prefer-camel-kebab-case: off no-unused-param: warning no-missing-resource: error no-undefined-param: error diff --git a/README.md b/README.md index 2167cff..8e5b1d0 100644 --- a/README.md +++ b/README.md @@ -188,13 +188,24 @@ If you see an error like ` Pipeline 'pipeline-test-perf-tag' references task 'un - Unused `Pipeline` parameters - Unused `TriggerTemplate` parameters - Unpinned images in `Task` steps -- _kebab-case_ naming violations +- _kebab-case_ and OR _camelCase_ naming violations - `Task` & `Pipeline` definitions with `tekton.dev/v1alpha1` `apiVersion` - Missing `TriggerBinding` parameter values - Usage of deprecated `Condition` instead of `WhenExpression` - Usage of deprecated resources (resources marked with `tekton.dev/deprecated` label) - Missing `hashbang` line from a `Step`s `script` +The default rule is for preferring _kebab-case_; _camelCase_ is equally popular and many of the official examples on the Tekton website are in camel case. To use the rule that accepts camel case as well swap the rules in the `.tektonrc.yaml` file + +```yaml +--- +rules: # error | warning | off + prefer-kebab-case: off + prefer-camel-kebab-case: warning +``` + + + ## Configuring `tekton-lint` You can configure `tekton-lint` with a configuration file ([`.tektonlintrc.yaml`](./.tektonlintrc.yaml)). You can decide which rules are enabled and at what error level. In addition you can specify external tekton tasks defined in a git repository; for example [OpenToolChain](https://github.com/open-toolchain/tekton-catalog) provides a set of tasks that are helpful. But if you lint just your own tekton files there will be errors about not being able to find `git-clone-repo` for example. Not will any checks be made to see if your usage is correct. diff --git a/regression-tests/customconfig/.tektonlintrc.yaml b/regression-tests/customconfig/.tektonlintrc.yaml new file mode 100644 index 0000000..ce729da --- /dev/null +++ b/regression-tests/customconfig/.tektonlintrc.yaml @@ -0,0 +1,8 @@ +--- +rules: # error | warning | off + prefer-kebab-case: off + prefer-camel-kebab-case: warning + +# custom: +# my_rules: custom_rules + diff --git a/regression-tests/customconfig/ace-pipeline.yaml b/regression-tests/customconfig/ace-pipeline.yaml new file mode 100644 index 0000000..876b607 --- /dev/null +++ b/regression-tests/customconfig/ace-pipeline.yaml @@ -0,0 +1,72 @@ +apiVersion: tekton.dev/v1beta1 +kind: Pipeline +metadata: + name: ace-pipeline +spec: + params: + - name: outputRegistry + type: string + - name: url + type: string + default: "https://github.com/ot4i/ace-demo-pipeline" + - name: revision + type: string + default: "main" + - name: buildImage + type: string + - name: runtimeBaseImage + type: string + - name: knativeDeploy + type: string + default: "false" + tasks: + - name: build-from-source + taskRef: + name: aceBuild + params: + - name: outputRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: buildImage + value: $(params.buildImage) + - name: runtimeBaseImage + value: $(params.runtimeBaseImage) + - name: deploy-to-cluster + taskRef: + name: deploy-to-cluster + params: + - name: dockerRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: tag + value: "$(tasks.build-from-source.results.tag)" + runAfter: + - build-from-source + when: + - input: "$(params.knativeDeploy)" + operator: in + values: ["false"] + - name: deploy-knative-to-cluster + taskRef: + name: knative-deploy + params: + - name: dockerRegistry + value: $(params.outputRegistry) + - name: url + value: $(params.url) + - name: revision + value: $(params.revision) + - name: tag + value: "$(tasks.build-from-source.results.tag)" + runAfter: + - build-from-source + when: + - input: "$(params.knativeDeploy)" + operator: in + values: ["true"] \ No newline at end of file diff --git a/regression-tests/customconfig/ace-pipeline.yaml.expect.json b/regression-tests/customconfig/ace-pipeline.yaml.expect.json new file mode 100644 index 0000000..e69160d --- /dev/null +++ b/regression-tests/customconfig/ace-pipeline.yaml.expect.json @@ -0,0 +1,53 @@ +[ + { + "message": "Pipeline 'ace-pipeline' references task 'aceBuild' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 521, + 529, + 530 + ], + "startLine": 25, + "startColumn": 15, + "endLine": 25, + "endColumn": 23 + } + }, + { + "message": "Pipeline 'ace-pipeline' references task 'deploy-to-cluster' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 930, + 947, + 948 + ], + "startLine": 39, + "startColumn": 15, + "endLine": 39, + "endColumn": 32 + } + }, + { + "message": "Pipeline 'ace-pipeline' references task 'knative-deploy' but the referenced task cannot be found. To fix this, include all the task definitions to the lint task for this pipeline.", + "rule": "no-missing-resource", + "level": "error", + "path": "./regression-tests/customconfig/ace-pipeline.yaml", + "loc": { + "range": [ + 1442, + 1456, + 1457 + ], + "startLine": 57, + "startColumn": 15, + "endLine": 57, + "endColumn": 29 + } + } +] \ No newline at end of file diff --git a/regression-tests/regression.test.ts b/regression-tests/regression.test.ts index 925f828..31be7fe 100644 --- a/regression-tests/regression.test.ts +++ b/regression-tests/regression.test.ts @@ -1,12 +1,13 @@ import fs from 'node:fs' import fg from 'fast-glob'; +import path from 'node:path'; import {Problem, Config, Linter} from '../src/index' const pattern = "./regression-tests/general/*.yaml" const yamlfiles = fg.globSync(pattern) +const customconfig = "./regression-tests/customconfig/*.yaml" - -describe("Regression Tests",()=>{ +describe("Default Config Regression Tests",()=>{ test.each(yamlfiles)("%s",async (yamlSrcPath)=>{ const cfg: Config = Config.getDefaultConfig() @@ -23,4 +24,25 @@ describe("Regression Tests",()=>{ expect(problems).toMatchObject(expected) }) +}) + +describe("Custom Config Regression Tests",()=>{ + + test.each(fg.globSync(customconfig))("%s", async (yamlSrcPath)=>{ + const cfgPath = path.resolve(path.dirname(yamlSrcPath)) + const cfg: Config = Config.getConfig(cfgPath); + cfg.globs=[yamlSrcPath] + const problems: Problem[] = await Linter.run(cfg) + + const expectedPath =`${yamlSrcPath}.expect.json` + + if (!fs.existsSync(expectedPath)){ + fs.writeFileSync(expectedPath, JSON.stringify(problems)) + } + + const expected = JSON.parse(fs.readFileSync(expectedPath,'utf-8')) + expect(problems).toMatchObject(expected) + + }) + }) \ No newline at end of file diff --git a/src/config.ts b/src/config.ts index c2d9bdf..013dec5 100644 --- a/src/config.ts +++ b/src/config.ts @@ -103,18 +103,25 @@ export class Config { public static getDefaultConfig(): Config { // create default cli-proxy + return Config.getConfig(process.cwd()); + } + + public static getConfig(dir: string): Config { + // create default cli-proxy const argv = { watch: false, color: true, format: json, quite: false, - config: process.cwd(), + config: dir, 'refresh-cache': false, + _: [], }; const config = new Config(argv); return config; } + } diff --git a/src/default-rule-config.ts b/src/default-rule-config.ts index d51ec5c..9186f8e 100644 --- a/src/default-rule-config.ts +++ b/src/default-rule-config.ts @@ -19,6 +19,7 @@ const defaultRules: RulesConfig = { 'no-latest-image': 'warning', 'prefer-beta': 'warning', 'prefer-kebab-case': 'warning', + 'prefer-camel-kebab-case': 'off', 'no-unused-param': 'warning', 'no-missing-resource': 'error', 'no-undefined-param': 'error', diff --git a/src/rule-loader.ts b/src/rule-loader.ts index add736c..4688e32 100644 --- a/src/rule-loader.ts +++ b/src/rule-loader.ts @@ -55,6 +55,9 @@ const defaultRules = { // prefer-kebab-case 'prefer-kebab-case': (await import('./rules/prefer-kebab-case.js')).default, + // prefer-camel-kebab-case + 'prefer-camel-kebab-case': (await import('./rules/prefer-camel-kebab-case.js')).default, + // prefer-when-expression 'prefer-when-expression': (await import('./rules/prefer-when-expression.js')).default, diff --git a/src/rules/prefer-camel-kebab-case.ts b/src/rules/prefer-camel-kebab-case.ts new file mode 100644 index 0000000..474748e --- /dev/null +++ b/src/rules/prefer-camel-kebab-case.ts @@ -0,0 +1,65 @@ +import { walk, pathToString } from '../walk.js'; + +const isValidKebabName = (name) => { + const valid = new RegExp('^[a-z0-9-()$.]*$'); + return valid.test(name); +}; + +const isValidCamelName = (name) => { + const valid = new RegExp('^[a-z_][a-z0-9A-Z()$.]*$'); + return valid.test(name); +}; + +const isValidName = (name) => { + return isValidKebabName(name) || isValidCamelName(name); +}; + +const naming = (resource, prefix, report) => (node, path, parent) => { + let name = node; + const isNameDefinition = /.name$/.test(path); + + if (path.includes('env') && path.includes('name')) return; + + if (isNameDefinition && !isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + 'name', + ); + return; + } + + const parameterPlacementRx = new RegExp(`\\$\\(${prefix}.(.*?)\\)`); + const m = node && node.toString().match(parameterPlacementRx); + + if (m) { + name = m[1]; + if (!isValidName(name)) { + report( + `Invalid name for '${name}' at ${pathToString( + path, + )} in '${resource}'. Names should be in lowercase, alphanumeric, kebab-case or camelCase format.`, + parent, + path[path.length - 1], + ); + } + } +}; + +export default (docs, tekton, report) => { + for (const pipeline of Object.values(tekton.pipelines)) { + walk(pipeline.spec.tasks, ['spec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk(pipeline.spec.finally, ['spec', 'finally'], naming(pipeline.metadata.name, 'params', report)); + } + + for (const pipeline of Object.values(tekton.pipelineRuns)) { + walk(pipeline.spec.tasks, ['spec', 'pipelineSpec', 'tasks'], naming(pipeline.metadata.name, 'params', report)); + walk( + pipeline.spec.finally, + ['spec', 'pipelineSpec', 'finally'], + naming(pipeline.metadata.name, 'params', report), + ); + } +};