Skip to content

Commit 959a19f

Browse files
feat: add release plan collaboration and versioned diffs
Signed-off-by: huanghongbo-hhb <huanghongbo@koderover.com>
1 parent f918802 commit 959a19f

16 files changed

Lines changed: 2751 additions & 86 deletions

File tree

community/rfc/2026-05-14-release-plan-collaboration-and-versioned-diff.md

Lines changed: 411 additions & 0 deletions
Large diffs are not rendered by default.

pkg/microservice/aslan/core/common/repository/models/release_plan.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
type ReleasePlan struct {
2626
ID primitive.ObjectID `bson:"_id,omitempty" yaml:"-" json:"id"`
2727
Index int64 `bson:"index" yaml:"index" json:"index"`
28+
Version int64 `bson:"version" yaml:"version" json:"version"`
2829
Name string `bson:"name" yaml:"name" json:"name"`
2930
Manager string `bson:"manager" yaml:"manager" json:"manager"`
3031
// ManagerID is the user id of the manager
@@ -120,19 +121,41 @@ type WorkflowReleaseJobSpec struct {
120121
}
121122

122123
type ReleasePlanLog struct {
123-
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
124-
PlanID string `bson:"plan_id" json:"plan_id"`
125-
Username string `bson:"username" json:"username"`
126-
Account string `bson:"account" json:"account"`
127-
Verb string `bson:"verb" json:"verb"`
128-
TargetName string `bson:"target_name" json:"target_name"`
129-
TargetType string `bson:"target_type" json:"target_type"`
130-
Before interface{} `bson:"before" json:"before"`
131-
After interface{} `bson:"after" json:"after"`
132-
Detail string `bson:"detail" json:"detail"`
133-
CreatedAt int64 `bson:"created_at" json:"created_at"`
124+
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
125+
PlanID string `bson:"plan_id" json:"plan_id"`
126+
SessionID string `bson:"session_id,omitempty" json:"session_id,omitempty"`
127+
Username string `bson:"username" json:"username"`
128+
Account string `bson:"account" json:"account"`
129+
Verb string `bson:"verb" json:"verb"`
130+
TargetName string `bson:"target_name" json:"target_name"`
131+
TargetType string `bson:"target_type" json:"target_type"`
132+
Before interface{} `bson:"before" json:"before"`
133+
After interface{} `bson:"after" json:"after"`
134+
Detail string `bson:"detail" json:"detail"`
135+
FromVersion int64 `bson:"from_version,omitempty" json:"from_version,omitempty"`
136+
ToVersion int64 `bson:"to_version,omitempty" json:"to_version,omitempty"`
137+
CreatedAt int64 `bson:"created_at" json:"created_at"`
134138
}
135139

136140
func (ReleasePlanLog) TableName() string {
137141
return "release_plan_log"
138142
}
143+
144+
type ReleasePlanVersion struct {
145+
ID primitive.ObjectID `bson:"_id,omitempty" json:"id"`
146+
PlanID string `bson:"plan_id" json:"plan_id"`
147+
BaseVersion int64 `bson:"base_version,omitempty" json:"base_version,omitempty"`
148+
Version int64 `bson:"version" json:"version"`
149+
Operator string `bson:"operator" json:"operator"`
150+
Account string `bson:"account" json:"account"`
151+
SectionKey string `bson:"section_key,omitempty" json:"section_key,omitempty"`
152+
SectionName string `bson:"section_name,omitempty" json:"section_name,omitempty"`
153+
Verb string `bson:"verb,omitempty" json:"verb,omitempty"`
154+
BaseSnapshot interface{} `bson:"base_snapshot,omitempty" json:"base_snapshot,omitempty"`
155+
Snapshot interface{} `bson:"snapshot" json:"snapshot"`
156+
CreatedAt int64 `bson:"created_at" json:"created_at"`
157+
}
158+
159+
func (ReleasePlanVersion) TableName() string {
160+
return "release_plan_version"
161+
}

pkg/microservice/aslan/core/common/repository/mongodb/release_plan.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ func (c *ReleasePlanColl) EnsureIndex(ctx context.Context) error {
7979
Keys: bson.M{"update_time": 1},
8080
Options: options.Index().SetUnique(false),
8181
},
82+
{
83+
Keys: bson.M{"version": 1},
84+
Options: options.Index().SetUnique(false),
85+
},
8286
}
8387

