Add api for forges (#3733)

This commit is contained in:
Anbraten 2024-06-20 13:08:54 +02:00 committed by GitHub
parent eadead6c07
commit fbb96ff8f5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 611 additions and 98 deletions

View file

@ -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": {

208
server/api/forge.go Normal file
View file

@ -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 <personal access token>)
// @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 <personal access token>)
// @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 <personal access token>)
// @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 <personal access token>)
// @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 <personal access token>)
// @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)
}

View file

@ -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 <personal access token>)
// @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 <personal access token>)
// @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)
}

View file

@ -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 <personal access token>)
// @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 <personal access token>)
// @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)
}

View file

@ -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
}

View file

@ -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)

View file

@ -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 }}

View file

@ -33,7 +33,7 @@
<i-simple-icons-gitea v-else-if="name === 'gitea'" class="h-8 w-8" />
<i-simple-icons-forgejo v-else-if="name === 'forgejo'" class="h-8 w-8" />
<i-ph-gitlab-logo-simple-fill v-else-if="name === 'gitlab'" class="h-8 w-8" />
<i-mdi-bitbucket v-else-if="name === 'bitbucket' || name === 'bitbucket_dc'" class="h-8 w-8" />
<i-mdi-bitbucket v-else-if="name === 'bitbucket' || name === 'bitbucket-dc'" class="h-8 w-8" />
<i-vaadin-question-circle-o v-else-if="name === 'question'" class="h-6 w-6" />
<i-ic-twotone-add v-else-if="name === 'plus'" class="h-6 w-6" />
<i-mdi-format-list-bulleted v-else-if="name === 'list'" class="h-6 w-6" />
@ -86,7 +86,7 @@ export type IconNames =
| 'gitea'
| 'gitlab'
| 'bitbucket'
| 'bitbucket_dc'
| 'bitbucket-dc'
| 'forgejo'
| 'question'
| 'list'

View file

@ -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,
});

View file

@ -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<Map<number, Forge>>(new Map());
async function loadForge(forgeId: number): Promise<Forge> {
const forge = await apiClient.getForge(forgeId);
forges.set(forge.id, forge);
return forge;
}
async function getForge(forgeId: number): Promise<Ref<Forge | undefined>> {
if (!forges.has(forgeId)) {
await loadForge(forgeId);
}
return computed(() => forges.get(forgeId));
}
return {
getForge,
loadForge,
};
});

View file

@ -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<Forge[] | null> {
const query = encodeQueryString(opts);
return this._get(`/api/forges?${query}`) as Promise<Forge[] | null>;
}
getForge(forgeId: Forge['id']): Promise<Forge> {
return this._get(`/api/forges/${forgeId}`) as Promise<Forge>;
}
createForge(forge: Partial<Forge>): Promise<Forge> {
return this._post('/api/forges', forge) as Promise<Forge>;
}
updateForge(forge: Partial<Forge>): Promise<unknown> {
return this._patch(`/api/forges/${forge.id}`, forge);
}
deleteForge(forge: Forge): Promise<unknown> {
return this._delete(`/api/forges/${forge.id}`);
}
getQueueInfo(): Promise<QueueInfo> {
return this._get('/api/queue/info') as Promise<QueueInfo>;
}

View file

@ -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<string, unknown>;
}

View file

@ -1,5 +1,6 @@
export * from './agent';
export * from './cron';
export * from './forge';
export * from './org';
export * from './pipeline';
export * from './pipelineConfig';

View file

@ -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;

View file

@ -17,7 +17,7 @@
<a v-if="badgeUrl" :href="badgeUrl" target="_blank">
<img :src="badgeUrl" />
</a>
<IconButton :href="repo.forge_url" :title="$t('repo.open_in_forge')" :icon="forge ?? 'repo'" class="forge" />
<IconButton :href="repo.forge_url" :title="$t('repo.open_in_forge')" :icon="forgeIcon" class="forge" />
<IconButton
v-if="repoPermissions.admin"
:to="{ name: 'repo-settings' }"
@ -49,6 +49,7 @@ import { computed, onMounted, provide, ref, toRef, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import type { IconNames } from '~/components/atomic/Icon.vue';
import IconButton from '~/components/atomic/IconButton.vue';
import ManualPipelinePopup from '~/components/layout/popups/ManualPipelinePopup.vue';
import Scaffold from '~/components/layout/scaffold/Scaffold.vue';
@ -56,8 +57,9 @@ import Tab from '~/components/layout/scaffold/Tab.vue';
import useApiClient from '~/compositions/useApiClient';
import useAuthentication from '~/compositions/useAuthentication';
import useConfig from '~/compositions/useConfig';
import { useForgeStore } from '~/compositions/useForgeStore';
import useNotifications from '~/compositions/useNotifications';
import type { RepoPermissions } from '~/lib/api/types';
import type { Forge, RepoPermissions } from '~/lib/api/types';
import { usePipelineStore } from '~/store/pipelines';
import { useRepoStore } from '~/store/repos';
@ -76,14 +78,21 @@ const route = useRoute();
const router = useRouter();
const i18n = useI18n();
const config = useConfig();
const forgeStore = useForgeStore();
const { forge } = useConfig(); // TODO: remove this and use the forge type from the corresponding repo
const repo = repoStore.getRepo(repositoryId);
const repoPermissions = ref<RepoPermissions>();
const pipelines = pipelineStore.getRepoPipelines(repositoryId);
provide('repo', repo);
provide('repo-permissions', repoPermissions);
provide('pipelines', pipelines);
const forge = ref<Forge>();
const forgeIcon = computed<IconNames>(() => {
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(() => {