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
|
# 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)
|
## 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
|
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",
|
"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",
|
"description": "GitOps management app for Gitea and GitLab - manage secrets, browse projects, view CI pipelines, and stream build logs",
|
||||||
"main": "mod.ts",
|
"main": "mod.ts",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -1139,6 +1139,7 @@ export class SyncManager {
|
|||||||
} else if (config.useGroupAvatarsForProjects) {
|
} else if (config.useGroupAvatarsForProjects) {
|
||||||
// Project has no avatar — inherit from parent group
|
// Project has no avatar — inherit from parent group
|
||||||
const groupPath = sourceFullPath.substring(0, sourceFullPath.lastIndexOf('/'));
|
const groupPath = sourceFullPath.substring(0, sourceFullPath.lastIndexOf('/'));
|
||||||
|
let groupAvatarApplied = false;
|
||||||
if (groupPath) {
|
if (groupPath) {
|
||||||
try {
|
try {
|
||||||
const sourceGroup = await this.fetchGroupRaw(sourceConn, groupPath);
|
const sourceGroup = await this.fetchGroupRaw(sourceConn, groupPath);
|
||||||
@@ -1147,6 +1148,7 @@ export class SyncManager {
|
|||||||
if (groupMeta.avatarUrl) {
|
if (groupMeta.avatarUrl) {
|
||||||
logger.syncLog('info', `Applying group avatar to ${targetFullPath}`, 'api');
|
logger.syncLog('info', `Applying group avatar to ${targetFullPath}`, 'api');
|
||||||
await this.syncProjectAvatar(sourceConn, targetConn, sourceFullPath, targetFullPath, groupMeta.avatarUrl, targetProject);
|
await this.syncProjectAvatar(sourceConn, targetConn, sourceFullPath, targetFullPath, groupMeta.avatarUrl, targetProject);
|
||||||
|
groupAvatarApplied = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -1154,6 +1156,15 @@ export class SyncManager {
|
|||||||
logger.syncLog('warn', `Group avatar sync failed for ${targetFullPath}: ${errMsg}`, 'api');
|
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) {
|
} catch (err) {
|
||||||
const errMsg = err instanceof Error ? err.message : String(err);
|
const errMsg = err instanceof Error ? err.message : String(err);
|
||||||
@@ -1199,6 +1210,8 @@ export class SyncManager {
|
|||||||
// Sync avatar
|
// Sync avatar
|
||||||
if (sourceMeta.avatarUrl) {
|
if (sourceMeta.avatarUrl) {
|
||||||
await this.syncGroupAvatar(sourceConn, targetConn, sourceGroupPath, targetGroupPath, sourceMeta.avatarUrl, targetGroup);
|
await this.syncGroupAvatar(sourceConn, targetConn, sourceGroupPath, targetGroupPath, sourceMeta.avatarUrl, targetGroup);
|
||||||
|
} else if (targetMeta.avatarUrl) {
|
||||||
|
await this.removeGroupAvatar(targetConn, targetGroupPath, targetGroup);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errMsg = err instanceof Error ? err.message : String(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(
|
private async syncGroupAvatar(
|
||||||
sourceConn: interfaces.data.IProviderConnection,
|
sourceConn: interfaces.data.IProviderConnection,
|
||||||
targetConn: 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 {
|
private uint8ArrayToBase64(bytes: Uint8Array): string {
|
||||||
let binary = '';
|
let binary = '';
|
||||||
for (let i = 0; i < bytes.length; i++) {
|
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