From 1f8b3b5e1b52fb769b4848d5047ded435d7a4b83 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Fri, 15 Dec 2023 10:03:05 +0100 Subject: [PATCH 1/6] Only update pipelineStatus in one place (#2952) --- pipeline/backend/types/config.go | 4 ++-- server/pipeline/create.go | 26 ++++++++++---------------- server/pipeline/pipelineStatus.go | 6 ++++-- 3 files changed, 16 insertions(+), 20 deletions(-) diff --git a/pipeline/backend/types/config.go b/pipeline/backend/types/config.go index b1df35a432..0fe1e6fc01 100644 --- a/pipeline/backend/types/config.go +++ b/pipeline/backend/types/config.go @@ -14,9 +14,9 @@ package types -// Config defines the runtime configuration of a pipeline. +// Config defines the runtime configuration of a workflow. type Config struct { - Stages []*Stage `json:"pipeline"` // pipeline stages + Stages []*Stage `json:"pipeline"` // workflow stages Networks []*Network `json:"networks"` // network definitions Volumes []*Volume `json:"volumes"` // volume definitions Secrets []*Secret `json:"secrets"` // secret definitions diff --git a/server/pipeline/create.go b/server/pipeline/create.go index d440eccb55..64bd6f6237 100644 --- a/server/pipeline/create.go +++ b/server/pipeline/create.go @@ -18,7 +18,6 @@ import ( "context" "fmt" "regexp" - "time" "github.com/rs/zerolog/log" @@ -128,16 +127,12 @@ func Create(ctx context.Context, _store store.Store, repo *model.Repo, pipeline } func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User, err error) error { - pipeline.Started = time.Now().Unix() - pipeline.Finished = pipeline.Started - pipeline.Status = model.StatusError - pipeline.Errors = errors.GetPipelineErrors(err) - dbErr := _store.UpdatePipeline(pipeline) - if dbErr != nil { - msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName) - log.Error().Err(dbErr).Msg(msg.Error()) - return msg + _pipeline, err := UpdateToStatusError(_store, *pipeline, err) + if err != nil { + return err } + // update value in ref + *pipeline = *_pipeline publishPipeline(ctx, pipeline, repo, repoUser) @@ -145,13 +140,12 @@ func updatePipelineWithErr(ctx context.Context, _store store.Store, pipeline *mo } func updatePipelinePending(ctx context.Context, _store store.Store, pipeline *model.Pipeline, repo *model.Repo, repoUser *model.User) error { - pipeline.Status = model.StatusPending - dbErr := _store.UpdatePipeline(pipeline) - if dbErr != nil { - msg := fmt.Errorf("failed to save pipeline for %s", repo.FullName) - log.Error().Err(dbErr).Msg(msg.Error()) - return msg + _pipeline, err := UpdateToStatusPending(_store, *pipeline, "") + if err != nil { + return err } + // update value in ref + *pipeline = *_pipeline publishPipeline(ctx, pipeline, repo, repoUser) diff --git a/server/pipeline/pipelineStatus.go b/server/pipeline/pipelineStatus.go index 2bf94423b3..1081b43636 100644 --- a/server/pipeline/pipelineStatus.go +++ b/server/pipeline/pipelineStatus.go @@ -29,9 +29,11 @@ func UpdateToStatusRunning(store model.UpdatePipelineStore, pipeline model.Pipel } func UpdateToStatusPending(store model.UpdatePipelineStore, pipeline model.Pipeline, reviewer string) (*model.Pipeline, error) { - pipeline.Reviewer = reviewer + if reviewer != "" { + pipeline.Reviewer = reviewer + pipeline.Reviewed = time.Now().Unix() + } pipeline.Status = model.StatusPending - pipeline.Reviewed = time.Now().Unix() return &pipeline, store.UpdatePipeline(&pipeline) } From 60a3922e02e69e9a31bc6e7660eb7523ae26b162 Mon Sep 17 00:00:00 2001 From: qwerty287 <80460567+qwerty287@users.noreply.github.com> Date: Sat, 16 Dec 2023 09:45:02 +0100 Subject: [PATCH 2/6] Update README badges (#2956) - use repo id - remove tickgit (the service is broken) - use dynamic pre-commit.ci badge --- README.md | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 3d76ddba9b..f509e798c4 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,8 @@


- - + + @@ -40,11 +40,8 @@ - - - - - + +


From 16803d62177796f9ae49372f272d5d8a58c6a3eb Mon Sep 17 00:00:00 2001 From: Anbraten Date: Sat, 16 Dec 2023 10:29:13 +0100 Subject: [PATCH 3/6] Show secrets from org and global level (#2873) Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com> --- cmd/server/docs/docs.go | 6 ++ server/model/secret.go | 13 ++- server/plugins/secrets/builtin.go | 15 ++-- server/plugins/secrets/builtin_test.go | 2 +- server/store/datastore/secret_test.go | 4 +- web/src/assets/locales/en.json | 12 +-- web/src/components/atomic/Button.vue | 2 +- .../components/repo/settings/SecretsTab.vue | 51 ++++++++++-- web/src/components/secrets/SecretEdit.vue | 52 ++++++++---- web/src/components/secrets/SecretList.vue | 36 ++++---- web/src/compositions/usePaginate.ts | 82 +++++++++++-------- web/src/lib/api/types/secret.ts | 2 + woodpecker-go/woodpecker/types.go | 2 + 13 files changed, 188 insertions(+), 91 deletions(-) diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index fedbdf4331..a8d14cfe20 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -4233,6 +4233,12 @@ const docTemplate = `{ "name": { "type": "string" }, + "org_id": { + "type": "integer" + }, + "repo_id": { + "type": "integer" + }, "value": { "type": "string" } diff --git a/server/model/secret.go b/server/model/secret.go index 2de79ce20e..7dbb087e97 100644 --- a/server/model/secret.go +++ b/server/model/secret.go @@ -70,8 +70,8 @@ type SecretStore interface { // Secret represents a secret variable, such as a password or token. type Secret struct { ID int64 `json:"id" xorm:"pk autoincr 'secret_id'"` - OrgID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_org_id'"` - RepoID int64 `json:"-" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"` + OrgID int64 `json:"org_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_org_id'"` + RepoID int64 `json:"repo_id" xorm:"NOT NULL DEFAULT 0 UNIQUE(s) INDEX 'secret_repo_id'"` Name string `json:"name" xorm:"NOT NULL UNIQUE(s) INDEX 'secret_name'"` Value string `json:"value,omitempty" xorm:"TEXT 'secret_value'"` Images []string `json:"images" xorm:"json 'secret_images'"` @@ -89,15 +89,20 @@ func (s *Secret) BeforeInsert() { } // Global secret. -func (s Secret) Global() bool { +func (s Secret) IsGlobal() bool { return s.RepoID == 0 && s.OrgID == 0 } // Organization secret. -func (s Secret) Organization() bool { +func (s Secret) IsOrganization() bool { return s.RepoID == 0 && s.OrgID != 0 } +// Repository secret. +func (s Secret) IsRepository() bool { + return s.RepoID != 0 && s.OrgID == 0 +} + // Match returns true if an image and event match the restricted list. func (s *Secret) Match(event WebhookEvent) bool { if len(s.Events) == 0 { diff --git a/server/plugins/secrets/builtin.go b/server/plugins/secrets/builtin.go index e19518ce99..bb2e40ffae 100644 --- a/server/plugins/secrets/builtin.go +++ b/server/plugins/secrets/builtin.go @@ -48,16 +48,17 @@ func (b *builtin) SecretListPipeline(repo *model.Repo, _ *model.Pipeline, p *mod // Priority order in case of duplicate names are repository, user/organization, global secrets := make([]*model.Secret, 0, len(s)) uniq := make(map[string]struct{}) - for _, cond := range []struct { - Global bool - Organization bool + for _, condition := range []struct { + IsRepository bool + IsOrganization bool + IsGlobal bool }{ - {}, - {Organization: true}, - {Global: true}, + {IsRepository: true}, + {IsOrganization: true}, + {IsGlobal: true}, } { for _, secret := range s { - if secret.Global() != cond.Global || secret.Organization() != cond.Organization { + if secret.IsRepository() != condition.IsRepository || secret.IsOrganization() != condition.IsOrganization || secret.IsGlobal() != condition.IsGlobal { continue } if _, ok := uniq[secret.Name]; ok { diff --git a/server/plugins/secrets/builtin_test.go b/server/plugins/secrets/builtin_test.go index a620a5735c..c2f2509ac9 100644 --- a/server/plugins/secrets/builtin_test.go +++ b/server/plugins/secrets/builtin_test.go @@ -51,7 +51,7 @@ func TestSecretListPipeline(t *testing.T) { // repo secret repoSecret := &model.Secret{ ID: 3, - OrgID: 1, + OrgID: 0, RepoID: 1, Name: "secret", Value: "value-repo", diff --git a/server/store/datastore/secret_test.go b/server/store/datastore/secret_test.go index 917f75c4e8..78d92eabad 100644 --- a/server/store/datastore/secret_test.go +++ b/server/store/datastore/secret_test.go @@ -253,7 +253,7 @@ func TestOrgSecretList(t *testing.T) { assert.NoError(t, err) assert.Len(t, list, 1) - assert.True(t, list[0].Organization()) + assert.True(t, list[0].IsOrganization()) } func TestGlobalSecretFind(t *testing.T) { @@ -306,5 +306,5 @@ func TestGlobalSecretList(t *testing.T) { assert.NoError(t, err) assert.Len(t, list, 1) - assert.True(t, list[0].Global()) + assert.True(t, list[0].IsGlobal()) } diff --git a/web/src/assets/locales/en.json b/web/src/assets/locales/en.json index 2885a2aa12..eabd2efa84 100644 --- a/web/src/assets/locales/en.json +++ b/web/src/assets/locales/en.json @@ -140,7 +140,7 @@ "saved": "Secret saved", "images": { "images": "Available for following images", - "desc": "Comma separated list of images where this secret is available, leave empty to allow all images" + "desc": "List of images where this secret is available, leave empty to allow all images" }, "events": { "events": "Available at following events", @@ -305,7 +305,7 @@ "saved": "Organization secret saved", "images": { "images": "Available for following images", - "desc": "Comma separated list of images where this secret is available, leave empty to allow all images" + "desc": "List of images where this secret is available, leave empty to allow all images" }, "plugins_only": "Only available for plugins", "events": { @@ -334,7 +334,7 @@ "saved": "Global secret saved", "images": { "images": "Available for following images", - "desc": "Comma separated list of images where this secret is available, leave empty to allow all images" + "desc": "List of images where this secret is available, leave empty to allow all images" }, "plugins_only": "Only available for plugins", "events": { @@ -476,7 +476,7 @@ "saved": "User secret saved", "images": { "images": "Available for following images", - "desc": "Comma separated list of images where this secret is available, leave empty to allow all images" + "desc": "List of images where this secret is available, leave empty to allow all images" }, "plugins_only": "Only available for plugins", "events": { @@ -504,5 +504,7 @@ "default": "default", "info": "Info", "running_version": "You are running Woodpecker {0}", - "update_woodpecker": "Please update your Woodpecker instance to {0}" + "update_woodpecker": "Please update your Woodpecker instance to {0}", + "global_level_secret": "global secret", + "org_level_secret": "organization secret" } diff --git a/web/src/components/atomic/Button.vue b/web/src/components/atomic/Button.vue index db656554d2..b9d6129a26 100644 --- a/web/src/components/atomic/Button.vue +++ b/web/src/components/atomic/Button.vue @@ -16,7 +16,7 @@ > - {{ text }} + {{ text }}
>('repo'); const selectedSecret = ref>(); const isEditingSecret = computed(() => !!selectedSecret.value?.id); -async function loadSecrets(page: number): Promise { +async function loadSecrets(page: number, level: 'repo' | 'org' | 'global'): Promise { if (!repo?.value) { throw new Error("Unexpected: Can't load repo"); } - return apiClient.getSecretList(repo.value.id, page); + switch (level) { + case 'repo': + return apiClient.getSecretList(repo.value.id, page); + case 'org': + return apiClient.getOrgSecretList(repo.value.org_id, page); + case 'global': + return apiClient.getGlobalSecretList(page); + default: + throw new Error(`Unexpected level: ${level}`); + } } -const { resetPage, data: secrets } = usePagination(loadSecrets, () => !selectedSecret.value); +const { resetPage, data: _secrets } = usePagination(loadSecrets, () => !selectedSecret.value, { + each: ['repo', 'org', 'global'], +}); +const secrets = computed(() => { + const secretsList: Record = {}; + + // eslint-disable-next-line no-restricted-syntax + for (const level of ['repo', 'org', 'global']) { + // eslint-disable-next-line no-restricted-syntax + for (const secret of _secrets.value) { + if ( + ((level === 'repo' && secret.repo_id !== 0 && secret.org_id === 0) || + (level === 'org' && secret.repo_id === 0 && secret.org_id !== 0) || + (level === 'global' && secret.repo_id === 0 && secret.org_id === 0)) && + !secretsList[secret.name] + ) { + secretsList[secret.name] = { ...secret, edit: secret.repo_id !== 0, level }; + } + } + } + + const levelsOrder = { + global: 0, + org: 1, + repo: 2, + }; + + return Object.values(secretsList) + .toSorted((a, b) => a.name.localeCompare(b.name)) + .toSorted((a, b) => levelsOrder[b.level] - levelsOrder[a.level]); +}); const { doSubmit: createSecret, isLoading: isSaving } = useAsyncAction(async () => { if (!repo?.value) { @@ -93,7 +132,7 @@ const { doSubmit: createSecret, isLoading: isSaving } = useAsyncAction(async () type: 'success', }); selectedSecret.value = undefined; - resetPage(); + await resetPage(); }); const { doSubmit: deleteSecret, isLoading: isDeleting } = useAsyncAction(async (_secret: Secret) => { @@ -103,7 +142,7 @@ const { doSubmit: deleteSecret, isLoading: isDeleting } = useAsyncAction(async ( await apiClient.deleteSecret(repo.value.id, _secret.name); notifications.notify({ title: i18n.t('repo.settings.secrets.deleted'), type: 'success' }); - resetPage(); + await resetPage(); }); function editSecret(secret: Secret) { diff --git a/web/src/components/secrets/SecretEdit.vue b/web/src/components/secrets/SecretEdit.vue index 0130f07b2e..aa52bbb99b 100644 --- a/web/src/components/secrets/SecretEdit.vue +++ b/web/src/components/secrets/SecretEdit.vue @@ -11,11 +11,27 @@ - + - + {{ $t(i18nPrefix + 'images.desc') }} + +
+
+ +
+
+ +
+
@@ -36,7 +52,7 @@ diff --git a/web/src/components/secrets/SecretList.vue b/web/src/components/secrets/SecretList.vue index e43f2bbca9..4faea19df9 100644 --- a/web/src/components/secrets/SecretList.vue +++ b/web/src/components/secrets/SecretList.vue @@ -6,22 +6,29 @@ class="items-center !bg-wp-background-200 !dark:bg-wp-background-100" > {{ secret.name }} +
- - +
{{ $t(i18nPrefix + 'none') }}
@@ -32,12 +39,13 @@ import { toRef } from 'vue'; import { useI18n } from 'vue-i18n'; +import Badge from '~/components/atomic/Badge.vue'; import IconButton from '~/components/atomic/IconButton.vue'; import ListItem from '~/components/atomic/ListItem.vue'; import { Secret } from '~/lib/api/types'; const props = defineProps<{ - modelValue: Secret[]; + modelValue: (Secret & { edit?: boolean })[]; isDeleting: boolean; i18nPrefix: string; }>(); diff --git a/web/src/compositions/usePaginate.ts b/web/src/compositions/usePaginate.ts index 69295c5a5c..33002c09c3 100644 --- a/web/src/compositions/usePaginate.ts +++ b/web/src/compositions/usePaginate.ts @@ -1,5 +1,5 @@ import { useInfiniteScroll } from '@vueuse/core'; -import { onMounted, Ref, ref, watch } from 'vue'; +import { onMounted, Ref, ref, UnwrapRef, watch } from 'vue'; export async function usePaginate(getSingle: (page: number) => Promise): Promise { let hasMore = true; @@ -15,59 +15,71 @@ export async function usePaginate(getSingle: (page: number) => Promise): return result; } -export function usePagination( - _loadData: (page: number) => Promise, +export function usePagination( + _loadData: (page: number, arg: S) => Promise, isActive: () => boolean = () => true, - scrollElement = ref(document.getElementById('scroll-component')), + { scrollElement: _scrollElement, each: _each }: { scrollElement?: Ref; each?: S[] } = {}, ) { + const scrollElement = _scrollElement ?? ref(document.getElementById('scroll-component')); const page = ref(1); const pageSize = ref(0); const hasMore = ref(true); const data = ref([]) as Ref; const loading = ref(false); + const each = ref(_each ?? []); async function loadData() { + if (loading.value === true || hasMore.value === false) { + return; + } + loading.value = true; - const newData = await _loadData(page.value); - hasMore.value = newData !== null && newData.length >= pageSize.value; - if (newData !== null && newData.length !== 0) { - if (page.value === 1) { - pageSize.value = newData.length; - data.value = newData; - } else { - data.value.push(...newData); + const newData = (await _loadData(page.value, each.value?.[0] as S)) ?? []; + hasMore.value = newData.length >= pageSize.value && newData.length > 0; + if (newData.length > 0) { + data.value.push(...newData); + } + + // last page and each has more + if (!hasMore.value && each.value.length > 0) { + // use next each element + each.value.shift(); + page.value = 1; + pageSize.value = 0; + hasMore.value = each.value.length > 0; + if (hasMore.value) { + loading.value = false; + await loadData(); } - } else if (page.value === 1) { - data.value = []; - } else { - hasMore.value = false; } + pageSize.value = newData.length; loading.value = false; } onMounted(loadData); watch(page, loadData); - useInfiniteScroll( - scrollElement, - () => { - if (isActive() && !loading.value && hasMore.value) { - // load more - page.value += 1; - } - }, - { distance: 10 }, - ); + function nextPage() { + if (isActive() && !loading.value && hasMore.value) { + page.value += 1; + } + } - const resetPage = () => { - if (page.value !== 1) { - // just set page = 1, will be handled by watcher - page.value = 1; - } else { - // we need to reload, but page is already 1, so changing won't trigger watcher - loadData(); + useInfiniteScroll(scrollElement, nextPage, { distance: 10 }); + + async function resetPage() { + const _page = page.value; + + hasMore.value = true; + data.value = []; + each.value = (_each ?? []) as UnwrapRef; + page.value = 1; + + if (_page === 1) { + // we need to reload manually as the page is already 1, so changing won't trigger watcher + await loadData(); } - }; + } - return { resetPage, data }; + return { resetPage, nextPage, data, hasMore, loading }; } diff --git a/web/src/lib/api/types/secret.ts b/web/src/lib/api/types/secret.ts index 33c9612574..301b83341c 100644 --- a/web/src/lib/api/types/secret.ts +++ b/web/src/lib/api/types/secret.ts @@ -2,6 +2,8 @@ import { WebhookEvents } from './webhook'; export type Secret = { id: string; + repo_id: number; + org_id: number; name: string; value: string; events: WebhookEvents[]; diff --git a/woodpecker-go/woodpecker/types.go b/woodpecker-go/woodpecker/types.go index d243d36f0e..ba2cff1b75 100644 --- a/woodpecker-go/woodpecker/types.go +++ b/woodpecker-go/woodpecker/types.go @@ -141,6 +141,8 @@ type ( // Secret represents a secret variable, such as a password or token. Secret struct { ID int64 `json:"id"` + OrgID int64 `json:"org_id"` + RepoID int64 `json:"repo_id"` Name string `json:"name"` Value string `json:"value,omitempty"` Images []string `json:"images"` From 57790e41760a8d3e8a1958369783925414775ce1 Mon Sep 17 00:00:00 2001 From: Robert Kaussow Date: Sat, 16 Dec 2023 21:27:46 +0100 Subject: [PATCH 4/6] Fix error container overflow (#2957) Fixes: https://github.com/woodpecker-ci/woodpecker/issues/2947 ![image](https://github.com/woodpecker-ci/woodpecker/assets/3391958/03198aec-fd3c-4fcd-8418-a7c5b0ff9d0b) On the mobile view, it now wraps to show error on top. That is still not perfect as it creates content jumps, after clicking on a pipeline with errors. I don't have a better idea yet, but IMO it's already an improvement as before it was quite unusable on mobile view. Before: ![image](https://github.com/woodpecker-ci/woodpecker/assets/3391958/20849de8-55d6-4839-b4b4-fe220003887d) After: ![image](https://github.com/woodpecker-ci/woodpecker/assets/3391958/8a80939b-d6a5-414d-b693-ef4583e2f37d) --- .../components/repo/pipeline/PipelineLog.vue | 2 +- .../repo/pipeline/PipelineStepList.vue | 2 +- web/src/views/repo/pipeline/Pipeline.vue | 20 +++++++++---------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/web/src/components/repo/pipeline/PipelineLog.vue b/web/src/components/repo/pipeline/PipelineLog.vue index c573b80e15..fbc1aaccb9 100644 --- a/web/src/components/repo/pipeline/PipelineLog.vue +++ b/web/src/components/repo/pipeline/PipelineLog.vue @@ -1,7 +1,7 @@