diff --git a/cmd/server/docs/docs.go b/cmd/server/docs/docs.go index 6d5cbed58..7ea2dd04f 100644 --- a/cmd/server/docs/docs.go +++ b/cmd/server/docs/docs.go @@ -597,6 +597,197 @@ const docTemplate = `{ } } }, + "/forges": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Forges" + ], + "summary": "List forges", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "default": 1, + "description": "for response pagination, page offset number", + "name": "page", + "in": "query" + }, + { + "type": "integer", + "default": 50, + "description": "for response pagination, max items per page", + "name": "perPage", + "in": "query" + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "type": "array", + "items": { + "$ref": "#/definitions/Forge" + } + } + } + } + }, + "post": { + "description": "Creates a new forge with a random token", + "produces": [ + "application/json" + ], + "tags": [ + "Forges" + ], + "summary": "Create a new forge", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "description": "the forge's data (only 'name' and 'no_schedule' are read)", + "name": "forge", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Forge" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Forge" + } + } + } + } + }, + "/forges/{forgeId}": { + "get": { + "produces": [ + "application/json" + ], + "tags": [ + "Forges" + ], + "summary": "Get a forge", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header" + }, + { + "type": "integer", + "description": "the forge's id", + "name": "forgeId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Forge" + } + } + } + }, + "delete": { + "produces": [ + "text/plain" + ], + "tags": [ + "Forges" + ], + "summary": "Delete a forge", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "the forge's id", + "name": "forgeId", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "OK" + } + } + }, + "patch": { + "produces": [ + "application/json" + ], + "tags": [ + "Forges" + ], + "summary": "Update a forge", + "parameters": [ + { + "type": "string", + "default": "Bearer \u003cpersonal access token\u003e", + "description": "Insert your personal access token", + "name": "Authorization", + "in": "header", + "required": true + }, + { + "type": "integer", + "description": "the forge's id", + "name": "forgeId", + "in": "path", + "required": true + }, + { + "description": "the forge's data", + "name": "forgeData", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Forge" + } + } + ], + "responses": { + "200": { + "description": "OK", + "schema": { + "$ref": "#/definitions/Forge" + } + } + } + } + }, "/healthz": { "get": { "description": "If everything is fine, just a 204 will be returned, a 500 signals server state is unhealthy.", @@ -3902,6 +4093,34 @@ const docTemplate = `{ } } }, + "Forge": { + "type": "object", + "properties": { + "additional_options": { + "type": "object", + "additionalProperties": {} + }, + "client": { + "type": "string" + }, + "id": { + "type": "integer" + }, + "oauth_host": { + "description": "public url for oauth if different from url", + "type": "string" + }, + "skip_verify": { + "type": "boolean" + }, + "type": { + "$ref": "#/definitions/model.ForgeType" + }, + "url": { + "type": "string" + } + } + }, "LogEntry": { "type": "object", "properties": { @@ -4524,6 +4743,27 @@ const docTemplate = `{ "EventManual" ] }, + "model.ForgeType": { + "type": "string", + "enum": [ + "github", + "gitlab", + "gitea", + "forgejo", + "bitbucket", + "bitbucket-dc", + "addon" + ], + "x-enum-varnames": [ + "ForgeTypeGithub", + "ForgeTypeGitlab", + "ForgeTypeGitea", + "ForgeTypeForgejo", + "ForgeTypeBitbucket", + "ForgeTypeBitbucketDatacenter", + "ForgeTypeAddon" + ] + }, "model.Workflow": { "type": "object", "properties": { diff --git a/server/api/forge.go b/server/api/forge.go new file mode 100644 index 000000000..cf82e3aa5 --- /dev/null +++ b/server/api/forge.go @@ -0,0 +1,208 @@ +// Copyright 2024 Woodpecker Authors +// +// 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 api + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + + "go.woodpecker-ci.org/woodpecker/v2/server/model" + "go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session" + "go.woodpecker-ci.org/woodpecker/v2/server/store" +) + +// GetForges +// +// @Summary List forges +// @Router /forges [get] +// @Produce json +// @Success 200 {array} Forge +// @Tags Forges +// @Param Authorization header string false "Insert your personal access token" default(Bearer ) +// @Param page query int false "for response pagination, page offset number" default(1) +// @Param perPage query int false "for response pagination, max items per page" default(50) +func GetForges(c *gin.Context) { + forges, err := store.FromContext(c).ForgeList(session.Pagination(c)) + if err != nil { + c.String(http.StatusInternalServerError, "Error getting forge list. %s", err) + return + } + + user := session.User(c) + if user != nil && user.Admin { + c.JSON(http.StatusOK, forges) + return + } + + // copy forges data without sensitive information + for i, forge := range forges { + forges[i] = forge.PublicCopy() + } + + c.JSON(http.StatusOK, forges) +} + +// GetForge +// +// @Summary Get a forge +// @Router /forges/{forgeId} [get] +// @Produce json +// @Success 200 {object} Forge +// @Tags Forges +// @Param Authorization header string false "Insert your personal access token" default(Bearer ) +// @Param forgeId path int true "the forge's id" +func GetForge(c *gin.Context) { + forgeID, err := strconv.ParseInt(c.Param("forgeId"), 10, 64) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + forge, err := store.FromContext(c).ForgeGet(forgeID) + if err != nil { + handleDBError(c, err) + return + } + + user := session.User(c) + if user != nil && user.Admin { + c.JSON(http.StatusOK, forge) + } else { + c.JSON(http.StatusOK, forge.PublicCopy()) + } +} + +// PatchForge +// +// @Summary Update a forge +// @Router /forges/{forgeId} [patch] +// @Produce json +// @Success 200 {object} Forge +// @Tags Forges +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param forgeId path int true "the forge's id" +// @Param forgeData body Forge true "the forge's data" +func PatchForge(c *gin.Context) { + _store := store.FromContext(c) + + // use this struct to allow updating the client secret + type ForgeWithClientSecret struct { + model.Forge + ClientSecret string `json:"client_secret"` + } + + in := &ForgeWithClientSecret{} + err := c.Bind(in) + if err != nil { + c.AbortWithStatus(http.StatusBadRequest) + return + } + + forgeID, err := strconv.ParseInt(c.Param("forgeId"), 10, 64) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + forge, err := _store.ForgeGet(forgeID) + if err != nil { + handleDBError(c, err) + return + } + forge.URL = in.URL + forge.Type = in.Type + forge.Client = in.Client + forge.OAuthHost = in.OAuthHost + forge.SkipVerify = in.SkipVerify + forge.AdditionalOptions = in.AdditionalOptions + if in.ClientSecret != "" { + forge.ClientSecret = in.ClientSecret + } + + err = _store.ForgeUpdate(forge) + if err != nil { + c.AbortWithStatus(http.StatusConflict) + return + } + + c.JSON(http.StatusOK, forge) +} + +// PostForge +// +// @Summary Create a new forge +// @Description Creates a new forge with a random token +// @Router /forges [post] +// @Produce json +// @Success 200 {object} Forge +// @Tags Forges +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param forge body Forge true "the forge's data (only 'name' and 'no_schedule' are read)" +func PostForge(c *gin.Context) { + in := &model.Forge{} + err := c.Bind(in) + if err != nil { + c.String(http.StatusBadRequest, err.Error()) + return + } + + forge := &model.Forge{ + URL: in.URL, + Type: in.Type, + Client: in.Client, + ClientSecret: in.ClientSecret, + OAuthHost: in.OAuthHost, + SkipVerify: in.SkipVerify, + AdditionalOptions: in.AdditionalOptions, + } + if err = store.FromContext(c).ForgeCreate(forge); err != nil { + c.String(http.StatusInternalServerError, err.Error()) + return + } + c.JSON(http.StatusOK, forge) +} + +// DeleteForge +// +// @Summary Delete a forge +// @Router /forges/{forgeId} [delete] +// @Produce plain +// @Success 200 +// @Tags Forges +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param forgeId path int true "the forge's id" +func DeleteForge(c *gin.Context) { + _store := store.FromContext(c) + + forgeID, err := strconv.ParseInt(c.Param("forgeId"), 10, 64) + if err != nil { + _ = c.AbortWithError(http.StatusBadRequest, err) + return + } + + forge, err := _store.ForgeGet(forgeID) + if err != nil { + handleDBError(c, err) + return + } + + if err = _store.ForgeDelete(forge); err != nil { + c.String(http.StatusInternalServerError, "Error deleting user. %s", err) + return + } + c.Status(http.StatusNoContent) +} diff --git a/server/api/org.go b/server/api/org.go index 507185c5c..227859558 100644 --- a/server/api/org.go +++ b/server/api/org.go @@ -28,6 +28,26 @@ import ( "go.woodpecker-ci.org/woodpecker/v2/server/store" ) +// GetOrgs +// +// @Summary List organizations +// @Description Returns all registered orgs in the system. Requires admin rights. +// @Router /orgs [get] +// @Produce json +// @Success 200 {array} Org +// @Tags Orgs +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param page query int false "for response pagination, page offset number" default(1) +// @Param perPage query int false "for response pagination, max items per page" default(50) +func GetOrgs(c *gin.Context) { + orgs, err := store.FromContext(c).OrgList(session.Pagination(c)) + if err != nil { + c.String(http.StatusInternalServerError, "Error getting user list. %s", err) + return + } + c.JSON(http.StatusOK, orgs) +} + // GetOrg // // @Summary Get an organization @@ -167,3 +187,31 @@ func LookupOrg(c *gin.Context) { c.JSON(http.StatusOK, org) } + +// DeleteOrg +// +// @Summary Delete an organization +// @Description Deletes the given org. Requires admin rights. +// @Router /orgs/{id} [delete] +// @Produce plain +// @Success 204 +// @Tags Orgs +// @Param Authorization header string true "Insert your personal access token" default(Bearer ) +// @Param id path string true "the org's id" +func DeleteOrg(c *gin.Context) { + _store := store.FromContext(c) + + orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64) + if err != nil { + c.String(http.StatusBadRequest, "Error parsing org id. %s", err) + return + } + + err = _store.OrgDelete(orgID) + if err != nil { + handleDBError(c, err) + return + } + + c.Status(http.StatusNoContent) +} diff --git a/server/api/orgs.go b/server/api/orgs.go deleted file mode 100644 index 8c0b1bd75..000000000 --- a/server/api/orgs.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2023 Woodpecker Authors -// -// 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 api - -import ( - "net/http" - "strconv" - - "github.com/gin-gonic/gin" - - "go.woodpecker-ci.org/woodpecker/v2/server/router/middleware/session" - "go.woodpecker-ci.org/woodpecker/v2/server/store" -) - -// GetOrgs -// -// @Summary List organizations -// @Description Returns all registered orgs in the system. Requires admin rights. -// @Router /orgs [get] -// @Produce json -// @Success 200 {array} Org -// @Tags Orgs -// @Param Authorization header string true "Insert your personal access token" default(Bearer ) -// @Param page query int false "for response pagination, page offset number" default(1) -// @Param perPage query int false "for response pagination, max items per page" default(50) -func GetOrgs(c *gin.Context) { - orgs, err := store.FromContext(c).OrgList(session.Pagination(c)) - if err != nil { - c.String(http.StatusInternalServerError, "Error getting user list. %s", err) - return - } - c.JSON(http.StatusOK, orgs) -} - -// DeleteOrg -// -// @Summary Delete an organization -// @Description Deletes the given org. Requires admin rights. -// @Router /orgs/{id} [delete] -// @Produce plain -// @Success 204 -// @Tags Orgs -// @Param Authorization header string true "Insert your personal access token" default(Bearer ) -// @Param id path string true "the org's id" -func DeleteOrg(c *gin.Context) { - _store := store.FromContext(c) - - orgID, err := strconv.ParseInt(c.Param("org_id"), 10, 64) - if err != nil { - c.String(http.StatusBadRequest, "Error parsing org id. %s", err) - return - } - - err = _store.OrgDelete(orgID) - if err != nil { - handleDBError(c, err) - return - } - - c.Status(http.StatusNoContent) -} diff --git a/server/model/forge.go b/server/model/forge.go index fc0976b7d..39a1af292 100644 --- a/server/model/forge.go +++ b/server/model/forge.go @@ -27,12 +27,23 @@ const ( ) type Forge struct { - ID int64 `xorm:"pk autoincr 'id'"` - Type ForgeType `xorm:"VARCHAR(250)"` - URL string `xorm:"VARCHAR(500) 'url'"` - Client string `xorm:"VARCHAR(250)"` - ClientSecret string `xorm:"VARCHAR(250)"` - SkipVerify bool `xorm:"bool"` - OAuthHost string `xorm:"VARCHAR(250) 'oauth_host'"` // public url for oauth if different from url - AdditionalOptions map[string]any `xorm:"json"` + ID int64 `json:"id" xorm:"pk autoincr 'id'"` + Type ForgeType `json:"type" xorm:"VARCHAR(250)"` + URL string `json:"url" xorm:"VARCHAR(500) 'url'"` + Client string `json:"client,omitempty" xorm:"VARCHAR(250)"` + ClientSecret string `json:"-" xorm:"VARCHAR(250)"` // do not expose client secret + SkipVerify bool `json:"skip_verify,omitempty" xorm:"bool"` + OAuthHost string `json:"oauth_host,omitempty" xorm:"VARCHAR(250) 'oauth_host'"` // public url for oauth if different from url + AdditionalOptions map[string]any `json:"additional_options,omitempty" xorm:"json"` +} // @name Forge + +// PublicCopy returns a copy of the forge without sensitive information and technical details. +func (f *Forge) PublicCopy() *Forge { + forge := &Forge{ + ID: f.ID, + Type: f.Type, + URL: f.URL, + } + + return forge } diff --git a/server/router/api.go b/server/router/api.go index 07b3498dd..b433c7c61 100644 --- a/server/router/api.go +++ b/server/router/api.go @@ -202,6 +202,16 @@ func apiRoutes(e *gin.RouterGroup) { agentBase.DELETE("/:agent", api.DeleteAgent) } + apiBase.GET("/forges", api.GetForges) + apiBase.GET("/forges/:forgeId", api.GetForge) + forgeBase := apiBase.Group("/forges") + { + forgeBase.Use(session.MustAdmin()) + forgeBase.POST("", api.PostForge) + forgeBase.PATCH("/:forgeId", api.PatchForge) + forgeBase.DELETE("/:forgeId", api.DeleteForge) + } + apiBase.GET("/signature/public-key", session.MustUser(), api.GetSignaturePublicKey) apiBase.POST("/hook", api.PostHook) diff --git a/server/web/config.go b/server/web/config.go index daa3a2fc5..44c6f1fe5 100644 --- a/server/web/config.go +++ b/server/web/config.go @@ -39,20 +39,11 @@ func Config(c *gin.Context) { csrf, _ = t.Sign(user.Hash) } - // TODO: remove this and use the forge type from the corresponding repo - mainForge, err := server.Config.Services.Manager.ForgeMain() - if err != nil { - log.Error().Err(err).Msg("could not get main forge") - c.AbortWithStatus(http.StatusInternalServerError) - return - } - configData := map[string]any{ "user": user, "csrf": csrf, "version": version.String(), "skip_version_check": server.Config.WebUI.SkipVersionCheck, - "forge": mainForge.Name(), "root_path": server.Config.Server.RootPath, "enable_swagger": server.Config.WebUI.EnableSwagger, } @@ -85,7 +76,6 @@ const configTemplate = ` window.WOODPECKER_USER = {{ json .user }}; window.WOODPECKER_CSRF = "{{ .csrf }}"; window.WOODPECKER_VERSION = "{{ .version }}"; -window.WOODPECKER_FORGE = "{{ .forge }}"; window.WOODPECKER_ROOT_PATH = "{{ .root_path }}"; window.WOODPECKER_ENABLE_SWAGGER = {{ .enable_swagger }}; window.WOODPECKER_SKIP_VERSION_CHECK = {{ .skip_version_check }} diff --git a/web/src/components/atomic/Icon.vue b/web/src/components/atomic/Icon.vue index d7cdd9eb4..732ed5c51 100644 --- a/web/src/components/atomic/Icon.vue +++ b/web/src/components/atomic/Icon.vue @@ -33,7 +33,7 @@ - + @@ -86,7 +86,7 @@ export type IconNames = | 'gitea' | 'gitlab' | 'bitbucket' - | 'bitbucket_dc' + | 'bitbucket-dc' | 'forgejo' | 'question' | 'list' diff --git a/web/src/compositions/useConfig.ts b/web/src/compositions/useConfig.ts index d9cfbf648..3f12e1f92 100644 --- a/web/src/compositions/useConfig.ts +++ b/web/src/compositions/useConfig.ts @@ -6,7 +6,6 @@ declare global { WOODPECKER_VERSION: string | undefined; WOODPECKER_SKIP_VERSION_CHECK: boolean | undefined; WOODPECKER_CSRF: string | undefined; - WOODPECKER_FORGE: 'github' | 'gitlab' | 'gitea' | 'forgejo' | 'bitbucket' | 'bitbucket_dc' | undefined; WOODPECKER_ROOT_PATH: string | undefined; WOODPECKER_ENABLE_SWAGGER: boolean | undefined; } @@ -17,7 +16,6 @@ export default () => ({ version: window.WOODPECKER_VERSION, skipVersionCheck: window.WOODPECKER_SKIP_VERSION_CHECK === true || false, csrf: window.WOODPECKER_CSRF ?? null, - forge: window.WOODPECKER_FORGE ?? null, rootPath: window.WOODPECKER_ROOT_PATH ?? '', enableSwagger: window.WOODPECKER_ENABLE_SWAGGER === true || false, }); diff --git a/web/src/compositions/useForgeStore.ts b/web/src/compositions/useForgeStore.ts new file mode 100644 index 000000000..9069aea92 --- /dev/null +++ b/web/src/compositions/useForgeStore.ts @@ -0,0 +1,30 @@ +import { defineStore } from 'pinia'; +import { computed, reactive, type Ref } from 'vue'; + +import useApiClient from '~/compositions/useApiClient'; +import type { Forge } from '~/lib/api/types'; + +export const useForgeStore = defineStore('forges', () => { + const apiClient = useApiClient(); + + const forges = reactive>(new Map()); + + async function loadForge(forgeId: number): Promise { + const forge = await apiClient.getForge(forgeId); + forges.set(forge.id, forge); + return forge; + } + + async function getForge(forgeId: number): Promise> { + if (!forges.has(forgeId)) { + await loadForge(forgeId); + } + + return computed(() => forges.get(forgeId)); + } + + return { + getForge, + loadForge, + }; +}); diff --git a/web/src/lib/api/index.ts b/web/src/lib/api/index.ts index 08fe6adb9..532aa6661 100644 --- a/web/src/lib/api/index.ts +++ b/web/src/lib/api/index.ts @@ -2,6 +2,7 @@ import ApiClient, { encodeQueryString } from './client'; import type { Agent, Cron, + Forge, Org, OrgPermissions, Pipeline, @@ -284,6 +285,27 @@ export default class WoodpeckerClient extends ApiClient { return this._delete(`/api/agents/${agent.id}`); } + getForges(opts?: PaginationOptions): Promise { + const query = encodeQueryString(opts); + return this._get(`/api/forges?${query}`) as Promise; + } + + getForge(forgeId: Forge['id']): Promise { + return this._get(`/api/forges/${forgeId}`) as Promise; + } + + createForge(forge: Partial): Promise { + return this._post('/api/forges', forge) as Promise; + } + + updateForge(forge: Partial): Promise { + return this._patch(`/api/forges/${forge.id}`, forge); + } + + deleteForge(forge: Forge): Promise { + return this._delete(`/api/forges/${forge.id}`); + } + getQueueInfo(): Promise { return this._get('/api/queue/info') as Promise; } diff --git a/web/src/lib/api/types/forge.ts b/web/src/lib/api/types/forge.ts new file mode 100644 index 000000000..cd1092773 --- /dev/null +++ b/web/src/lib/api/types/forge.ts @@ -0,0 +1,12 @@ +export type ForgeType = 'github' | 'gitlab' | 'gitea' | 'bitbucket' | 'bitbucket-dc' | 'addon'; + +export interface Forge { + id: number; + type: ForgeType; + url: string; + client?: string; + client_secret?: string; + skip_verify?: boolean; + oauth_host?: string; + additional_options?: Record; +} diff --git a/web/src/lib/api/types/index.ts b/web/src/lib/api/types/index.ts index 7918c24d7..1015ebf94 100644 --- a/web/src/lib/api/types/index.ts +++ b/web/src/lib/api/types/index.ts @@ -1,5 +1,6 @@ export * from './agent'; export * from './cron'; +export * from './forge'; export * from './org'; export * from './pipeline'; export * from './pipelineConfig'; diff --git a/web/src/lib/api/types/repo.ts b/web/src/lib/api/types/repo.ts index 7e9acdae4..a6b73576f 100644 --- a/web/src/lib/api/types/repo.ts +++ b/web/src/lib/api/types/repo.ts @@ -9,6 +9,9 @@ export interface Repo { // The id of the repository on the source control management system. forge_remote_id: string; + // The id of the forge that the repository is on. + forge_id: number; + // The source control management being used. // Currently, this is either 'git' or 'hg' (Mercurial). scm: string; diff --git a/web/src/views/repo/RepoWrapper.vue b/web/src/views/repo/RepoWrapper.vue index e7d9eec7f..e50f4fda0 100644 --- a/web/src/views/repo/RepoWrapper.vue +++ b/web/src/views/repo/RepoWrapper.vue @@ -17,7 +17,7 @@ - + (); const pipelines = pipelineStore.getRepoPipelines(repositoryId); provide('repo', repo); provide('repo-permissions', repoPermissions); provide('pipelines', pipelines); +const forge = ref(); +const forgeIcon = computed(() => { + if (forge.value && forge.value.type !== 'addon') { + return forge.value.type; + } + return 'repo'; +}); const showManualPipelinePopup = ref(false); @@ -102,6 +111,10 @@ async function loadRepo() { await repoStore.loadRepo(repositoryId.value); await pipelineStore.loadRepoPipelines(repositoryId.value); + + if (repo.value) { + forge.value = (await forgeStore.getForge(repo.value?.forge_id)).value; + } } onMounted(() => {