8488
_, err := c.Indexes().CreateMany(ctx, mod, mongotool.CreateIndexOptions(ctx))
@@ -121,6 +125,35 @@ func (c *ReleasePlanColl) UpdateByID(ctx context.Context, idString string, args
121125
return err
122126
}
123127

128+
func (c *ReleasePlanColl) UpdateVersionByID(ctx context.Context, idString string, version int64) error {
129+
id, err := primitive.ObjectIDFromHex(idString)
130+
if err != nil {
131+
return fmt.Errorf("invalid id")
132+
}
133+
134+
query := bson.M{"_id": id}
135+
change := bson.M{"$set": bson.M{"version": version}}
136+
_, err = c.UpdateOne(ctx, query, change)
137+
return err
138+
}
139+
140+
func (c *ReleasePlanColl) IncrementVersionByID(ctx context.Context, idString string) (int64, error) {
141+
id, err := primitive.ObjectIDFromHex(idString)
142+
if err != nil {
143+
return 0, fmt.Errorf("invalid id")
144+
}
145+
146+
query := bson.M{"_id": id}
147+
change := bson.M{"$inc": bson.M{"version": 1}}
148+
opts := options.FindOneAndUpdate().SetReturnDocument(options.After)
149+
150+
result := new(models.ReleasePlan)
151+
if err := c.FindOneAndUpdate(ctx, query, change, opts).Decode(result); err != nil {
152+
return 0, err
153+
}
154+
return result.Version, nil
155+
}
156+
124157
func (c *ReleasePlanColl) DeleteByID(ctx context.Context, idString string) error {
125158
id, err := primitive.ObjectIDFromHex(idString)
126159
if err != nil {

pkg/microservice/aslan/core/common/repository/mongodb/release_plan_log.go

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,17 @@ func (c *ReleasePlanLogColl) GetCollectionName() string {
4848
}
4949

5050
func (c *ReleasePlanLogColl) EnsureIndex(ctx context.Context) error {
51-
return nil
51+
mod := []mongo.IndexModel{
52+
{
53+
Keys: bson.D{{Key: "plan_id", Value: 1}, {Key: "created_at", Value: -1}},
54+
},
55+
{
56+
Keys: bson.D{{Key: "session_id", Value: 1}},
57+
},
58+
}
59+
60+
_, err := c.Indexes().CreateMany(ctx, mod, mongotool.CreateIndexOptions(ctx))
61+
return err
5262
}
5363

5464
func (c *ReleasePlanLogColl) Create(args *models.ReleasePlanLog) error {
@@ -76,7 +86,7 @@ func (c *ReleasePlanLogColl) ListByOptions(opt *ListReleasePlanLogOption) ([]*mo
7686
ctx := context.Background()
7787
opts := options.Find()
7888
if opt.IsSort {
79-
opts.SetSort(bson.D{{"create_time", -1}})
89+
opts.SetSort(bson.D{{"created_at", -1}})
8090
}
8191
if opt.PlanID != "" {
8292
query["plan_id"] = opt.PlanID
@@ -94,3 +104,42 @@ func (c *ReleasePlanLogColl) ListByOptions(opt *ListReleasePlanLogOption) ([]*mo
94104

95105
return resp, nil
96106
}
107+
108+
func (c *ReleasePlanLogColl) FillVersionsBySessionID(planID, sessionID string, fromVersion, toVersion int64) error {
109+
if sessionID == "" {
110+
return errors.New("empty session id")
111+
}
112+
113+
query := bson.M{
114+
"plan_id": planID,
115+
"session_id": sessionID,
116+
"$or": []bson.M{
117+
{"to_version": bson.M{"$exists": false}},
118+
{"to_version": 0},
119+
},
120+
}
121+
change := bson.M{"$set": bson.M{
122+
"from_version": fromVersion,
123+
"to_version": toVersion,
124+
}}
125+
126+
_, err := c.UpdateMany(context.Background(), query, change)
127+
return err
128+
}
129+
130+
func (c *ReleasePlanLogColl) CountPendingBySessionID(planID, sessionID string) (int64, error) {
131+
if sessionID == "" {
132+
return 0, errors.New("empty session id")
133+
}
134+
135+
query := bson.M{
136+
"plan_id": planID,
137+
"session_id": sessionID,
138+
"$or": []bson.M{
139+
{"to_version": bson.M{"$exists": false}},
140+
{"to_version": 0},
141+
},
142+
}
143+
144+
return c.CountDocuments(context.Background(), query)
145+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
/*
2+
* Copyright 2026 The KodeRover Authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package mongodb
18+
19+
import (
20+
"context"
21+
22+
"github.com/pkg/errors"
23+
"go.mongodb.org/mongo-driver/bson"
24+
"go.mongodb.org/mongo-driver/mongo"
25+
"go.mongodb.org/mongo-driver/mongo/options"
26+
27+
"github.com/koderover/zadig/v2/pkg/microservice/aslan/config"
28+
"github.com/koderover/zadig/v2/pkg/microservice/aslan/core/common/repository/models"
29+
mongotool "github.com/koderover/zadig/v2/pkg/tool/mongo"
30+
)
31+
32+
type ReleasePlanVersionColl struct {
33+
*mongo.Collection
34+
35+
coll string
36+
}
37+
38+
func NewReleasePlanVersionColl() *ReleasePlanVersionColl {
39+
name := models.ReleasePlanVersion{}.TableName()
40+
return &ReleasePlanVersionColl{
41+
Collection: mongotool.Database(config.MongoDatabase()).Collection(name),
42+
coll: name,
43+
}
44+
}
45+
46+
func (c *ReleasePlanVersionColl) GetCollectionName() string {
47+
return c.coll
48+
}
49+
50+
func (c *ReleasePlanVersionColl) EnsureIndex(ctx context.Context) error {
51+
mod := []mongo.IndexModel{
52+
{
53+
Keys: bson.D{{Key: "plan_id", Value: 1}, {Key: "version", Value: 1}},
54+
Options: options.Index().SetUnique(true),
55+
},
56+
{
57+
Keys: bson.D{{Key: "plan_id", Value: 1}, {Key: "created_at", Value: -1}},
58+
},
59+
}
60+
61+
_, err := c.Indexes().CreateMany(ctx, mod, mongotool.CreateIndexOptions(ctx))
62+
return err
63+
}
64+
65+
func (c *ReleasePlanVersionColl) Create(args *models.ReleasePlanVersion) error {
66+
if args == nil {
67+
return errors.New("nil ReleasePlanVersion")
68+
}
69+
70+
_, err := c.InsertOne(context.Background(), args)
71+
return err
72+
}
73+
74+
func (c *ReleasePlanVersionColl) Get(planID string, version int64) (*models.ReleasePlanVersion, error) {
75+
resp := new(models.ReleasePlanVersion)
76+
err := c.FindOne(context.Background(), bson.M{
77+
"plan_id": planID,
78+
"version": version,
79+
}).Decode(resp)
80+
return resp, err
81+
}
82+
83+
func (c *ReleasePlanVersionColl) GetLatest(planID string) (*models.ReleasePlanVersion, error) {
84+
resp := new(models.ReleasePlanVersion)
85+
err := c.FindOne(context.Background(), bson.M{
86+
"plan_id": planID,
87+
}, options.FindOne().SetSort(bson.D{{Key: "version", Value: -1}})).Decode(resp)
88+
return resp, err
89+
}

pkg/microservice/aslan/core/release_plan/handler/release_plan.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package handler
1919
import (
2020
"fmt"
2121
"strings"
22+
"strconv"
2223

2324
"github.com/gin-gonic/gin"
2425

@@ -78,6 +79,56 @@ func GetReleasePlanLogs(c *gin.Context) {
7879
ctx.Resp, ctx.RespErr = service.GetReleasePlanLogs(c.Param("id"))
7980
}
8081

82+
func GetReleasePlanCollaborationEditors(c *gin.Context) {
83+
ctx, err := internalhandler.NewContextWithAuthorization(c)
84+
defer func() { internalhandler.JSONResponse(c, ctx) }()
85+
86+
if err != nil {
87+
ctx.Logger.Errorf("failed to generate authorization info for user: %s, error: %s", ctx.UserID, err)
88+
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
89+
ctx.UnAuthorized = true
90+
return
91+
}
92+
93+
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.ReleasePlan.View {
94+
ctx.UnAuthorized = true
95+
return
96+
}
97+
98+
err = commonutil.CheckZadigEnterpriseLicense()
99+
if err != nil {
100+
ctx.RespErr = err
101+
return
102+
}
103+
104+
ctx.Resp, ctx.RespErr = service.GetReleasePlanCollaborationEditors(c.Param("id"))
105+
}
106+
107+
func ReleasePlanCollaborationWS(c *gin.Context) {
108+
ctx, err := internalhandler.NewContextWithAuthorization(c)
109+
defer func() { internalhandler.JSONResponse(c, ctx) }()
110+
111+
if err != nil {
112+
ctx.Logger.Errorf("failed to generate authorization info for user: %s, error: %s", ctx.UserID, err)
113+
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
114+
ctx.UnAuthorized = true
115+
return
116+
}
117+
118+
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.ReleasePlan.View {
119+
ctx.UnAuthorized = true
120+
return
121+
}
122+
123+
err = commonutil.CheckZadigEnterpriseLicense()
124+
if err != nil {
125+
ctx.RespErr = err
126+
return
127+
}
128+
129+
ctx.RespErr = service.OpenReleasePlanCollaborationWS(c, ctx, c.Param("id"))
130+
}
131+
81132
func CreateReleasePlan(c *gin.Context) {
82133
ctx, err := internalhandler.NewContextWithAuthorization(c)
83134
defer func() { internalhandler.JSONResponse(c, ctx) }()
@@ -189,6 +240,36 @@ func UpdateReleasePlan(c *gin.Context) {
189240
ctx.RespErr = service.UpdateReleasePlan(ctx, c.Param("id"), req)
190241
}
191242

243+
func GetReleasePlanVersionDiff(c *gin.Context) {
244+
ctx, err := internalhandler.NewContextWithAuthorization(c)
245+
defer func() { internalhandler.JSONResponse(c, ctx) }()
246+
247+
if err != nil {
248+
ctx.RespErr = fmt.Errorf("authorization Info Generation failed: err %s", err)
249+
ctx.UnAuthorized = true
250+
return
251+
}
252+
253+
if !ctx.Resources.IsSystemAdmin && !ctx.Resources.SystemActions.ReleasePlan.View {
254+
ctx.UnAuthorized = true
255+
return
256+
}
257+
258+
err = commonutil.CheckZadigEnterpriseLicense()
259+
if err != nil {
260+
ctx.RespErr = err
261+
return
262+
}
263+
264+
version, err := strconv.ParseInt(c.Param("version"), 10, 64)
265+
if err != nil {
266+
ctx.RespErr = e.ErrInvalidParam.AddDesc(err.Error())
267+
return
268+
}
269+
270+
ctx.Resp, ctx.RespErr = service.GetReleasePlanVersionDiff(c.Param("id"), version)
271+
}
272+
192273
func GetReleasePlanJobDetail(c *gin.Context) {
193274
ctx, err := internalhandler.NewContextWithAuthorization(c)
194275
defer func() { internalhandler.JSONResponse(c, ctx) }()

pkg/microservice/aslan/core/release_plan/handler/router.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,11 @@ func (*Router) Inject(router *gin.RouterGroup) {
2828
v1.POST("/:id/copy", CopyReleasePlan)
2929
v1.GET("/:id", GetReleasePlan)
3030
v1.GET("/:id/logs", GetReleasePlanLogs)
31+
v1.GET("/:id/collaboration/editors", GetReleasePlanCollaborationEditors)
32+
v1.GET("/:id/collaboration/ws", ReleasePlanCollaborationWS)
3133
v1.PUT("/:id", UpdateReleasePlan)
34+
v1.POST("/:id/versions/commit", CommitReleasePlanVersion)
35+
v1.GET("/:id/versions/:fromVersion/:toVersion/diff", GetReleasePlanVersionDiff)
3236
v1.GET("/:id/job/:jobID", GetReleasePlanJobDetail)
3337
v1.DELETE("/:id", DeleteReleasePlan)
3438

0 commit comments

Comments
 (0)