You've already forked woodpecker
mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2026-06-03 16:35:37 +02:00
Handle re-created forge repos gracefully (#6370)
Co-authored-by: qwerty287 <80460567+qwerty287@users.noreply.github.com>
This commit is contained in:
@@ -5270,6 +5270,14 @@ const docTemplate = `{
|
||||
"full_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"has_forge_name_conflict": {
|
||||
"description": "HasForgeNameConflict is true if forge returned a repo with same name but different forge remote id",
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_no_forge_repo": {
|
||||
"description": "HasNoForgeRepo is true if repo only exist in the woodpecker store and not at the forge anymore",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
@@ -5381,6 +5389,14 @@ const docTemplate = `{
|
||||
"full_name": {
|
||||
"type": "string"
|
||||
},
|
||||
"has_forge_name_conflict": {
|
||||
"description": "HasForgeNameConflict is true if forge returned a repo with same name but different forge remote id",
|
||||
"type": "boolean"
|
||||
},
|
||||
"has_no_forge_repo": {
|
||||
"description": "HasNoForgeRepo is true if repo only exist in the woodpecker store and not at the forge anymore",
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "integer"
|
||||
},
|
||||
|
||||
+15
-2
@@ -740,9 +740,22 @@ func repairRepo(c *gin.Context, repo *model.Repo, updatePermissions bool) error
|
||||
|
||||
from, err := _forge.Repo(c, repoUser, repo.ForgeRemoteID, repo.Owner, repo.Name)
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("get repo '%s/%s' from forge", repo.Owner, repo.Name)
|
||||
return err
|
||||
// If we have valid ForgeRemoteID and can not find the repo,
|
||||
// we assume the repo was deleted and try to get a new one if it was re-created.
|
||||
if errors.Is(err, forge_types.ErrRepoNotFound) && repo.ForgeRemoteID.IsValid() {
|
||||
from, err = _forge.Repo(c, repoUser, "", repo.Owner, repo.Name)
|
||||
if err == nil {
|
||||
log.Debug().Str("repoFullName", repo.FullName).
|
||||
Str("old ForgeRemoteID", string(repo.ForgeRemoteID)).Str("new ForgeRemoteID", string(from.ForgeRemoteID)).
|
||||
Msgf("RepoRepair detected remote repo ID change and updated it")
|
||||
}
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
log.Error().Err(err).Msgf("get repo '%s/%s' from forge", repo.Owner, repo.Name)
|
||||
return fmt.Errorf("fetching repo from forge: %w", err)
|
||||
}
|
||||
|
||||
from.ForgeID = repo.ForgeID
|
||||
|
||||
if repo.FullName != from.FullName {
|
||||
|
||||
+33
-6
@@ -18,6 +18,7 @@ import (
|
||||
"encoding/base32"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -109,9 +110,13 @@ func GetRepos(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
active := map[model.ForgeRemoteID]*model.Repo{}
|
||||
dbReposMap := map[model.ForgeRemoteID]*model.Repo{}
|
||||
dbStaleReposMap := map[int64]*model.Repo{}
|
||||
dbReposFullNameMap := map[string]*model.Repo{}
|
||||
for _, r := range dbRepos {
|
||||
active[r.ForgeRemoteID] = r
|
||||
dbReposMap[r.ForgeRemoteID] = r
|
||||
dbReposFullNameMap[strings.ToLower(r.FullName)] = r
|
||||
dbStaleReposMap[r.ID] = r
|
||||
}
|
||||
|
||||
_repos, err := utils.Paginate(func(page int) ([]*model.Repo, error) {
|
||||
@@ -131,18 +136,40 @@ func GetRepos(c *gin.Context) {
|
||||
r.ForgeID = user.ForgeID
|
||||
|
||||
if r.Perm.Push && server.Config.Permissions.OwnersAllowlist.IsAllowed(r) {
|
||||
if active[r.ForgeRemoteID] != nil {
|
||||
existingRepo := active[r.ForgeRemoteID]
|
||||
if existingRepo := dbReposMap[r.ForgeRemoteID]; existingRepo != nil {
|
||||
// update repo with forge response
|
||||
existingRepo.Update(r)
|
||||
existingRepo.IsActive = active[r.ForgeRemoteID].IsActive
|
||||
// re-apply active info
|
||||
existingRepo.IsActive = dbReposMap[r.ForgeRemoteID].IsActive
|
||||
// add to final return list
|
||||
repos = append(repos, existingRepo)
|
||||
// not stale, so remove it
|
||||
delete(dbStaleReposMap, existingRepo.ID)
|
||||
} else if r.Perm.Admin {
|
||||
// you must be admin to enable the repo
|
||||
// you must be admin of the remote repo to enable the repo
|
||||
repos = append(repos, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// detect conflicts
|
||||
for _, r := range repos {
|
||||
// calc if we have a remote repo with different remote id but same name as a stored one
|
||||
if existingRepo := dbReposFullNameMap[strings.ToLower(r.FullName)]; existingRepo != nil && existingRepo.ForgeRemoteID != r.ForgeRemoteID {
|
||||
r.ID = existingRepo.ID
|
||||
r.HasForgeNameConflict = true
|
||||
|
||||
// not stale, so remove it
|
||||
delete(dbStaleReposMap, existingRepo.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// return stale repos
|
||||
for _, staleRepo := range dbStaleReposMap {
|
||||
staleRepo.HasNoForgeRepo = true
|
||||
repos = append(repos, staleRepo)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, repos)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -79,6 +79,14 @@ type Repo struct {
|
||||
SecretExtensionEndpoint string `json:"secret_extension_endpoint" xorm:"varchar(500) 'secret_extension_endpoint'"`
|
||||
SecretExtensionNetrc bool `json:"secret_extension_netrc" xorm:"DEFAULT FALSE 'secret_extension_netrc'"`
|
||||
|
||||
// Rest API Only
|
||||
|
||||
// HasForgeNameConflict is true if forge returned a repo with same name but different forge remote id
|
||||
HasForgeNameConflict bool `json:"has_forge_name_conflict,omitempty" xorm:"-"`
|
||||
|
||||
// HasNoForgeRepo is true if repo only exist in the woodpecker store and not at the forge anymore
|
||||
HasNoForgeRepo bool ` json:"has_no_forge_repo,omitempty" xorm:"-"`
|
||||
|
||||
// internal usage
|
||||
|
||||
Perm *Perm `json:"-" xorm:"-"`
|
||||
|
||||
@@ -74,7 +74,12 @@
|
||||
"enable": "Enable",
|
||||
"enabled": "Already enabled",
|
||||
"disabled": "Disabled",
|
||||
"success": "Repository enabled"
|
||||
"success": "Repository enabled",
|
||||
"new_forge_repo": "new repo on forge",
|
||||
"stale_wp_repo": "outdated Woodpecker repo",
|
||||
"conflict": "Conflict",
|
||||
"conflict_desc": "This repository was recreated on the forge with a new ID, but an outdated entry with the same name still exists in Woodpecker. Delete the outdated to enable the new one or Repair the old.",
|
||||
"forge_repo_missing": "Forge repo is missing!"
|
||||
},
|
||||
"open_in_forge": "Open repository in forge",
|
||||
"visibility": {
|
||||
|
||||
@@ -99,6 +99,12 @@ export interface Repo {
|
||||
|
||||
// Whether to include netrc credentials in secret extension requests
|
||||
secret_extension_netrc: boolean;
|
||||
|
||||
// True if forge returned a repo with same name but different forge remote id
|
||||
has_forge_name_conflict?: boolean;
|
||||
|
||||
// True if repo only exist in the woodpecker store and not at the forge anymore
|
||||
has_no_forge_repo?: boolean;
|
||||
}
|
||||
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
+63
-16
@@ -6,23 +6,70 @@
|
||||
|
||||
<div class="space-y-4">
|
||||
<template v-if="repos !== undefined && repos.length > 0">
|
||||
<ListItem
|
||||
v-for="repo in searchedRepos"
|
||||
:key="repo.id"
|
||||
class="items-center"
|
||||
:to="repo.active ? { name: 'repo', params: { repoId: repo.id } } : undefined"
|
||||
>
|
||||
<span class="text-wp-text-100">{{ repo.full_name }}</span>
|
||||
<span v-if="repo.active" class="text-wp-text-alt-100 ml-auto">{{ $t('repo.enable.enabled') }}</span>
|
||||
<div v-else class="ml-auto flex items-center">
|
||||
<Badge v-if="repo.id" class="md:display-unset mr-2 hidden" :value="$t('repo.enable.disabled')" />
|
||||
<Button
|
||||
:text="$t('repo.enable.enable')"
|
||||
:is-loading="isActivatingRepo && repoToActivate?.forge_remote_id === repo.forge_remote_id"
|
||||
@click="activateRepo(repo)"
|
||||
/>
|
||||
<template v-for="repo in searchedRepos" :key="repo.forge_remote_id">
|
||||
<!-- Conflict case: forge repo exists but a stale Woodpecker repo with same name blocks activation -->
|
||||
<div v-if="repo.has_forge_name_conflict" class="space-y-0">
|
||||
<!-- New forge repo (that causes conflict) -->
|
||||
<ListItem class="items-center rounded-b-none! border-b-0!">
|
||||
<div class="flex w-full items-center">
|
||||
<span class="text-wp-text-100">{{ repo.full_name }}</span>
|
||||
<span class="text-wp-text-alt-100 ml-2 text-xs">{{ $t('repo.enable.new_forge_repo') }}</span>
|
||||
<div class="ml-auto flex items-center">
|
||||
<Button :text="$t('repo.enable.conflict')" :title="$t('repo.enable.conflict_desc')" disabled />
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
|
||||
<!-- Old stale Woodpecker repo -->
|
||||
<ListItem
|
||||
:to="{ name: 'repo', params: { repoId: repo.id } }"
|
||||
class="items-center rounded-t-none! border-t-0! opacity-80"
|
||||
>
|
||||
<span class="text-wp-text-alt-100">{{ repo.full_name }}</span>
|
||||
<span class="text-wp-text-alt-100 ml-2 text-xs">{{ $t('repo.enable.stale_wp_repo') }}</span>
|
||||
<div class="ml-auto" @click.prevent.stop>
|
||||
<Button
|
||||
start-icon="toolbox"
|
||||
:text="$t('repo.settings.actions.actions')"
|
||||
:to="{ name: 'repo-settings-actions', params: { repoId: repo.id } }"
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
</div>
|
||||
</ListItem>
|
||||
|
||||
<!-- Conflict case: has no forge counterpart -->
|
||||
<ListItem
|
||||
v-else-if="repo.has_no_forge_repo"
|
||||
:to="{ name: 'repo', params: { repoId: repo.id } }"
|
||||
class="items-center"
|
||||
>
|
||||
<span class="text-wp-text-100">{{ repo.full_name }}</span>
|
||||
<span class="text-wp-text-alt-100 ml-auto">{{ $t('repo.enable.forge_repo_missing') }}</span>
|
||||
</ListItem>
|
||||
|
||||
<!-- Normal case: already active -->
|
||||
<ListItem v-else-if="repo.active" :to="{ name: 'repo', params: { repoId: repo.id } }" class="items-center">
|
||||
<span class="text-wp-text-100">{{ repo.full_name }}</span>
|
||||
<span class="text-wp-text-alt-100 ml-auto">{{ $t('repo.enable.enabled') }}</span>
|
||||
</ListItem>
|
||||
|
||||
<!-- Normal case: can be enabled -->
|
||||
<ListItem
|
||||
v-else
|
||||
class="items-center"
|
||||
:to="repo.id ? { name: 'repo', params: { repoId: repo.id } } : undefined"
|
||||
>
|
||||
<span class="text-wp-text-100">{{ repo.full_name }}</span>
|
||||
<div class="ml-auto flex items-center">
|
||||
<Badge v-if="repo.id" class="md:display-unset mr-2 hidden" :value="$t('repo.enable.disabled')" />
|
||||
<Button
|
||||
:text="$t('repo.enable.enable')"
|
||||
:is-loading="isActivatingRepo && repoToActivate?.forge_remote_id === repo.forge_remote_id"
|
||||
@click="activateRepo(repo)"
|
||||
/>
|
||||
</div>
|
||||
</ListItem>
|
||||
</template>
|
||||
</template>
|
||||
<div v-else-if="loading" class="text-wp-text-100 flex justify-center">
|
||||
<Icon name="spinner" />
|
||||
|
||||
Reference in New Issue
Block a user