mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-11-22 01:41:02 +00:00
Improve linter messages in UI (#4351)
This commit is contained in:
parent
04e8309e60
commit
a1193f0fb4
9 changed files with 122 additions and 77 deletions
|
@ -78,7 +78,7 @@ func (l *Linter) lintFile(config *WorkflowConfig) error {
|
|||
var linterErr error
|
||||
|
||||
if len(config.Workflow.Steps.ContainerList) == 0 {
|
||||
linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing steps section", config.File, "steps", false))
|
||||
linterErr = multierr.Append(linterErr, newLinterError("Invalid or missing `steps` section", config.File, "steps", false))
|
||||
}
|
||||
|
||||
if err := l.lintCloneSteps(config); err != nil {
|
||||
|
@ -123,7 +123,7 @@ func (l *Linter) lintCloneSteps(config *WorkflowConfig) error {
|
|||
if !utils.MatchImageDynamic(container.Image, trustedClonePlugins...) {
|
||||
linterErr = multierr.Append(linterErr,
|
||||
newLinterError(
|
||||
"Specified clone image does not match allow list, netrc will not be injected",
|
||||
"Specified clone image does not match allow list, netrc is not injected",
|
||||
config.File, fmt.Sprintf("clone.%s", container.Name), true),
|
||||
)
|
||||
}
|
||||
|
@ -173,7 +173,7 @@ func (l *Linter) lintImage(config *WorkflowConfig, c *types.Container, area stri
|
|||
func (l *Linter) lintPrivilegedPlugins(config *WorkflowConfig, c *types.Container, area string) error {
|
||||
// lint for conflicts of https://github.com/woodpecker-ci/woodpecker/pull/3918
|
||||
if utils.MatchImage(c.Image, "plugins/docker", "plugins/gcr", "plugins/ecr", "woodpeckerci/plugin-docker-buildx") {
|
||||
msg := fmt.Sprintf("The formerly privileged plugin '%s' is no longer privileged by default, if required, add it to WOODPECKER_PLUGINS_PRIVILEGED", c.Image)
|
||||
msg := fmt.Sprintf("The formerly privileged plugin `%s` is no longer privileged by default, if required, add it to `WOODPECKER_PLUGINS_PRIVILEGED`", c.Image)
|
||||
// check first if user did not add them back
|
||||
if l.privilegedPlugins != nil && !utils.MatchImageDynamic(c.Image, *l.privilegedPlugins...) {
|
||||
return newLinterError(msg, config.File, fmt.Sprintf("%s.%s", area, c.Name), false)
|
||||
|
@ -191,16 +191,16 @@ func (l *Linter) lintSettings(config *WorkflowConfig, c *types.Container, field
|
|||
return nil
|
||||
}
|
||||
if len(c.Commands) != 0 {
|
||||
return newLinterError("Cannot configure both commands and settings", config.File, fmt.Sprintf("%s.%s", field, c.Name), false)
|
||||
return newLinterError("Cannot configure both `commands` and `settings`", config.File, fmt.Sprintf("%s.%s", field, c.Name), false)
|
||||
}
|
||||
if len(c.Entrypoint) != 0 {
|
||||
return newLinterError("Cannot configure both entrypoint and settings", config.File, fmt.Sprintf("%s.%s", field, c.Name), false)
|
||||
return newLinterError("Cannot configure both `entrypoint` and `settings`", config.File, fmt.Sprintf("%s.%s", field, c.Name), false)
|
||||
}
|
||||
if len(c.Environment) != 0 {
|
||||
return newLinterError("Should not configure both environment and settings", config.File, fmt.Sprintf("%s.%s", field, c.Name), true)
|
||||
return newLinterError("Should not configure both `environment` and `settings`", config.File, fmt.Sprintf("%s.%s", field, c.Name), true)
|
||||
}
|
||||
if len(c.Secrets) != 0 {
|
||||
return newLinterError("Should not configure both secrets and settings", config.File, fmt.Sprintf("%s.%s", field, c.Name), true)
|
||||
return newLinterError("Should not configure both `secrets` and `settings`", config.File, fmt.Sprintf("%s.%s", field, c.Name), true)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -210,32 +210,32 @@ func (l *Linter) lintTrusted(config *WorkflowConfig, c *types.Container, area st
|
|||
errors := []string{}
|
||||
if !l.trusted.Security {
|
||||
if c.Privileged {
|
||||
errors = append(errors, "Insufficient privileges to use privileged mode")
|
||||
errors = append(errors, "Insufficient trust level to use `privileged` mode")
|
||||
}
|
||||
}
|
||||
if !l.trusted.Network {
|
||||
if len(c.DNS) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use custom dns")
|
||||
errors = append(errors, "Insufficient trust level to use custom `dns`")
|
||||
}
|
||||
if len(c.DNSSearch) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use dns_search")
|
||||
errors = append(errors, "Insufficient trust level to use `dns_search`")
|
||||
}
|
||||
if len(c.ExtraHosts) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use extra_hosts")
|
||||
errors = append(errors, "Insufficient trust level to use `extra_hosts`")
|
||||
}
|
||||
if len(c.NetworkMode) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use network_mode")
|
||||
errors = append(errors, "Insufficient trust level to use `network_mode`")
|
||||
}
|
||||
}
|
||||
if !l.trusted.Volumes {
|
||||
if len(c.Devices) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use devices")
|
||||
errors = append(errors, "Insufficient trust level to use `devices`")
|
||||
}
|
||||
if len(c.Volumes.Volumes) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use volumes")
|
||||
errors = append(errors, "Insufficient trust level to use `volumes`")
|
||||
}
|
||||
if len(c.Tmpfs) != 0 {
|
||||
errors = append(errors, "Insufficient privileges to use tmpfs")
|
||||
errors = append(errors, "Insufficient trust level to use `tmpfs`")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,7 +279,7 @@ func (l *Linter) lintDeprecations(config *WorkflowConfig) (err error) {
|
|||
if len(container.Secrets) > 0 {
|
||||
err = multierr.Append(err, &errorTypes.PipelineError{
|
||||
Type: errorTypes.PipelineErrorTypeDeprecation,
|
||||
Message: "Secrets are deprecated, use environment with from_secret",
|
||||
Message: "Usage of `secrets` is deprecated, use `environment` with `from_secret`",
|
||||
Data: errors.DeprecationErrorData{
|
||||
File: config.File,
|
||||
Field: fmt.Sprintf("steps.%s.secrets", container.Name),
|
||||
|
@ -328,7 +328,7 @@ func (l *Linter) lintBadHabits(config *WorkflowConfig) (err error) {
|
|||
if field != "" {
|
||||
err = multierr.Append(err, &errorTypes.PipelineError{
|
||||
Type: errorTypes.PipelineErrorTypeBadHabit,
|
||||
Message: "Please set an event filter for all steps or the whole workflow on all items of the when block",
|
||||
Message: "Set an event filter for all steps or the entire workflow on all items of the `when` block",
|
||||
Data: errors.BadHabitErrorData{
|
||||
File: config.File,
|
||||
Field: field,
|
||||
|
|
|
@ -114,7 +114,7 @@ func TestLintErrors(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
from: "",
|
||||
want: "Invalid or missing steps section",
|
||||
want: "Invalid or missing `steps` section",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: '' } }",
|
||||
|
@ -122,48 +122,48 @@ func TestLintErrors(t *testing.T) {
|
|||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, privileged: true } }",
|
||||
want: "Insufficient privileges to use privileged mode",
|
||||
want: "Insufficient trust level to use `privileged` mode",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, dns: [ 8.8.8.8 ] } }",
|
||||
want: "Insufficient privileges to use custom dns",
|
||||
want: "Insufficient trust level to use custom `dns`",
|
||||
},
|
||||
|
||||
{
|
||||
from: "steps: { build: { image: golang, dns_search: [ example.com ] } }",
|
||||
want: "Insufficient privileges to use dns_search",
|
||||
want: "Insufficient trust level to use `dns_search`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, devices: [ '/dev/tty0:/dev/tty0' ] } }",
|
||||
want: "Insufficient privileges to use devices",
|
||||
want: "Insufficient trust level to use `devices`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, extra_hosts: [ 'somehost:162.242.195.82' ] } }",
|
||||
want: "Insufficient privileges to use extra_hosts",
|
||||
want: "Insufficient trust level to use `extra_hosts`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, network_mode: host } }",
|
||||
want: "Insufficient privileges to use network_mode",
|
||||
want: "Insufficient trust level to use `network_mode`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, volumes: [ '/opt/data:/var/lib/mysql' ] } }",
|
||||
want: "Insufficient privileges to use volumes",
|
||||
want: "Insufficient trust level to use `volumes`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, network_mode: 'container:name' } }",
|
||||
want: "Insufficient privileges to use network_mode",
|
||||
want: "Insufficient trust level to use `network_mode`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, settings: { test: 'true' }, commands: [ 'echo ja', 'echo nein' ] } }",
|
||||
want: "Cannot configure both commands and settings",
|
||||
want: "Cannot configure both `commands` and `settings`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, settings: { test: 'true' }, entrypoint: [ '/bin/fish' ] } }",
|
||||
want: "Cannot configure both entrypoint and settings",
|
||||
want: "Cannot configure both `entrypoint` and `settings`",
|
||||
},
|
||||
{
|
||||
from: "steps: { build: { image: golang, settings: { test: 'true' }, environment: { 'TEST': 'true' } } }",
|
||||
want: "Should not configure both environment and settings",
|
||||
want: "Should not configure both `environment` and `settings`",
|
||||
},
|
||||
{
|
||||
from: "{pipeline: { build: { image: golang, settings: { test: 'true' } } }, when: { branch: main, event: push } }",
|
||||
|
@ -171,11 +171,11 @@ func TestLintErrors(t *testing.T) {
|
|||
},
|
||||
{
|
||||
from: "{steps: { build: { image: plugins/docker, settings: { test: 'true' } } }, when: { branch: main, event: push } } }",
|
||||
want: "The formerly privileged plugin 'plugins/docker' is no longer privileged by default, if required, add it to WOODPECKER_PLUGINS_PRIVILEGED",
|
||||
want: "The formerly privileged plugin `plugins/docker` is no longer privileged by default, if required, add it to `WOODPECKER_PLUGINS_PRIVILEGED`",
|
||||
},
|
||||
{
|
||||
from: "{steps: { build: { image: golang, settings: { test: 'true' } } }, when: { branch: main, event: push }, clone: { git: { image: some-other/plugin-git:v1.1.0 } } }",
|
||||
want: "Specified clone image does not match allow list, netrc will not be injected",
|
||||
want: "Specified clone image does not match allow list, netrc is not injected",
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -209,11 +209,11 @@ func TestBadHabits(t *testing.T) {
|
|||
}{
|
||||
{
|
||||
from: "steps: { build: { image: golang } }",
|
||||
want: "Please set an event filter for all steps or the whole workflow on all items of the when block",
|
||||
want: "Set an event filter for all steps or the entire workflow on all items of the `when` block",
|
||||
},
|
||||
{
|
||||
from: "when: [{branch: xyz}, {event: push}]\nsteps: { build: { image: golang } }",
|
||||
want: "Please set an event filter for all steps or the whole workflow on all items of the when block",
|
||||
want: "Set an event filter for all steps or the entire workflow on all items of the `when` block",
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
"@vueuse/core": "^11.0.0",
|
||||
"ansi_up": "^6.0.2",
|
||||
"dayjs": "^1.11.12",
|
||||
"dompurify": "^3.2.0",
|
||||
"fuse.js": "^7.0.0",
|
||||
"js-base64": "^3.7.7",
|
||||
"lodash": "^4.17.21",
|
||||
"marked": "^15.0.0",
|
||||
"node-emoji": "^2.1.3",
|
||||
"pinia": "^2.2.1",
|
||||
"prismjs": "^1.29.0",
|
||||
|
|
|
@ -29,6 +29,9 @@ importers:
|
|||
dayjs:
|
||||
specifier: ^1.11.12
|
||||
version: 1.11.13
|
||||
dompurify:
|
||||
specifier: ^3.2.0
|
||||
version: 3.2.0
|
||||
fuse.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
|
@ -38,6 +41,9 @@ importers:
|
|||
lodash:
|
||||
specifier: ^4.17.21
|
||||
version: 4.17.21
|
||||
marked:
|
||||
specifier: ^15.0.0
|
||||
version: 15.0.0
|
||||
node-emoji:
|
||||
specifier: ^2.1.3
|
||||
version: 2.1.3
|
||||
|
@ -1310,6 +1316,9 @@ packages:
|
|||
resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==}
|
||||
engines: {node: '>= 4'}
|
||||
|
||||
dompurify@3.2.0:
|
||||
resolution: {integrity: sha512-AMdOzK44oFWqHEi0wpOqix/fUNY707OmoeFDnbi3Q5I8uOpy21ufUA5cDJPr0bosxrflOVD/H2DMSvuGKJGfmQ==}
|
||||
|
||||
domutils@3.1.0:
|
||||
resolution: {integrity: sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==}
|
||||
|
||||
|
@ -1921,6 +1930,11 @@ packages:
|
|||
markdown-table@3.0.3:
|
||||
resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==}
|
||||
|
||||
marked@15.0.0:
|
||||
resolution: {integrity: sha512-0mouKmBROJv/WSHJBPZZyYofUgawMChnD5je/g+aOBXsHDjb/IsnTQj7mnhQZu+qPJmRQ0ecX3mLGEUm3BgwYA==}
|
||||
engines: {node: '>= 18'}
|
||||
hasBin: true
|
||||
|
||||
mdast-util-find-and-replace@3.0.1:
|
||||
resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==}
|
||||
|
||||
|
@ -4055,6 +4069,8 @@ snapshots:
|
|||
dependencies:
|
||||
domelementtype: 2.3.0
|
||||
|
||||
dompurify@3.2.0: {}
|
||||
|
||||
domutils@3.1.0:
|
||||
dependencies:
|
||||
dom-serializer: 2.0.0
|
||||
|
@ -4766,6 +4782,8 @@ snapshots:
|
|||
|
||||
markdown-table@3.0.3: {}
|
||||
|
||||
marked@15.0.0: {}
|
||||
|
||||
mdast-util-find-and-replace@3.0.1:
|
||||
dependencies:
|
||||
'@types/mdast': 4.0.4
|
||||
|
|
|
@ -3,9 +3,9 @@
|
|||
:href="`${docsUrl}`"
|
||||
:title="$t('documentation_for', { topic })"
|
||||
target="_blank"
|
||||
class="text-wp-link-100 hover:text-wp-link-200 cursor-pointer mt-1"
|
||||
class="text-wp-link-100 hover:text-wp-link-200 cursor-pointer"
|
||||
>
|
||||
<Icon name="question" class="!w-4 !h-4" />
|
||||
<Icon name="question" class="!w-5 !h-5" />
|
||||
</a>
|
||||
</template>
|
||||
|
||||
|
|
18
web/src/components/atomic/RenderMarkdown.vue
Normal file
18
web/src/components/atomic/RenderMarkdown.vue
Normal file
|
@ -0,0 +1,18 @@
|
|||
<template>
|
||||
<span v-html="contentHTML" />
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import DOMPurify from 'dompurify';
|
||||
import { marked } from 'marked';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps<{
|
||||
content: string;
|
||||
}>();
|
||||
|
||||
const contentHTML = computed<string>(() => {
|
||||
const dirtyHTML = marked.parse(props.content);
|
||||
return DOMPurify.sanitize(dirtyHTML as string, { USE_PROFILES: { html: true } });
|
||||
});
|
||||
</script>
|
|
@ -14,9 +14,9 @@
|
|||
</template>
|
||||
<template #description>
|
||||
<i18n-t keypath="repo.settings.general.pipeline_path.desc" tag="p" class="text-sm text-wp-text-alt-100">
|
||||
<span class="code-box-inline px-1">{{ $t('repo.settings.general.pipeline_path.desc_path_example') }}</span>
|
||||
<span class="code-box-inline">{{ $t('repo.settings.general.pipeline_path.desc_path_example') }}</span>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span class="code-box-inline px-1">/</span>
|
||||
<span class="code-box-inline">/</span>
|
||||
</i18n-t>
|
||||
</template>
|
||||
</InputField>
|
||||
|
|
|
@ -147,6 +147,7 @@ body,
|
|||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.code-box-inline {
|
||||
@apply bg-wp-code-200 rounded-md text-wp-code-text-100;
|
||||
.code-box-inline,
|
||||
code:not(pre > code) {
|
||||
@apply bg-wp-code-200 rounded-md text-wp-code-text-100 px-1 py-px;
|
||||
}
|
||||
|
|
|
@ -1,37 +1,47 @@
|
|||
<template>
|
||||
<Panel>
|
||||
<div class="grid justify-center gap-x-4 text-left grid-3-1">
|
||||
<template v-for="(error, i) in pipeline!.errors" :key="i">
|
||||
<Icon
|
||||
name="attention"
|
||||
class="flex-shrink-0 my-1"
|
||||
:class="{
|
||||
'text-wp-state-warn-100': error.is_warning,
|
||||
'text-wp-state-error-100': !error.is_warning,
|
||||
}"
|
||||
/>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>[{{ error.type }}]</span>
|
||||
<span
|
||||
v-if="isLinterError(error) || isDeprecationError(error) || isBadHabitError(error)"
|
||||
class="whitespace-nowrap"
|
||||
>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span v-if="error.data?.file" class="font-bold">{{ error.data?.file }}: </span>
|
||||
<span>{{ error.data?.field }}</span>
|
||||
</span>
|
||||
<span v-else />
|
||||
<a
|
||||
v-if="isDeprecationError(error) || isBadHabitError(error)"
|
||||
:href="error.data?.docs"
|
||||
target="_blank"
|
||||
class="underline col-span-full col-start-2 md:col-span-auto md:col-start-auto"
|
||||
>
|
||||
{{ error.message }}
|
||||
</a>
|
||||
<span v-else class="col-span-full col-start-2 md:col-span-auto md:col-start-auto">
|
||||
{{ error.message }}
|
||||
</span>
|
||||
<div class="flex flex-col gap-y-4">
|
||||
<template v-for="(error, _index) in pipeline!.errors" :key="_index">
|
||||
<div>
|
||||
<div class="grid grid-cols-[minmax(10rem,auto),3fr]">
|
||||
<span class="flex items-center gap-x-2">
|
||||
<Icon
|
||||
name="attention"
|
||||
class="flex-shrink-0 my-1"
|
||||
:class="{
|
||||
'text-wp-state-warn-100': error.is_warning,
|
||||
'text-wp-state-error-100': !error.is_warning,
|
||||
}"
|
||||
/>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span>
|
||||
<code>{{ error.type }}</code>
|
||||
</span>
|
||||
</span>
|
||||
<span
|
||||
v-if="isLinterError(error) || isDeprecationError(error) || isBadHabitError(error)"
|
||||
class="flex items-center gap-x-2 whitespace-nowrap"
|
||||
>
|
||||
<span>
|
||||
<!-- eslint-disable-next-line @intlify/vue-i18n/no-raw-text -->
|
||||
<span v-if="error.data?.file" class="font-bold">{{ error.data?.file }}: </span>
|
||||
<span>{{ error.data?.field }}</span>
|
||||
</span>
|
||||
<DocsLink
|
||||
v-if="isDeprecationError(error) || isBadHabitError(error)"
|
||||
:topic="error.data?.field || ''"
|
||||
:url="error.data?.docs || ''"
|
||||
/>
|
||||
</span>
|
||||
<span v-else />
|
||||
</div>
|
||||
<div class="grid grid-cols-[minmax(10rem,auto),4fr] col-start-2">
|
||||
<span />
|
||||
<span>
|
||||
<RenderMarkdown :content="error.message" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</Panel>
|
||||
|
@ -40,7 +50,9 @@
|
|||
<script lang="ts" setup>
|
||||
import { inject, type Ref } from 'vue';
|
||||
|
||||
import DocsLink from '~/components/atomic/DocsLink.vue';
|
||||
import Icon from '~/components/atomic/Icon.vue';
|
||||
import RenderMarkdown from '~/components/atomic/RenderMarkdown.vue';
|
||||
import Panel from '~/components/layout/Panel.vue';
|
||||
import type { Pipeline, PipelineError } from '~/lib/api/types';
|
||||
|
||||
|
@ -63,9 +75,3 @@ function isBadHabitError(error: PipelineError): error is PipelineError<{ file?:
|
|||
return error.type === 'bad_habit';
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.grid-3-1 {
|
||||
grid-template-columns: auto auto auto 1fr;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue