feat(sync): remove target avatar when source has none
Add removeProjectAvatar and removeGroupAvatar methods to keep avatars fully in sync by clearing the target avatar when the source has none.
This commit is contained in:
@@ -1,5 +1,13 @@
|
||||
# Changelog
|
||||
|
||||
## 2026-02-28 - 2.9.0 - feat(sync)
|
||||
remove target avatar when source has none to keep avatars fully in sync
|
||||
|
||||
- Add removeProjectAvatar and removeGroupAvatar methods for GitLab and Gitea APIs
|
||||
- In syncProjectMetadata, remove target project avatar when source has no avatar and no group fallback applies
|
||||
- When useGroupAvatarsForProjects is enabled but the group also has no avatar, remove the target avatar
|
||||
- In syncGroupMetadata, remove target group avatar when source group has no avatar
|
||||
|
||||
## 2026-02-28 - 2.8.0 - feat(sync)
|
||||
add sync subsystem: SyncManager, OpsServer sync handlers, Sync UI and state, provider groupFilter support, and realtime sync log streaming via TypedSocket
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@serve.zone/gitops",
|
||||
"version": "2.8.0",
|
||||
"version": "2.9.0",
|
||||
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
|
||||
"main": "mod.ts",
|
||||
"type": "module",
|
||||
|
||||
@@ -1139,6 +1139,7 @@ export class SyncManager {
|
||||
} else if (config.useGroupAvatarsForProjects) {
|
||||
// Project has no avatar — inherit from parent group
|
||||
const groupPath = sourceFullPath.substring(0, sourceFullPath.lastIndexOf('/'));
|
||||
let groupAvatarApplied = false;
|
||||
if (groupPath) {
|
||||
try {
|
||||
const sourceGroup = await this.fetchGroupRaw(sourceConn, groupPath);
|
||||
@@ -1147,6 +1148,7 @@ export class SyncManager {
|
||||
if (groupMeta.avatarUrl) {
|
||||
logger.syncLog('info', `Applying group avatar to ${targetFullPath}`, 'api');
|
||||
await this.syncProjectAvatar(sourceConn, targetConn, sourceFullPath, targetFullPath, groupMeta.avatarUrl, targetProject);
|
||||
groupAvatarApplied = true;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -1154,6 +1156,15 @@ export class SyncManager {
|
||||
logger.syncLog('warn', `Group avatar sync failed for ${targetFullPath}: ${errMsg}`, 'api');
|
||||
}
|
||||
}
|
||||
// If group also has no avatar, remove target avatar
|
||||
if (!groupAvatarApplied && targetMeta.avatarUrl) {
|
||||
await this.removeProjectAvatar(targetConn, targetFullPath, targetProject);
|
||||
}
|
||||
} else {
|
||||
// No source avatar, no group fallback — remove target avatar if present
|
||||
if (targetMeta.avatarUrl) {
|
||||
await this.removeProjectAvatar(targetConn, targetFullPath, targetProject);
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
@@ -1199,6 +1210,8 @@ export class SyncManager {
|
||||
// Sync avatar
|
||||
if (sourceMeta.avatarUrl) {
|
||||
await this.syncGroupAvatar(sourceConn, targetConn, sourceGroupPath, targetGroupPath, sourceMeta.avatarUrl, targetGroup);
|
||||
} else if (targetMeta.avatarUrl) {
|
||||
await this.removeGroupAvatar(targetConn, targetGroupPath, targetGroup);
|
||||
}
|
||||
} catch (err) {
|
||||
const errMsg = err instanceof Error ? err.message : String(err);
|
||||
@@ -1383,6 +1396,25 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async removeProjectAvatar(
|
||||
targetConn: interfaces.data.IProviderConnection,
|
||||
targetFullPath: string,
|
||||
targetRawProject: any,
|
||||
): Promise<void> {
|
||||
logger.syncLog('info', `Removing avatar from ${targetFullPath}`, 'api');
|
||||
if (targetConn.providerType === 'gitlab') {
|
||||
await this.rawApiCall(targetConn, 'PUT', `/api/v4/projects/${targetRawProject.id}`, {
|
||||
avatar: '',
|
||||
});
|
||||
} else {
|
||||
const segments = targetFullPath.split('/');
|
||||
const repo = segments.pop()!;
|
||||
const owner = segments[0] || '';
|
||||
await this.rawApiCall(targetConn, 'DELETE',
|
||||
`/api/v1/repos/${encodeURIComponent(owner)}/${encodeURIComponent(repo)}/avatar`);
|
||||
}
|
||||
}
|
||||
|
||||
private async syncGroupAvatar(
|
||||
sourceConn: interfaces.data.IProviderConnection,
|
||||
targetConn: interfaces.data.IProviderConnection,
|
||||
@@ -1422,6 +1454,23 @@ export class SyncManager {
|
||||
}
|
||||
}
|
||||
|
||||
private async removeGroupAvatar(
|
||||
targetConn: interfaces.data.IProviderConnection,
|
||||
targetGroupPath: string,
|
||||
targetRawGroup: any,
|
||||
): Promise<void> {
|
||||
logger.syncLog('info', `Removing avatar from group ${targetGroupPath}`, 'api');
|
||||
if (targetConn.providerType === 'gitlab') {
|
||||
await this.rawApiCall(targetConn, 'PUT', `/api/v4/groups/${targetRawGroup.id}`, {
|
||||
avatar: '',
|
||||
});
|
||||
} else {
|
||||
const orgName = targetGroupPath.split('/')[0] || targetGroupPath;
|
||||
await this.rawApiCall(targetConn, 'DELETE',
|
||||
`/api/v1/orgs/${encodeURIComponent(orgName)}/avatar`);
|
||||
}
|
||||
}
|
||||
|
||||
private uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||
let binary = '';
|
||||
for (let i = 0; i < bytes.length; i++) {
|
||||
|
||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user