diff --git a/docs/references/deploy.yml.md b/docs/references/deploy.yml.md index 1344603f..44b65e88 100644 --- a/docs/references/deploy.yml.md +++ b/docs/references/deploy.yml.md @@ -18,6 +18,7 @@ Field |Type |Required |Description `payload` |*object* or *string* |`false` |This field is JSON payload with extra information about the deployment. `production_environment` |*boolean* |`false` |This field specifies whether this runtime environment is production or not. `deployable_ref` |*string* |`false` |This field specifies which the ref(branch, SHA, tag) is deployable or not. It supports the regular expression, [re2](https://github.com/google/re2/wiki/Syntax) by Google, to match the ref. +`auto_deploy_on` |*string* |`false` |This field controls auto-deployment behaviour given a ref(branch, SHA, tag). It supports the regular expression, [re2](https://github.com/google/re2/wiki/Syntax) by Google, to match the ref. `review` |*[Review](#review)* |`false` |This field configures review. ## Review diff --git a/ent/client.go b/ent/client.go index 136a4380..2c020ddd 100644 --- a/ent/client.go +++ b/ent/client.go @@ -1446,6 +1446,22 @@ func (c *RepoClient) QueryDeploymentStatistics(r *Repo) *DeploymentStatisticsQue return query } +// QueryOwner queries the owner edge of a Repo. +func (c *RepoClient) QueryOwner(r *Repo) *UserQuery { + query := &UserQuery{config: c.config} + query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) { + id := r.ID + step := sqlgraph.NewStep( + sqlgraph.From(repo.Table, repo.FieldID, id), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, repo.OwnerTable, repo.OwnerColumn), + ) + fromV = sqlgraph.Neighbors(r.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *RepoClient) Hooks() []Hook { return c.hooks.Repo @@ -1754,6 +1770,22 @@ func (c *UserClient) QueryLocks(u *User) *LockQuery { return query } +// QueryRepo queries the repo edge of a User. +func (c *UserClient) QueryRepo(u *User) *RepoQuery { + query := &RepoQuery{config: c.config} + query.path = func(ctx context.Context) (fromV *sql.Selector, _ error) { + id := u.ID + step := sqlgraph.NewStep( + sqlgraph.From(user.Table, user.FieldID, id), + sqlgraph.To(repo.Table, repo.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, user.RepoTable, user.RepoColumn), + ) + fromV = sqlgraph.Neighbors(u.driver.Dialect(), step) + return fromV, nil + } + return query +} + // Hooks returns the client hooks. func (c *UserClient) Hooks() []Hook { return c.hooks.User diff --git a/ent/migrate/schema.go b/ent/migrate/schema.go index b889b2f7..1b2263af 100644 --- a/ent/migrate/schema.go +++ b/ent/migrate/schema.go @@ -339,12 +339,21 @@ var ( {Name: "created_at", Type: field.TypeTime}, {Name: "updated_at", Type: field.TypeTime}, {Name: "latest_deployed_at", Type: field.TypeTime, Nullable: true}, + {Name: "owner_id", Type: field.TypeInt64, Nullable: true}, } // ReposTable holds the schema information for the "repos" table. ReposTable = &schema.Table{ Name: "repos", Columns: ReposColumns, PrimaryKey: []*schema.Column{ReposColumns[0]}, + ForeignKeys: []*schema.ForeignKey{ + { + Symbol: "repos_users_repo", + Columns: []*schema.Column{ReposColumns[10]}, + RefColumns: []*schema.Column{UsersColumns[0]}, + OnDelete: schema.SetNull, + }, + }, Indexes: []*schema.Index{ { Name: "repo_namespace_name", @@ -443,6 +452,7 @@ func init() { NotificationRecordsTable.ForeignKeys[0].RefTable = EventsTable PermsTable.ForeignKeys[0].RefTable = ReposTable PermsTable.ForeignKeys[1].RefTable = UsersTable + ReposTable.ForeignKeys[0].RefTable = UsersTable ReviewsTable.ForeignKeys[0].RefTable = DeploymentsTable ReviewsTable.ForeignKeys[1].RefTable = UsersTable } diff --git a/ent/mutation.go b/ent/mutation.go index cc452279..eeec2c68 100644 --- a/ent/mutation.go +++ b/ent/mutation.go @@ -7249,6 +7249,8 @@ type RepoMutation struct { deployment_statistics map[int]struct{} removeddeployment_statistics map[int]struct{} cleareddeployment_statistics bool + owner *int64 + clearedowner bool done bool oldValue func(context.Context) (*Repo, error) predicates []predicate.Repo @@ -7710,6 +7712,55 @@ func (m *RepoMutation) ResetLatestDeployedAt() { delete(m.clearedFields, repo.FieldLatestDeployedAt) } +// SetOwnerID sets the "owner_id" field. +func (m *RepoMutation) SetOwnerID(i int64) { + m.owner = &i +} + +// OwnerID returns the value of the "owner_id" field in the mutation. +func (m *RepoMutation) OwnerID() (r int64, exists bool) { + v := m.owner + if v == nil { + return + } + return *v, true +} + +// OldOwnerID returns the old "owner_id" field's value of the Repo entity. +// If the Repo object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *RepoMutation) OldOwnerID(ctx context.Context) (v int64, err error) { + if !m.op.Is(OpUpdateOne) { + return v, fmt.Errorf("OldOwnerID is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, fmt.Errorf("OldOwnerID requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldOwnerID: %w", err) + } + return oldValue.OwnerID, nil +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (m *RepoMutation) ClearOwnerID() { + m.owner = nil + m.clearedFields[repo.FieldOwnerID] = struct{}{} +} + +// OwnerIDCleared returns if the "owner_id" field was cleared in this mutation. +func (m *RepoMutation) OwnerIDCleared() bool { + _, ok := m.clearedFields[repo.FieldOwnerID] + return ok +} + +// ResetOwnerID resets all changes to the "owner_id" field. +func (m *RepoMutation) ResetOwnerID() { + m.owner = nil + delete(m.clearedFields, repo.FieldOwnerID) +} + // AddPermIDs adds the "perms" edge to the Perm entity by ids. func (m *RepoMutation) AddPermIDs(ids ...int) { if m.perms == nil { @@ -7980,6 +8031,32 @@ func (m *RepoMutation) ResetDeploymentStatistics() { m.removeddeployment_statistics = nil } +// ClearOwner clears the "owner" edge to the User entity. +func (m *RepoMutation) ClearOwner() { + m.clearedowner = true +} + +// OwnerCleared reports if the "owner" edge to the User entity was cleared. +func (m *RepoMutation) OwnerCleared() bool { + return m.OwnerIDCleared() || m.clearedowner +} + +// OwnerIDs returns the "owner" edge IDs in the mutation. +// Note that IDs always returns len(IDs) <= 1 for unique edges, and you should use +// OwnerID instead. It exists only for internal usage by the builders. +func (m *RepoMutation) OwnerIDs() (ids []int64) { + if id := m.owner; id != nil { + ids = append(ids, *id) + } + return +} + +// ResetOwner resets all changes to the "owner" edge. +func (m *RepoMutation) ResetOwner() { + m.owner = nil + m.clearedowner = false +} + // Where appends a list predicates to the RepoMutation builder. func (m *RepoMutation) Where(ps ...predicate.Repo) { m.predicates = append(m.predicates, ps...) @@ -7999,7 +8076,7 @@ func (m *RepoMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *RepoMutation) Fields() []string { - fields := make([]string, 0, 9) + fields := make([]string, 0, 10) if m.namespace != nil { fields = append(fields, repo.FieldNamespace) } @@ -8027,6 +8104,9 @@ func (m *RepoMutation) Fields() []string { if m.latest_deployed_at != nil { fields = append(fields, repo.FieldLatestDeployedAt) } + if m.owner != nil { + fields = append(fields, repo.FieldOwnerID) + } return fields } @@ -8053,6 +8133,8 @@ func (m *RepoMutation) Field(name string) (ent.Value, bool) { return m.UpdatedAt() case repo.FieldLatestDeployedAt: return m.LatestDeployedAt() + case repo.FieldOwnerID: + return m.OwnerID() } return nil, false } @@ -8080,6 +8162,8 @@ func (m *RepoMutation) OldField(ctx context.Context, name string) (ent.Value, er return m.OldUpdatedAt(ctx) case repo.FieldLatestDeployedAt: return m.OldLatestDeployedAt(ctx) + case repo.FieldOwnerID: + return m.OldOwnerID(ctx) } return nil, fmt.Errorf("unknown Repo field %s", name) } @@ -8152,6 +8236,13 @@ func (m *RepoMutation) SetField(name string, value ent.Value) error { } m.SetLatestDeployedAt(v) return nil + case repo.FieldOwnerID: + v, ok := value.(int64) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetOwnerID(v) + return nil } return fmt.Errorf("unknown Repo field %s", name) } @@ -8203,6 +8294,9 @@ func (m *RepoMutation) ClearedFields() []string { if m.FieldCleared(repo.FieldLatestDeployedAt) { fields = append(fields, repo.FieldLatestDeployedAt) } + if m.FieldCleared(repo.FieldOwnerID) { + fields = append(fields, repo.FieldOwnerID) + } return fields } @@ -8223,6 +8317,9 @@ func (m *RepoMutation) ClearField(name string) error { case repo.FieldLatestDeployedAt: m.ClearLatestDeployedAt() return nil + case repo.FieldOwnerID: + m.ClearOwnerID() + return nil } return fmt.Errorf("unknown Repo nullable field %s", name) } @@ -8258,13 +8355,16 @@ func (m *RepoMutation) ResetField(name string) error { case repo.FieldLatestDeployedAt: m.ResetLatestDeployedAt() return nil + case repo.FieldOwnerID: + m.ResetOwnerID() + return nil } return fmt.Errorf("unknown Repo field %s", name) } // AddedEdges returns all edge names that were set/added in this mutation. func (m *RepoMutation) AddedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.perms != nil { edges = append(edges, repo.EdgePerms) } @@ -8280,6 +8380,9 @@ func (m *RepoMutation) AddedEdges() []string { if m.deployment_statistics != nil { edges = append(edges, repo.EdgeDeploymentStatistics) } + if m.owner != nil { + edges = append(edges, repo.EdgeOwner) + } return edges } @@ -8317,13 +8420,17 @@ func (m *RepoMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case repo.EdgeOwner: + if id := m.owner; id != nil { + return []ent.Value{*id} + } } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *RepoMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.removedperms != nil { edges = append(edges, repo.EdgePerms) } @@ -8382,7 +8489,7 @@ func (m *RepoMutation) RemovedIDs(name string) []ent.Value { // ClearedEdges returns all edge names that were cleared in this mutation. func (m *RepoMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.clearedperms { edges = append(edges, repo.EdgePerms) } @@ -8398,6 +8505,9 @@ func (m *RepoMutation) ClearedEdges() []string { if m.cleareddeployment_statistics { edges = append(edges, repo.EdgeDeploymentStatistics) } + if m.clearedowner { + edges = append(edges, repo.EdgeOwner) + } return edges } @@ -8415,6 +8525,8 @@ func (m *RepoMutation) EdgeCleared(name string) bool { return m.clearedlocks case repo.EdgeDeploymentStatistics: return m.cleareddeployment_statistics + case repo.EdgeOwner: + return m.clearedowner } return false } @@ -8423,6 +8535,9 @@ func (m *RepoMutation) EdgeCleared(name string) bool { // if that edge is not defined in the schema. func (m *RepoMutation) ClearEdge(name string) error { switch name { + case repo.EdgeOwner: + m.ClearOwner() + return nil } return fmt.Errorf("unknown Repo unique edge %s", name) } @@ -8446,6 +8561,9 @@ func (m *RepoMutation) ResetEdge(name string) error { case repo.EdgeDeploymentStatistics: m.ResetDeploymentStatistics() return nil + case repo.EdgeOwner: + m.ResetOwner() + return nil } return fmt.Errorf("unknown Repo edge %s", name) } @@ -9250,6 +9368,9 @@ type UserMutation struct { locks map[int]struct{} removedlocks map[int]struct{} clearedlocks bool + repo map[int64]struct{} + removedrepo map[int64]struct{} + clearedrepo bool done bool oldValue func(context.Context) (*User, error) predicates []predicate.User @@ -9919,6 +10040,60 @@ func (m *UserMutation) ResetLocks() { m.removedlocks = nil } +// AddRepoIDs adds the "repo" edge to the Repo entity by ids. +func (m *UserMutation) AddRepoIDs(ids ...int64) { + if m.repo == nil { + m.repo = make(map[int64]struct{}) + } + for i := range ids { + m.repo[ids[i]] = struct{}{} + } +} + +// ClearRepo clears the "repo" edge to the Repo entity. +func (m *UserMutation) ClearRepo() { + m.clearedrepo = true +} + +// RepoCleared reports if the "repo" edge to the Repo entity was cleared. +func (m *UserMutation) RepoCleared() bool { + return m.clearedrepo +} + +// RemoveRepoIDs removes the "repo" edge to the Repo entity by IDs. +func (m *UserMutation) RemoveRepoIDs(ids ...int64) { + if m.removedrepo == nil { + m.removedrepo = make(map[int64]struct{}) + } + for i := range ids { + delete(m.repo, ids[i]) + m.removedrepo[ids[i]] = struct{}{} + } +} + +// RemovedRepo returns the removed IDs of the "repo" edge to the Repo entity. +func (m *UserMutation) RemovedRepoIDs() (ids []int64) { + for id := range m.removedrepo { + ids = append(ids, id) + } + return +} + +// RepoIDs returns the "repo" edge IDs in the mutation. +func (m *UserMutation) RepoIDs() (ids []int64) { + for id := range m.repo { + ids = append(ids, id) + } + return +} + +// ResetRepo resets all changes to the "repo" edge. +func (m *UserMutation) ResetRepo() { + m.repo = nil + m.clearedrepo = false + m.removedrepo = nil +} + // Where appends a list predicates to the UserMutation builder. func (m *UserMutation) Where(ps ...predicate.User) { m.predicates = append(m.predicates, ps...) @@ -10173,7 +10348,7 @@ func (m *UserMutation) ResetField(name string) error { // AddedEdges returns all edge names that were set/added in this mutation. func (m *UserMutation) AddedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.chat_user != nil { edges = append(edges, user.EdgeChatUser) } @@ -10189,6 +10364,9 @@ func (m *UserMutation) AddedEdges() []string { if m.locks != nil { edges = append(edges, user.EdgeLocks) } + if m.repo != nil { + edges = append(edges, user.EdgeRepo) + } return edges } @@ -10224,13 +10402,19 @@ func (m *UserMutation) AddedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case user.EdgeRepo: + ids := make([]ent.Value, 0, len(m.repo)) + for id := range m.repo { + ids = append(ids, id) + } + return ids } return nil } // RemovedEdges returns all edge names that were removed in this mutation. func (m *UserMutation) RemovedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.removedperms != nil { edges = append(edges, user.EdgePerms) } @@ -10243,6 +10427,9 @@ func (m *UserMutation) RemovedEdges() []string { if m.removedlocks != nil { edges = append(edges, user.EdgeLocks) } + if m.removedrepo != nil { + edges = append(edges, user.EdgeRepo) + } return edges } @@ -10274,13 +10461,19 @@ func (m *UserMutation) RemovedIDs(name string) []ent.Value { ids = append(ids, id) } return ids + case user.EdgeRepo: + ids := make([]ent.Value, 0, len(m.removedrepo)) + for id := range m.removedrepo { + ids = append(ids, id) + } + return ids } return nil } // ClearedEdges returns all edge names that were cleared in this mutation. func (m *UserMutation) ClearedEdges() []string { - edges := make([]string, 0, 5) + edges := make([]string, 0, 6) if m.clearedchat_user { edges = append(edges, user.EdgeChatUser) } @@ -10296,6 +10489,9 @@ func (m *UserMutation) ClearedEdges() []string { if m.clearedlocks { edges = append(edges, user.EdgeLocks) } + if m.clearedrepo { + edges = append(edges, user.EdgeRepo) + } return edges } @@ -10313,6 +10509,8 @@ func (m *UserMutation) EdgeCleared(name string) bool { return m.clearedreviews case user.EdgeLocks: return m.clearedlocks + case user.EdgeRepo: + return m.clearedrepo } return false } @@ -10347,6 +10545,9 @@ func (m *UserMutation) ResetEdge(name string) error { case user.EdgeLocks: m.ResetLocks() return nil + case user.EdgeRepo: + m.ResetRepo() + return nil } return fmt.Errorf("unknown User edge %s", name) } diff --git a/ent/repo.go b/ent/repo.go index ecd06395..6be65140 100644 --- a/ent/repo.go +++ b/ent/repo.go @@ -9,6 +9,7 @@ import ( "entgo.io/ent/dialect/sql" "github.com/gitploy-io/gitploy/ent/repo" + "github.com/gitploy-io/gitploy/ent/user" ) // Repo is the model entity for the Repo schema. @@ -34,6 +35,8 @@ type Repo struct { UpdatedAt time.Time `json:"updated_at"` // LatestDeployedAt holds the value of the "latest_deployed_at" field. LatestDeployedAt time.Time `json:"latest_deployed_at,omitemtpy"` + // OwnerID holds the value of the "owner_id" field. + OwnerID int64 `json:"owner_id,omitemtpy"` // Edges holds the relations/edges for other nodes in the graph. // The values are being populated by the RepoQuery when eager-loading is set. Edges RepoEdges `json:"edges"` @@ -51,9 +54,11 @@ type RepoEdges struct { Locks []*Lock `json:"locks,omitempty"` // DeploymentStatistics holds the value of the deployment_statistics edge. DeploymentStatistics []*DeploymentStatistics `json:"deployment_statistics,omitempty"` + // Owner holds the value of the owner edge. + Owner *User `json:"owner,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [5]bool + loadedTypes [6]bool } // PermsOrErr returns the Perms value or an error if the edge @@ -101,6 +106,20 @@ func (e RepoEdges) DeploymentStatisticsOrErr() ([]*DeploymentStatistics, error) return nil, &NotLoadedError{edge: "deployment_statistics"} } +// OwnerOrErr returns the Owner value or an error if the edge +// was not loaded in eager-loading, or loaded but was not found. +func (e RepoEdges) OwnerOrErr() (*User, error) { + if e.loadedTypes[5] { + if e.Owner == nil { + // The edge owner was loaded in eager-loading, + // but was not found. + return nil, &NotFoundError{label: user.Label} + } + return e.Owner, nil + } + return nil, &NotLoadedError{edge: "owner"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*Repo) scanValues(columns []string) ([]interface{}, error) { values := make([]interface{}, len(columns)) @@ -108,7 +127,7 @@ func (*Repo) scanValues(columns []string) ([]interface{}, error) { switch columns[i] { case repo.FieldActive: values[i] = new(sql.NullBool) - case repo.FieldID, repo.FieldWebhookID: + case repo.FieldID, repo.FieldWebhookID, repo.FieldOwnerID: values[i] = new(sql.NullInt64) case repo.FieldNamespace, repo.FieldName, repo.FieldDescription, repo.FieldConfigPath: values[i] = new(sql.NullString) @@ -189,6 +208,12 @@ func (r *Repo) assignValues(columns []string, values []interface{}) error { } else if value.Valid { r.LatestDeployedAt = value.Time } + case repo.FieldOwnerID: + if value, ok := values[i].(*sql.NullInt64); !ok { + return fmt.Errorf("unexpected type %T for field owner_id", values[i]) + } else if value.Valid { + r.OwnerID = value.Int64 + } } } return nil @@ -219,6 +244,11 @@ func (r *Repo) QueryDeploymentStatistics() *DeploymentStatisticsQuery { return (&RepoClient{config: r.config}).QueryDeploymentStatistics(r) } +// QueryOwner queries the "owner" edge of the Repo entity. +func (r *Repo) QueryOwner() *UserQuery { + return (&RepoClient{config: r.config}).QueryOwner(r) +} + // Update returns a builder for updating this Repo. // Note that you need to call Repo.Unwrap() before calling this method if this Repo // was returned from a transaction, and the transaction was committed or rolled back. @@ -260,6 +290,8 @@ func (r *Repo) String() string { builder.WriteString(r.UpdatedAt.Format(time.ANSIC)) builder.WriteString(", latest_deployed_at=") builder.WriteString(r.LatestDeployedAt.Format(time.ANSIC)) + builder.WriteString(", owner_id=") + builder.WriteString(fmt.Sprintf("%v", r.OwnerID)) builder.WriteByte(')') return builder.String() } diff --git a/ent/repo/repo.go b/ent/repo/repo.go index 9d44cbf3..fd79c018 100644 --- a/ent/repo/repo.go +++ b/ent/repo/repo.go @@ -29,6 +29,8 @@ const ( FieldUpdatedAt = "updated_at" // FieldLatestDeployedAt holds the string denoting the latest_deployed_at field in the database. FieldLatestDeployedAt = "latest_deployed_at" + // FieldOwnerID holds the string denoting the owner_id field in the database. + FieldOwnerID = "owner_id" // EdgePerms holds the string denoting the perms edge name in mutations. EdgePerms = "perms" // EdgeDeployments holds the string denoting the deployments edge name in mutations. @@ -39,6 +41,8 @@ const ( EdgeLocks = "locks" // EdgeDeploymentStatistics holds the string denoting the deployment_statistics edge name in mutations. EdgeDeploymentStatistics = "deployment_statistics" + // EdgeOwner holds the string denoting the owner edge name in mutations. + EdgeOwner = "owner" // Table holds the table name of the repo in the database. Table = "repos" // PermsTable is the table that holds the perms relation/edge. @@ -76,6 +80,13 @@ const ( DeploymentStatisticsInverseTable = "deployment_statistics" // DeploymentStatisticsColumn is the table column denoting the deployment_statistics relation/edge. DeploymentStatisticsColumn = "repo_id" + // OwnerTable is the table that holds the owner relation/edge. + OwnerTable = "repos" + // OwnerInverseTable is the table name for the User entity. + // It exists in this package in order to avoid circular dependency with the "user" package. + OwnerInverseTable = "users" + // OwnerColumn is the table column denoting the owner relation/edge. + OwnerColumn = "owner_id" ) // Columns holds all SQL columns for repo fields. @@ -90,6 +101,7 @@ var Columns = []string{ FieldCreatedAt, FieldUpdatedAt, FieldLatestDeployedAt, + FieldOwnerID, } // ValidColumn reports if the column name is valid (part of the table columns). diff --git a/ent/repo/where.go b/ent/repo/where.go index d65cce62..bac4dafa 100644 --- a/ent/repo/where.go +++ b/ent/repo/where.go @@ -156,6 +156,13 @@ func LatestDeployedAt(v time.Time) predicate.Repo { }) } +// OwnerID applies equality check predicate on the "owner_id" field. It's identical to OwnerIDEQ. +func OwnerID(v int64) predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldOwnerID), v)) + }) +} + // NamespaceEQ applies the EQ predicate on the "namespace" field. func NamespaceEQ(v string) predicate.Repo { return predicate.Repo(func(s *sql.Selector) { @@ -946,6 +953,68 @@ func LatestDeployedAtNotNil() predicate.Repo { }) } +// OwnerIDEQ applies the EQ predicate on the "owner_id" field. +func OwnerIDEQ(v int64) predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldOwnerID), v)) + }) +} + +// OwnerIDNEQ applies the NEQ predicate on the "owner_id" field. +func OwnerIDNEQ(v int64) predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldOwnerID), v)) + }) +} + +// OwnerIDIn applies the In predicate on the "owner_id" field. +func OwnerIDIn(vs ...int64) predicate.Repo { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Repo(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.In(s.C(FieldOwnerID), v...)) + }) +} + +// OwnerIDNotIn applies the NotIn predicate on the "owner_id" field. +func OwnerIDNotIn(vs ...int64) predicate.Repo { + v := make([]interface{}, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Repo(func(s *sql.Selector) { + // if not arguments were provided, append the FALSE constants, + // since we can't apply "IN ()". This will make this predicate falsy. + if len(v) == 0 { + s.Where(sql.False()) + return + } + s.Where(sql.NotIn(s.C(FieldOwnerID), v...)) + }) +} + +// OwnerIDIsNil applies the IsNil predicate on the "owner_id" field. +func OwnerIDIsNil() predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + s.Where(sql.IsNull(s.C(FieldOwnerID))) + }) +} + +// OwnerIDNotNil applies the NotNil predicate on the "owner_id" field. +func OwnerIDNotNil() predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + s.Where(sql.NotNull(s.C(FieldOwnerID))) + }) +} + // HasPerms applies the HasEdge predicate on the "perms" edge. func HasPerms() predicate.Repo { return predicate.Repo(func(s *sql.Selector) { @@ -1086,6 +1155,34 @@ func HasDeploymentStatisticsWith(preds ...predicate.DeploymentStatistics) predic }) } +// HasOwner applies the HasEdge predicate on the "owner" edge. +func HasOwner() predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(OwnerTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, OwnerTable, OwnerColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasOwnerWith applies the HasEdge predicate on the "owner" edge with a given conditions (other predicates). +func HasOwnerWith(preds ...predicate.User) predicate.Repo { + return predicate.Repo(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(OwnerInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, OwnerTable, OwnerColumn), + ) + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.Repo) predicate.Repo { return predicate.Repo(func(s *sql.Selector) { diff --git a/ent/repo_create.go b/ent/repo_create.go index 14e0ef51..b9c2a6aa 100644 --- a/ent/repo_create.go +++ b/ent/repo_create.go @@ -16,6 +16,7 @@ import ( "github.com/gitploy-io/gitploy/ent/lock" "github.com/gitploy-io/gitploy/ent/perm" "github.com/gitploy-io/gitploy/ent/repo" + "github.com/gitploy-io/gitploy/ent/user" ) // RepoCreate is the builder for creating a Repo entity. @@ -127,6 +128,20 @@ func (rc *RepoCreate) SetNillableLatestDeployedAt(t *time.Time) *RepoCreate { return rc } +// SetOwnerID sets the "owner_id" field. +func (rc *RepoCreate) SetOwnerID(i int64) *RepoCreate { + rc.mutation.SetOwnerID(i) + return rc +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (rc *RepoCreate) SetNillableOwnerID(i *int64) *RepoCreate { + if i != nil { + rc.SetOwnerID(*i) + } + return rc +} + // SetID sets the "id" field. func (rc *RepoCreate) SetID(i int64) *RepoCreate { rc.mutation.SetID(i) @@ -208,6 +223,11 @@ func (rc *RepoCreate) AddDeploymentStatistics(d ...*DeploymentStatistics) *RepoC return rc.AddDeploymentStatisticIDs(ids...) } +// SetOwner sets the "owner" edge to the User entity. +func (rc *RepoCreate) SetOwner(u *User) *RepoCreate { + return rc.SetOwnerID(u.ID) +} + // Mutation returns the RepoMutation object of the builder. func (rc *RepoCreate) Mutation() *RepoMutation { return rc.mutation @@ -520,6 +540,26 @@ func (rc *RepoCreate) createSpec() (*Repo, *sqlgraph.CreateSpec) { } _spec.Edges = append(_spec.Edges, edge) } + if nodes := rc.mutation.OwnerIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: repo.OwnerTable, + Columns: []string{repo.OwnerColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: user.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _node.OwnerID = nodes[0] + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/ent/repo_query.go b/ent/repo_query.go index 31d918de..a9e873ae 100644 --- a/ent/repo_query.go +++ b/ent/repo_query.go @@ -20,6 +20,7 @@ import ( "github.com/gitploy-io/gitploy/ent/perm" "github.com/gitploy-io/gitploy/ent/predicate" "github.com/gitploy-io/gitploy/ent/repo" + "github.com/gitploy-io/gitploy/ent/user" ) // RepoQuery is the builder for querying Repo entities. @@ -37,6 +38,7 @@ type RepoQuery struct { withCallback *CallbackQuery withLocks *LockQuery withDeploymentStatistics *DeploymentStatisticsQuery + withOwner *UserQuery modifiers []func(s *sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector @@ -184,6 +186,28 @@ func (rq *RepoQuery) QueryDeploymentStatistics() *DeploymentStatisticsQuery { return query } +// QueryOwner chains the current query on the "owner" edge. +func (rq *RepoQuery) QueryOwner() *UserQuery { + query := &UserQuery{config: rq.config} + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := rq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := rq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(repo.Table, repo.FieldID, selector), + sqlgraph.To(user.Table, user.FieldID), + sqlgraph.Edge(sqlgraph.M2O, true, repo.OwnerTable, repo.OwnerColumn), + ) + fromU = sqlgraph.SetNeighbors(rq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first Repo entity from the query. // Returns a *NotFoundError when no Repo was found. func (rq *RepoQuery) First(ctx context.Context) (*Repo, error) { @@ -370,6 +394,7 @@ func (rq *RepoQuery) Clone() *RepoQuery { withCallback: rq.withCallback.Clone(), withLocks: rq.withLocks.Clone(), withDeploymentStatistics: rq.withDeploymentStatistics.Clone(), + withOwner: rq.withOwner.Clone(), // clone intermediate query. sql: rq.sql.Clone(), path: rq.path, @@ -431,6 +456,17 @@ func (rq *RepoQuery) WithDeploymentStatistics(opts ...func(*DeploymentStatistics return rq } +// WithOwner tells the query-builder to eager-load the nodes that are connected to +// the "owner" edge. The optional arguments are used to configure the query builder of the edge. +func (rq *RepoQuery) WithOwner(opts ...func(*UserQuery)) *RepoQuery { + query := &UserQuery{config: rq.config} + for _, opt := range opts { + opt(query) + } + rq.withOwner = query + return rq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -496,12 +532,13 @@ func (rq *RepoQuery) sqlAll(ctx context.Context) ([]*Repo, error) { var ( nodes = []*Repo{} _spec = rq.querySpec() - loadedTypes = [5]bool{ + loadedTypes = [6]bool{ rq.withPerms != nil, rq.withDeployments != nil, rq.withCallback != nil, rq.withLocks != nil, rq.withDeploymentStatistics != nil, + rq.withOwner != nil, } ) _spec.ScanValues = func(columns []string) ([]interface{}, error) { @@ -652,6 +689,32 @@ func (rq *RepoQuery) sqlAll(ctx context.Context) ([]*Repo, error) { } } + if query := rq.withOwner; query != nil { + ids := make([]int64, 0, len(nodes)) + nodeids := make(map[int64][]*Repo) + for i := range nodes { + fk := nodes[i].OwnerID + if _, ok := nodeids[fk]; !ok { + ids = append(ids, fk) + } + nodeids[fk] = append(nodeids[fk], nodes[i]) + } + query.Where(user.IDIn(ids...)) + neighbors, err := query.All(ctx) + if err != nil { + return nil, err + } + for _, n := range neighbors { + nodes, ok := nodeids[n.ID] + if !ok { + return nil, fmt.Errorf(`unexpected foreign-key "owner_id" returned %v`, n.ID) + } + for i := range nodes { + nodes[i].Edges.Owner = n + } + } + } + return nodes, nil } diff --git a/ent/repo_update.go b/ent/repo_update.go index b18632f0..cec869a4 100644 --- a/ent/repo_update.go +++ b/ent/repo_update.go @@ -17,6 +17,7 @@ import ( "github.com/gitploy-io/gitploy/ent/perm" "github.com/gitploy-io/gitploy/ent/predicate" "github.com/gitploy-io/gitploy/ent/repo" + "github.com/gitploy-io/gitploy/ent/user" ) // RepoUpdate is the builder for updating Repo entities. @@ -145,6 +146,26 @@ func (ru *RepoUpdate) ClearLatestDeployedAt() *RepoUpdate { return ru } +// SetOwnerID sets the "owner_id" field. +func (ru *RepoUpdate) SetOwnerID(i int64) *RepoUpdate { + ru.mutation.SetOwnerID(i) + return ru +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (ru *RepoUpdate) SetNillableOwnerID(i *int64) *RepoUpdate { + if i != nil { + ru.SetOwnerID(*i) + } + return ru +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (ru *RepoUpdate) ClearOwnerID() *RepoUpdate { + ru.mutation.ClearOwnerID() + return ru +} + // AddPermIDs adds the "perms" edge to the Perm entity by IDs. func (ru *RepoUpdate) AddPermIDs(ids ...int) *RepoUpdate { ru.mutation.AddPermIDs(ids...) @@ -220,6 +241,11 @@ func (ru *RepoUpdate) AddDeploymentStatistics(d ...*DeploymentStatistics) *RepoU return ru.AddDeploymentStatisticIDs(ids...) } +// SetOwner sets the "owner" edge to the User entity. +func (ru *RepoUpdate) SetOwner(u *User) *RepoUpdate { + return ru.SetOwnerID(u.ID) +} + // Mutation returns the RepoMutation object of the builder. func (ru *RepoUpdate) Mutation() *RepoMutation { return ru.mutation @@ -330,6 +356,12 @@ func (ru *RepoUpdate) RemoveDeploymentStatistics(d ...*DeploymentStatistics) *Re return ru.RemoveDeploymentStatisticIDs(ids...) } +// ClearOwner clears the "owner" edge to the User entity. +func (ru *RepoUpdate) ClearOwner() *RepoUpdate { + ru.mutation.ClearOwner() + return ru +} + // Save executes the query and returns the number of nodes affected by the update operation. func (ru *RepoUpdate) Save(ctx context.Context) (int, error) { var ( @@ -763,6 +795,41 @@ func (ru *RepoUpdate) sqlSave(ctx context.Context) (n int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if ru.mutation.OwnerCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: repo.OwnerTable, + Columns: []string{repo.OwnerColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: user.FieldID, + }, + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := ru.mutation.OwnerIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: repo.OwnerTable, + Columns: []string{repo.OwnerColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: user.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if n, err = sqlgraph.UpdateNodes(ctx, ru.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{repo.Label} @@ -895,6 +962,26 @@ func (ruo *RepoUpdateOne) ClearLatestDeployedAt() *RepoUpdateOne { return ruo } +// SetOwnerID sets the "owner_id" field. +func (ruo *RepoUpdateOne) SetOwnerID(i int64) *RepoUpdateOne { + ruo.mutation.SetOwnerID(i) + return ruo +} + +// SetNillableOwnerID sets the "owner_id" field if the given value is not nil. +func (ruo *RepoUpdateOne) SetNillableOwnerID(i *int64) *RepoUpdateOne { + if i != nil { + ruo.SetOwnerID(*i) + } + return ruo +} + +// ClearOwnerID clears the value of the "owner_id" field. +func (ruo *RepoUpdateOne) ClearOwnerID() *RepoUpdateOne { + ruo.mutation.ClearOwnerID() + return ruo +} + // AddPermIDs adds the "perms" edge to the Perm entity by IDs. func (ruo *RepoUpdateOne) AddPermIDs(ids ...int) *RepoUpdateOne { ruo.mutation.AddPermIDs(ids...) @@ -970,6 +1057,11 @@ func (ruo *RepoUpdateOne) AddDeploymentStatistics(d ...*DeploymentStatistics) *R return ruo.AddDeploymentStatisticIDs(ids...) } +// SetOwner sets the "owner" edge to the User entity. +func (ruo *RepoUpdateOne) SetOwner(u *User) *RepoUpdateOne { + return ruo.SetOwnerID(u.ID) +} + // Mutation returns the RepoMutation object of the builder. func (ruo *RepoUpdateOne) Mutation() *RepoMutation { return ruo.mutation @@ -1080,6 +1172,12 @@ func (ruo *RepoUpdateOne) RemoveDeploymentStatistics(d ...*DeploymentStatistics) return ruo.RemoveDeploymentStatisticIDs(ids...) } +// ClearOwner clears the "owner" edge to the User entity. +func (ruo *RepoUpdateOne) ClearOwner() *RepoUpdateOne { + ruo.mutation.ClearOwner() + return ruo +} + // Select allows selecting one or more fields (columns) of the returned entity. // The default is selecting all fields defined in the entity schema. func (ruo *RepoUpdateOne) Select(field string, fields ...string) *RepoUpdateOne { @@ -1537,6 +1635,41 @@ func (ruo *RepoUpdateOne) sqlSave(ctx context.Context) (_node *Repo, err error) } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if ruo.mutation.OwnerCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: repo.OwnerTable, + Columns: []string{repo.OwnerColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: user.FieldID, + }, + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := ruo.mutation.OwnerIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.M2O, + Inverse: true, + Table: repo.OwnerTable, + Columns: []string{repo.OwnerColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: user.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &Repo{config: ruo.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/ent/schema/repo.go b/ent/schema/repo.go index f27142b2..7b44a113 100644 --- a/ent/schema/repo.go +++ b/ent/schema/repo.go @@ -35,6 +35,9 @@ func (Repo) Fields() []ent.Field { // Denormalization to sort with deployment. field.Time("latest_deployed_at"). Optional(), + // The 'owner_id' field is the ID who activated the repository. + field.Int64("owner_id"). + Optional(), } } @@ -61,6 +64,10 @@ func (Repo) Edges() []ent.Edge { Annotations(entsql.Annotation{ OnDelete: entsql.Cascade, }), + edge.From("owner", User.Type). + Ref("repo"). + Field("owner_id"). + Unique(), } } diff --git a/ent/schema/user.go b/ent/schema/user.go index 002b1bb1..e62eb3df 100644 --- a/ent/schema/user.go +++ b/ent/schema/user.go @@ -54,5 +54,6 @@ func (User) Edges() []ent.Edge { edge.To("deployments", Deployment.Type), edge.To("reviews", Review.Type), edge.To("locks", Lock.Type), + edge.To("repos", Repo.Type), } } diff --git a/ent/user.go b/ent/user.go index af049bcf..09fdda40 100644 --- a/ent/user.go +++ b/ent/user.go @@ -52,9 +52,11 @@ type UserEdges struct { Reviews []*Review `json:"reviews,omitempty"` // Locks holds the value of the locks edge. Locks []*Lock `json:"locks,omitempty"` + // Repo holds the value of the repo edge. + Repo []*Repo `json:"repo,omitempty"` // loadedTypes holds the information for reporting if a // type was loaded (or requested) in eager-loading or not. - loadedTypes [5]bool + loadedTypes [6]bool } // ChatUserOrErr returns the ChatUser value or an error if the edge @@ -107,6 +109,15 @@ func (e UserEdges) LocksOrErr() ([]*Lock, error) { return nil, &NotLoadedError{edge: "locks"} } +// RepoOrErr returns the Repo value or an error if the edge +// was not loaded in eager-loading. +func (e UserEdges) RepoOrErr() ([]*Repo, error) { + if e.loadedTypes[5] { + return e.Repo, nil + } + return nil, &NotLoadedError{edge: "repo"} +} + // scanValues returns the types for scanning values from sql.Rows. func (*User) scanValues(columns []string) ([]interface{}, error) { values := make([]interface{}, len(columns)) @@ -225,6 +236,11 @@ func (u *User) QueryLocks() *LockQuery { return (&UserClient{config: u.config}).QueryLocks(u) } +// QueryRepo queries the "repo" edge of the User entity. +func (u *User) QueryRepo() *RepoQuery { + return (&UserClient{config: u.config}).QueryRepo(u) +} + // Update returns a builder for updating this User. // Note that you need to call User.Unwrap() before calling this method if this User // was returned from a transaction, and the transaction was committed or rolled back. diff --git a/ent/user/user.go b/ent/user/user.go index 9532cbe1..2938adbc 100644 --- a/ent/user/user.go +++ b/ent/user/user.go @@ -39,6 +39,8 @@ const ( EdgeReviews = "reviews" // EdgeLocks holds the string denoting the locks edge name in mutations. EdgeLocks = "locks" + // EdgeRepo holds the string denoting the repo edge name in mutations. + EdgeRepo = "repo" // Table holds the table name of the user in the database. Table = "users" // ChatUserTable is the table that holds the chat_user relation/edge. @@ -76,6 +78,13 @@ const ( LocksInverseTable = "locks" // LocksColumn is the table column denoting the locks relation/edge. LocksColumn = "user_id" + // RepoTable is the table that holds the repo relation/edge. + RepoTable = "repos" + // RepoInverseTable is the table name for the Repo entity. + // It exists in this package in order to avoid circular dependency with the "repo" package. + RepoInverseTable = "repos" + // RepoColumn is the table column denoting the repo relation/edge. + RepoColumn = "owner_id" ) // Columns holds all SQL columns for user fields. diff --git a/ent/user/where.go b/ent/user/where.go index a3780593..2bb08161 100644 --- a/ent/user/where.go +++ b/ent/user/where.go @@ -1093,6 +1093,34 @@ func HasLocksWith(preds ...predicate.Lock) predicate.User { }) } +// HasRepo applies the HasEdge predicate on the "repo" edge. +func HasRepo() predicate.User { + return predicate.User(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(RepoTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, RepoTable, RepoColumn), + ) + sqlgraph.HasNeighbors(s, step) + }) +} + +// HasRepoWith applies the HasEdge predicate on the "repo" edge with a given conditions (other predicates). +func HasRepoWith(preds ...predicate.Repo) predicate.User { + return predicate.User(func(s *sql.Selector) { + step := sqlgraph.NewStep( + sqlgraph.From(Table, FieldID), + sqlgraph.To(RepoInverseTable, FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, RepoTable, RepoColumn), + ) + sqlgraph.HasNeighborsWith(s, step, func(s *sql.Selector) { + for _, p := range preds { + p(s) + } + }) + }) +} + // And groups predicates with the AND operator between them. func And(predicates ...predicate.User) predicate.User { return predicate.User(func(s *sql.Selector) { diff --git a/ent/user_create.go b/ent/user_create.go index 7cbc3f3e..0a22a39d 100644 --- a/ent/user_create.go +++ b/ent/user_create.go @@ -14,6 +14,7 @@ import ( "github.com/gitploy-io/gitploy/ent/deployment" "github.com/gitploy-io/gitploy/ent/lock" "github.com/gitploy-io/gitploy/ent/perm" + "github.com/gitploy-io/gitploy/ent/repo" "github.com/gitploy-io/gitploy/ent/review" "github.com/gitploy-io/gitploy/ent/user" ) @@ -196,6 +197,21 @@ func (uc *UserCreate) AddLocks(l ...*Lock) *UserCreate { return uc.AddLockIDs(ids...) } +// AddRepoIDs adds the "repo" edge to the Repo entity by IDs. +func (uc *UserCreate) AddRepoIDs(ids ...int64) *UserCreate { + uc.mutation.AddRepoIDs(ids...) + return uc +} + +// AddRepo adds the "repo" edges to the Repo entity. +func (uc *UserCreate) AddRepo(r ...*Repo) *UserCreate { + ids := make([]int64, len(r)) + for i := range r { + ids[i] = r[i].ID + } + return uc.AddRepoIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (uc *UserCreate) Mutation() *UserMutation { return uc.mutation @@ -514,6 +530,25 @@ func (uc *UserCreate) createSpec() (*User, *sqlgraph.CreateSpec) { } _spec.Edges = append(_spec.Edges, edge) } + if nodes := uc.mutation.RepoIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges = append(_spec.Edges, edge) + } return _node, _spec } diff --git a/ent/user_query.go b/ent/user_query.go index 5750d407..220e83d3 100644 --- a/ent/user_query.go +++ b/ent/user_query.go @@ -18,6 +18,7 @@ import ( "github.com/gitploy-io/gitploy/ent/lock" "github.com/gitploy-io/gitploy/ent/perm" "github.com/gitploy-io/gitploy/ent/predicate" + "github.com/gitploy-io/gitploy/ent/repo" "github.com/gitploy-io/gitploy/ent/review" "github.com/gitploy-io/gitploy/ent/user" ) @@ -37,6 +38,7 @@ type UserQuery struct { withDeployments *DeploymentQuery withReviews *ReviewQuery withLocks *LockQuery + withRepo *RepoQuery modifiers []func(s *sql.Selector) // intermediate query (i.e. traversal path). sql *sql.Selector @@ -184,6 +186,28 @@ func (uq *UserQuery) QueryLocks() *LockQuery { return query } +// QueryRepo chains the current query on the "repo" edge. +func (uq *UserQuery) QueryRepo() *RepoQuery { + query := &RepoQuery{config: uq.config} + query.path = func(ctx context.Context) (fromU *sql.Selector, err error) { + if err := uq.prepareQuery(ctx); err != nil { + return nil, err + } + selector := uq.sqlQuery(ctx) + if err := selector.Err(); err != nil { + return nil, err + } + step := sqlgraph.NewStep( + sqlgraph.From(user.Table, user.FieldID, selector), + sqlgraph.To(repo.Table, repo.FieldID), + sqlgraph.Edge(sqlgraph.O2M, false, user.RepoTable, user.RepoColumn), + ) + fromU = sqlgraph.SetNeighbors(uq.driver.Dialect(), step) + return fromU, nil + } + return query +} + // First returns the first User entity from the query. // Returns a *NotFoundError when no User was found. func (uq *UserQuery) First(ctx context.Context) (*User, error) { @@ -370,6 +394,7 @@ func (uq *UserQuery) Clone() *UserQuery { withDeployments: uq.withDeployments.Clone(), withReviews: uq.withReviews.Clone(), withLocks: uq.withLocks.Clone(), + withRepo: uq.withRepo.Clone(), // clone intermediate query. sql: uq.sql.Clone(), path: uq.path, @@ -431,6 +456,17 @@ func (uq *UserQuery) WithLocks(opts ...func(*LockQuery)) *UserQuery { return uq } +// WithRepo tells the query-builder to eager-load the nodes that are connected to +// the "repo" edge. The optional arguments are used to configure the query builder of the edge. +func (uq *UserQuery) WithRepo(opts ...func(*RepoQuery)) *UserQuery { + query := &RepoQuery{config: uq.config} + for _, opt := range opts { + opt(query) + } + uq.withRepo = query + return uq +} + // GroupBy is used to group vertices by one or more fields/columns. // It is often used with aggregate functions, like: count, max, mean, min, sum. // @@ -496,12 +532,13 @@ func (uq *UserQuery) sqlAll(ctx context.Context) ([]*User, error) { var ( nodes = []*User{} _spec = uq.querySpec() - loadedTypes = [5]bool{ + loadedTypes = [6]bool{ uq.withChatUser != nil, uq.withPerms != nil, uq.withDeployments != nil, uq.withReviews != nil, uq.withLocks != nil, + uq.withRepo != nil, } ) _spec.ScanValues = func(columns []string) ([]interface{}, error) { @@ -651,6 +688,31 @@ func (uq *UserQuery) sqlAll(ctx context.Context) ([]*User, error) { } } + if query := uq.withRepo; query != nil { + fks := make([]driver.Value, 0, len(nodes)) + nodeids := make(map[int64]*User) + for i := range nodes { + fks = append(fks, nodes[i].ID) + nodeids[nodes[i].ID] = nodes[i] + nodes[i].Edges.Repo = []*Repo{} + } + query.Where(predicate.Repo(func(s *sql.Selector) { + s.Where(sql.InValues(user.RepoColumn, fks...)) + })) + neighbors, err := query.All(ctx) + if err != nil { + return nil, err + } + for _, n := range neighbors { + fk := n.OwnerID + node, ok := nodeids[fk] + if !ok { + return nil, fmt.Errorf(`unexpected foreign-key "owner_id" returned %v for node %v`, fk, n.ID) + } + node.Edges.Repo = append(node.Edges.Repo, n) + } + } + return nodes, nil } diff --git a/ent/user_update.go b/ent/user_update.go index d8b699cc..90979b68 100644 --- a/ent/user_update.go +++ b/ent/user_update.go @@ -15,6 +15,7 @@ import ( "github.com/gitploy-io/gitploy/ent/lock" "github.com/gitploy-io/gitploy/ent/perm" "github.com/gitploy-io/gitploy/ent/predicate" + "github.com/gitploy-io/gitploy/ent/repo" "github.com/gitploy-io/gitploy/ent/review" "github.com/gitploy-io/gitploy/ent/user" ) @@ -175,6 +176,21 @@ func (uu *UserUpdate) AddLocks(l ...*Lock) *UserUpdate { return uu.AddLockIDs(ids...) } +// AddRepoIDs adds the "repo" edge to the Repo entity by IDs. +func (uu *UserUpdate) AddRepoIDs(ids ...int64) *UserUpdate { + uu.mutation.AddRepoIDs(ids...) + return uu +} + +// AddRepo adds the "repo" edges to the Repo entity. +func (uu *UserUpdate) AddRepo(r ...*Repo) *UserUpdate { + ids := make([]int64, len(r)) + for i := range r { + ids[i] = r[i].ID + } + return uu.AddRepoIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (uu *UserUpdate) Mutation() *UserMutation { return uu.mutation @@ -270,6 +286,27 @@ func (uu *UserUpdate) RemoveLocks(l ...*Lock) *UserUpdate { return uu.RemoveLockIDs(ids...) } +// ClearRepo clears all "repo" edges to the Repo entity. +func (uu *UserUpdate) ClearRepo() *UserUpdate { + uu.mutation.ClearRepo() + return uu +} + +// RemoveRepoIDs removes the "repo" edge to Repo entities by IDs. +func (uu *UserUpdate) RemoveRepoIDs(ids ...int64) *UserUpdate { + uu.mutation.RemoveRepoIDs(ids...) + return uu +} + +// RemoveRepo removes "repo" edges to Repo entities. +func (uu *UserUpdate) RemoveRepo(r ...*Repo) *UserUpdate { + ids := make([]int64, len(r)) + for i := range r { + ids[i] = r[i].ID + } + return uu.RemoveRepoIDs(ids...) +} + // Save executes the query and returns the number of nodes affected by the update operation. func (uu *UserUpdate) Save(ctx context.Context) (int, error) { var ( @@ -658,6 +695,60 @@ func (uu *UserUpdate) sqlSave(ctx context.Context) (n int, err error) { } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if uu.mutation.RepoCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := uu.mutation.RemovedRepoIDs(); len(nodes) > 0 && !uu.mutation.RepoCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := uu.mutation.RepoIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } if n, err = sqlgraph.UpdateNodes(ctx, uu.driver, _spec); err != nil { if _, ok := err.(*sqlgraph.NotFoundError); ok { err = &NotFoundError{user.Label} @@ -820,6 +911,21 @@ func (uuo *UserUpdateOne) AddLocks(l ...*Lock) *UserUpdateOne { return uuo.AddLockIDs(ids...) } +// AddRepoIDs adds the "repo" edge to the Repo entity by IDs. +func (uuo *UserUpdateOne) AddRepoIDs(ids ...int64) *UserUpdateOne { + uuo.mutation.AddRepoIDs(ids...) + return uuo +} + +// AddRepo adds the "repo" edges to the Repo entity. +func (uuo *UserUpdateOne) AddRepo(r ...*Repo) *UserUpdateOne { + ids := make([]int64, len(r)) + for i := range r { + ids[i] = r[i].ID + } + return uuo.AddRepoIDs(ids...) +} + // Mutation returns the UserMutation object of the builder. func (uuo *UserUpdateOne) Mutation() *UserMutation { return uuo.mutation @@ -915,6 +1021,27 @@ func (uuo *UserUpdateOne) RemoveLocks(l ...*Lock) *UserUpdateOne { return uuo.RemoveLockIDs(ids...) } +// ClearRepo clears all "repo" edges to the Repo entity. +func (uuo *UserUpdateOne) ClearRepo() *UserUpdateOne { + uuo.mutation.ClearRepo() + return uuo +} + +// RemoveRepoIDs removes the "repo" edge to Repo entities by IDs. +func (uuo *UserUpdateOne) RemoveRepoIDs(ids ...int64) *UserUpdateOne { + uuo.mutation.RemoveRepoIDs(ids...) + return uuo +} + +// RemoveRepo removes "repo" edges to Repo entities. +func (uuo *UserUpdateOne) RemoveRepo(r ...*Repo) *UserUpdateOne { + ids := make([]int64, len(r)) + for i := range r { + ids[i] = r[i].ID + } + return uuo.RemoveRepoIDs(ids...) +} + // Select allows selecting one or more fields (columns) of the returned entity. // The default is selecting all fields defined in the entity schema. func (uuo *UserUpdateOne) Select(field string, fields ...string) *UserUpdateOne { @@ -1327,6 +1454,60 @@ func (uuo *UserUpdateOne) sqlSave(ctx context.Context) (_node *User, err error) } _spec.Edges.Add = append(_spec.Edges.Add, edge) } + if uuo.mutation.RepoCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := uuo.mutation.RemovedRepoIDs(); len(nodes) > 0 && !uuo.mutation.RepoCleared() { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Clear = append(_spec.Edges.Clear, edge) + } + if nodes := uuo.mutation.RepoIDs(); len(nodes) > 0 { + edge := &sqlgraph.EdgeSpec{ + Rel: sqlgraph.O2M, + Inverse: false, + Table: user.RepoTable, + Columns: []string{user.RepoColumn}, + Bidi: false, + Target: &sqlgraph.EdgeTarget{ + IDSpec: &sqlgraph.FieldSpec{ + Type: field.TypeInt64, + Column: repo.FieldID, + }, + }, + } + for _, k := range nodes { + edge.Target.Nodes = append(edge.Target.Nodes, k) + } + _spec.Edges.Add = append(_spec.Edges.Add, edge) + } _node = &User{config: uuo.config} _spec.Assign = _node.assignValues _spec.ScanValues = _node.scanValues diff --git a/internal/interactor/deployment.go b/internal/interactor/deployment.go index 4fb11574..dab0498b 100644 --- a/internal/interactor/deployment.go +++ b/internal/interactor/deployment.go @@ -37,6 +37,10 @@ func (i *Interactor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *en return nil, err } + if err := env.Eval(&vo.EvalValues{IsRollback: d.IsRollback}); err != nil { + return nil, err + } + number, err := i.Store.GetNextDeploymentNumberOfRepo(ctx, r) if err != nil { return nil, e.NewError( @@ -135,6 +139,10 @@ func (i *Interactor) DeployToRemote(ctx context.Context, u *ent.User, r *ent.Rep ) } + if err := env.Eval(&vo.EvalValues{IsRollback: d.IsRollback}); err != nil { + return nil, err + } + rd, err := i.createRemoteDeployment(ctx, u, r, d, env) if err != nil { return nil, err diff --git a/internal/interactor/repo.go b/internal/interactor/repo.go index 4ede16f7..4e2b2ddb 100644 --- a/internal/interactor/repo.go +++ b/internal/interactor/repo.go @@ -9,13 +9,15 @@ import ( ) func (i *Interactor) ActivateRepo(ctx context.Context, u *ent.User, r *ent.Repo, c *vo.WebhookConfig) (*ent.Repo, error) { - hid, err := i.CreateWebhook(ctx, u, r, c) + hid, err := i.SCM.CreateWebhook(ctx, u, r, c) if err != nil { return nil, fmt.Errorf("failed to create a webhook: %s", err) } r.WebhookID = hid - r, err = i.Activate(ctx, r) + r.OwnerID = u.ID + + r, err = i.Store.Activate(ctx, r) if err != nil { return nil, fmt.Errorf("failed to activate the webhook: %w", err) } @@ -24,12 +26,12 @@ func (i *Interactor) ActivateRepo(ctx context.Context, u *ent.User, r *ent.Repo, } func (i *Interactor) DeactivateRepo(ctx context.Context, u *ent.User, r *ent.Repo) (*ent.Repo, error) { - err := i.DeleteWebhook(ctx, u, r, r.WebhookID) + err := i.SCM.DeleteWebhook(ctx, u, r, r.WebhookID) if err != nil { return nil, fmt.Errorf("failed to delete the webhook: %w", err) } - r, err = i.Deactivate(ctx, r) + r, err = i.Store.Deactivate(ctx, r) if err != nil { return nil, fmt.Errorf("failed to deactivate the webhook: %w", err) } diff --git a/internal/pkg/github/repos.go b/internal/pkg/github/repos.go index 7419c055..15f9f479 100644 --- a/internal/pkg/github/repos.go +++ b/internal/pkg/github/repos.go @@ -250,7 +250,7 @@ func (g *Github) CreateWebhook(ctx context.Context, u *ent.User, r *ent.Repo, c "secret": c.Secret, "insecure_ssl": mapInsecureSSL(c.InsecureSSL), }, - Events: []string{"deployment_status"}, + Events: []string{"deployment_status", "push"}, Active: github.Bool(true), }) if err != nil { diff --git a/internal/pkg/store/repo.go b/internal/pkg/store/repo.go index 775f32f8..d4cd912e 100644 --- a/internal/pkg/store/repo.go +++ b/internal/pkg/store/repo.go @@ -47,6 +47,7 @@ func (s *Store) ListReposOfUser(ctx context.Context, u *ent.User, q, namespace, On(t.C(perm.FieldRepoID), s.C(repo.FieldID)). Where(sql.EQ(t.C(perm.FieldUserID), u.ID)) }). + WithOwner(). Limit(perPage). Offset(offset(page, perPage)) @@ -95,7 +96,13 @@ func (s *Store) ListReposOfUser(ctx context.Context, u *ent.User, q, namespace, } func (s *Store) FindRepoByID(ctx context.Context, id int64) (*ent.Repo, error) { - r, err := s.c.Repo.Get(ctx, id) + r, err := s.c.Repo. + Query(). + Where( + repo.IDEQ(id), + ). + WithOwner(). + Only(ctx) if ent.IsNotFound(err) { return nil, e.NewErrorWithMessage(e.ErrorCodeNotFound, "The repository is not found.", err) } else if err != nil { @@ -118,6 +125,7 @@ func (s *Store) FindRepoOfUserByID(ctx context.Context, u *ent.User, id int64) ( Where( repo.IDEQ(id), ). + WithOwner(). Only(ctx) if ent.IsNotFound(err) { return nil, e.NewErrorWithMessage(e.ErrorCodeNotFound, "The repository is not found.", err) @@ -144,6 +152,7 @@ func (s *Store) FindRepoOfUserByNamespaceName(ctx context.Context, u *ent.User, repo.NameEQ(name), ), ). + WithOwner(). Only(ctx) if ent.IsNotFound(err) { return nil, e.NewErrorWithMessage(e.ErrorCodeNotFound, "The repository is not found.", err) @@ -196,6 +205,7 @@ func (s *Store) Activate(ctx context.Context, r *ent.Repo) (*ent.Repo, error) { UpdateOne(r). SetActive(true). SetWebhookID(r.WebhookID). + SetOwnerID(r.OwnerID). Save(ctx) if ent.IsValidationError(err) { return nil, e.NewErrorWithMessage( @@ -214,6 +224,7 @@ func (s *Store) Deactivate(ctx context.Context, r *ent.Repo) (*ent.Repo, error) UpdateOne(r). SetActive(false). SetWebhookID(0). + SetOwnerID(0). Save(ctx) if ent.IsValidationError(err) { return nil, e.NewErrorWithMessage( diff --git a/internal/pkg/store/repo_test.go b/internal/pkg/store/repo_test.go index ef03fe0c..5dab1888 100644 --- a/internal/pkg/store/repo_test.go +++ b/internal/pkg/store/repo_test.go @@ -215,3 +215,35 @@ func TestStore_SyncRepo(t *testing.T) { } }) } + +func TestStore_Activate(t *testing.T) { + t.Run("Update webhook ID and owner ID.", func(t *testing.T) { + client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", + enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), + ) + defer client.Close() + + ctx := context.Background() + + r := client.Repo. + Create(). + SetID(1). + SetNamespace("octocat"). + SetName("Hello"). + SetDescription(""). + SaveX(ctx) + + r.WebhookID = 1 + r.OwnerID = 1 + + s := NewStore(client) + r, err := s.Activate(ctx, r) + if err != nil { + t.Fatalf("Activate returns an error: %s", err) + } + + if !r.Active { + t.Fatalf("Activate failed: %v", r) + } + }) +} diff --git a/internal/server/api/v1/repos/deployment.go b/internal/server/api/v1/repos/deployment.go index 27fd9a0b..aab95241 100644 --- a/internal/server/api/v1/repos/deployment.go +++ b/internal/server/api/v1/repos/deployment.go @@ -107,12 +107,6 @@ func (r *Repo) CreateDeployment(c *gin.Context) { return } - if err := env.Eval(&vo.EvalValues{}); err != nil { - r.log.Check(gb.GetZapLogLevel(err), "Failed to evaluate variables in the configuration.").Write(zap.Error(err)) - gb.ResponseWithError(c, err) - return - } - d, err := r.i.Deploy(ctx, u, re, &ent.Deployment{ Type: deployment.Type(p.Type), @@ -188,12 +182,6 @@ func (r *Repo) UpdateDeployment(c *gin.Context) { return } - if err := env.Eval(&vo.EvalValues{IsRollback: d.IsRollback}); err != nil { - r.log.Check(gb.GetZapLogLevel(err), "Failed to evaluate variables in the configuration.").Write(zap.Error(err)) - gb.ResponseWithError(c, err) - return - } - if d, err = r.i.DeployToRemote(ctx, u, re, d, env); err != nil { r.log.Check(gb.GetZapLogLevel(err), "It has failed to deploy to the remote.").Write(zap.Error(err)) gb.ResponseWithError(c, err) @@ -258,12 +246,6 @@ func (r *Repo) RollbackDeployment(c *gin.Context) { return } - if err := env.Eval(&vo.EvalValues{IsRollback: true}); err != nil { - r.log.Check(gb.GetZapLogLevel(err), "Failed to evaluate variables in the configuration.").Write(zap.Error(err)) - gb.ResponseWithError(c, err) - return - } - d, err = r.i.Deploy(ctx, u, re, &ent.Deployment{ Type: d.Type, diff --git a/internal/server/hooks/hook.go b/internal/server/hooks/hook.go index 79ce7cae..17b99b54 100644 --- a/internal/server/hooks/hook.go +++ b/internal/server/hooks/hook.go @@ -2,6 +2,7 @@ package hooks import ( "net/http" + "strings" "github.com/gin-gonic/gin" "github.com/gin-gonic/gin/binding" @@ -47,28 +48,56 @@ func NewHooks(c *ConfigHooks, i Interactor) *Hooks { // HandleHook handles deployment status event, basically, // it creates a new deployment status for the deployment. func (h *Hooks) HandleHook(c *gin.Context) { - if isFromGithub(c) { - h.handleGithubHook(c) + if !isFromGithub(c) { + gb.ResponseWithError(c, e.NewError(e.ErrorCodeInvalidRequest, nil)) return } - gb.ResponseWithError( - c, - e.NewError(e.ErrorCodeInvalidRequest, nil), - ) -} - -func (h *Hooks) handleGithubHook(c *gin.Context) { - ctx := c.Request.Context() + if h.WebhookSecret != "" { + if err := h.validateGitHubSignature(c); err != nil { + h.log.Warn("Failed to validate the signature.", zap.Error(err)) + gb.ResponseWithError( + c, + e.NewErrorWithMessage(e.ErrorCodeInvalidRequest, "It has failed to validate the signature.", err), + ) + return + } + } - if !isGithubDeploymentStatusEvent(c) { - c.String(http.StatusOK, "It is not deployment status event.") + switch eventName := c.GetHeader(headerGtihubEvent); eventName { + case "deployment_status": + h.handleGithubDeploymentEvent(c) + return + case "push": + h.handleGithubPushEvent(c) + return + default: + gb.ResponseWithError(c, e.NewError(e.ErrorCodeInvalidRequest, nil)) return } +} + +func (h *Hooks) validateGitHubSignature(c *gin.Context) error { + // Read the payload which was set by the "ShouldBindBodyWith" method call. + // https://github.com/gin-gonic/gin/issues/439 + var b interface{} + c.ShouldBindBodyWith(b, binding.JSON) + + var payload []byte + body, _ := c.Get(gin.BodyBytesKey) + payload = body.([]byte) + + sig := c.GetHeader(headerGithubSignature) + + return github.ValidateSignature(sig, payload, []byte(h.WebhookSecret)) +} + +func (h *Hooks) handleGithubDeploymentEvent(c *gin.Context) { + ctx := c.Request.Context() evt := &github.DeploymentStatusEvent{} if err := c.ShouldBindBodyWith(evt, binding.JSON); err != nil { - h.log.Warn("failed to bind the payload.", zap.Error(err)) + h.log.Warn("Failed to bind the payload.", zap.Error(err)) gb.ResponseWithError( c, e.NewErrorWithMessage(e.ErrorCodeInvalidRequest, "It has failed to bind the payload.", err), @@ -76,26 +105,6 @@ func (h *Hooks) handleGithubHook(c *gin.Context) { return } - // Validate Signature if the secret is exist. - if secret := h.WebhookSecret; secret != "" { - // Read the payload which was set by the "ShouldBindBodyWith" method call. - // https://github.com/gin-gonic/gin/issues/439 - var payload []byte - body, _ := c.Get(gin.BodyBytesKey) - payload = body.([]byte) - - sig := c.GetHeader(headerGithubSignature) - - if err := github.ValidateSignature(sig, payload, []byte(secret)); err != nil { - h.log.Warn("Failed to validate the signature.", zap.Error(err)) - gb.ResponseWithError( - c, - e.NewErrorWithMessage(e.ErrorCodeInvalidRequest, "It has failed to validate the signature.", err), - ) - return - } - } - // Convert event to the deployment status. ds := mapGithubDeploymentStatus(evt) @@ -141,12 +150,81 @@ func (h *Hooks) handleGithubHook(c *gin.Context) { gb.Response(c, http.StatusCreated, ds) } -func isFromGithub(c *gin.Context) bool { - return c.GetHeader(headerGithubDelivery) != "" +func (h *Hooks) handleGithubPushEvent(c *gin.Context) { + ctx := c.Request.Context() + + evt := &github.PushEvent{} + if err := c.ShouldBindBodyWith(evt, binding.JSON); err != nil { + h.log.Warn("Failed to bind the payload.", zap.Error(err)) + gb.ResponseWithError( + c, + e.NewErrorWithMessage(e.ErrorCodeInvalidRequest, "It has failed to bind the payload.", err), + ) + return + } + + r, err := h.i.FindRepoByID(ctx, *evt.Repo.ID) + if err != nil { + h.log.Check(gb.GetZapLogLevel(err), "Failed to find the repository.").Write(zap.Error(err)) + gb.ResponseWithError(c, err) + return + } else if r.Edges.Owner == nil { + h.log.Warn("The owner is not found.", zap.Int64("repo_id", r.ID)) + gb.ResponseWithError(c, + e.NewErrorWithMessage(e.ErrorCodeInternalError, "The owner is not found.", nil), + ) + return + } + + config, err := h.i.GetConfig(ctx, r.Edges.Owner, r) + if err != nil { + h.log.Check(gb.GetZapLogLevel(err), "Failed to find the configuration file.").Write(zap.Error(err)) + gb.ResponseWithError(c, err) + return + } + + for _, env := range config.Envs { + ok, err := env.IsAutoDeployOn(*evt.Ref) + if err != nil { + h.log.Warn("Failed to validate the ref is matched with 'auto_deploy_on'.", zap.Error(err)) + continue + } + if !ok { + continue + } + + typ, ref, err := parseGithubRef(*evt.Ref) + if err != nil { + h.log.Error("Failed to parse the ref.", zap.Error(err)) + continue + } + + h.log.Info("Trigger to deploy the ref.", zap.String("ref", *evt.Ref), zap.String("environment", env.Name)) + d := &ent.Deployment{ + Type: typ, + Ref: ref, + Env: env.Name, + } + d, err = h.i.Deploy(ctx, r.Edges.Owner, r, d, env) + if err != nil { + h.log.Error("Failed to deploy.", zap.Error(err)) + continue + } + + if _, err := h.i.CreateEvent(ctx, &ent.Event{ + Kind: event.KindDeployment, + Type: event.TypeCreated, + DeploymentID: d.ID, + }); err != nil { + h.log.Error("It has failed to create the event.", zap.Error(err)) + } + } + + c.Status(http.StatusOK) } -func isGithubDeploymentStatusEvent(c *gin.Context) bool { - return c.GetHeader(headerGtihubEvent) == "deployment_status" +func isFromGithub(c *gin.Context) bool { + return c.GetHeader(headerGithubDelivery) != "" } func mapGithubDeploymentStatus(e *github.DeploymentStatusEvent) *ent.DeploymentStatus { @@ -187,3 +265,20 @@ func mapGithubState(state string) deployment.Status { return deployment.StatusRunning } } + +func parseGithubRef(ref string) (deployment.Type, string, error) { + const ( + prefixBranchRef = "refs/heads/" + prefixTagRef = "refs/tags/" + ) + + if strings.HasPrefix(ref, prefixBranchRef) { + return deployment.TypeBranch, strings.TrimPrefix(ref, prefixBranchRef), nil + } + + if strings.HasPrefix(ref, prefixTagRef) { + return deployment.TypeTag, strings.TrimPrefix(ref, prefixTagRef), nil + } + + return "", "", e.NewErrorWithMessage(e.ErrorCodeInternalError, "The ref must be one of branch or tag.", nil) +} diff --git a/internal/server/hooks/hook_test.go b/internal/server/hooks/hook_test.go index a85e9f30..666fea10 100644 --- a/internal/server/hooks/hook_test.go +++ b/internal/server/hooks/hook_test.go @@ -8,6 +8,7 @@ import ( "os" "testing" + "github.com/AlekSi/pointer" "github.com/gin-gonic/gin" "github.com/golang/mock/gomock" "github.com/google/go-github/v32/github" @@ -15,6 +16,7 @@ import ( "github.com/gitploy-io/gitploy/ent" "github.com/gitploy-io/gitploy/ent/deployment" "github.com/gitploy-io/gitploy/internal/server/hooks/mock" + "github.com/gitploy-io/gitploy/vo" ) func init() { @@ -24,7 +26,7 @@ func init() { func TestHook_HandleHook(t *testing.T) { t.Run("Listen the deployment event.", func(t *testing.T) { e := &github.DeploymentStatusEvent{} - bytes, _ := ioutil.ReadFile("./testdata/github.hook.json") + bytes, _ := ioutil.ReadFile("./testdata/github.deployment_status.json") if err := json.Unmarshal(bytes, &e); err != nil { t.Fatalf("It has failed to unmarshal: %s", err) } @@ -83,7 +85,7 @@ func TestHook_HandleHook(t *testing.T) { r.POST("/hooks", h.HandleHook) // Build the Github webhook. - json, err := os.Open("./testdata/github.hook.json") + json, err := os.Open("./testdata/github.deployment_status.json") if err != nil { t.Errorf("It has failed to open the JSON file: %s", err) t.FailNow() @@ -102,4 +104,80 @@ func TestHook_HandleHook(t *testing.T) { t.FailNow() } }) + + t.Run("Listen the push event.", func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + m := mock.NewMockInteractor(ctrl) + + m. + EXPECT(). + FindRepoByID(gomock.Any(), gomock.Any()). + Return(&ent.Repo{ + Edges: ent.RepoEdges{ + Owner: &ent.User{}, + }, + }, nil) + + t.Log("Return the auto-deployment environment.") + m. + EXPECT(). + GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})). + Return(&vo.Config{ + Envs: []*vo.Env{ + { + Name: "dev", + }, + { + Name: "production", + AutoDeployOn: pointer.ToString("refs/tags/.*"), + }, + }, + }, nil) + + m. + EXPECT(). + Deploy( + gomock.Any(), + gomock.AssignableToTypeOf(&ent.User{}), + gomock.AssignableToTypeOf(&ent.Repo{}), + gomock.Eq(&ent.Deployment{ + Type: deployment.TypeTag, + Ref: "simple-tag", + Env: "production", + }), + gomock.AssignableToTypeOf(&vo.Env{}), + ). + Return(&ent.Deployment{}, nil) + + m. + EXPECT(). + CreateEvent(gomock.Any(), gomock.AssignableToTypeOf(&ent.Event{})). + Return(&ent.Event{}, nil) + + h := NewHooks(&ConfigHooks{}, m) + r := gin.New() + r.POST("/hooks", h.HandleHook) + + // Build the Github webhook. + json, err := os.Open("./testdata/github.push.json") + if err != nil { + t.Errorf("It has failed to open the JSON file: %s", err) + t.FailNow() + } + req, _ := http.NewRequest("POST", "/hooks", json) + req.Header.Set(headerGithubDelivery, "72d3162e") + req.Header.Set(headerGtihubEvent, "push") + + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + + // Validate the result. + if w.Code != http.StatusOK { + t.Errorf("w.Code = %d, wanted %d", w.Code, http.StatusOK) + t.Logf("w.Body = %v", w.Body) + t.FailNow() + } + }) } diff --git a/internal/server/hooks/interface.go b/internal/server/hooks/interface.go index a41f5427..5a5b3514 100644 --- a/internal/server/hooks/interface.go +++ b/internal/server/hooks/interface.go @@ -6,14 +6,18 @@ import ( "context" "github.com/gitploy-io/gitploy/ent" + "github.com/gitploy-io/gitploy/vo" ) type ( Interactor interface { + FindRepoByID(ctx context.Context, id int64) (*ent.Repo, error) FindDeploymentByUID(ctx context.Context, uid int64) (*ent.Deployment, error) SyncDeploymentStatus(ctx context.Context, ds *ent.DeploymentStatus) (*ent.DeploymentStatus, error) + Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) UpdateDeployment(ctx context.Context, d *ent.Deployment) (*ent.Deployment, error) ProduceDeploymentStatisticsOfRepo(ctx context.Context, r *ent.Repo, d *ent.Deployment) (*ent.DeploymentStatistics, error) CreateEvent(ctx context.Context, e *ent.Event) (*ent.Event, error) + GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.Config, error) } ) diff --git a/internal/server/hooks/mock/interactor.go b/internal/server/hooks/mock/interactor.go index b1fd7e60..5234e359 100644 --- a/internal/server/hooks/mock/interactor.go +++ b/internal/server/hooks/mock/interactor.go @@ -9,6 +9,7 @@ import ( reflect "reflect" ent "github.com/gitploy-io/gitploy/ent" + vo "github.com/gitploy-io/gitploy/vo" gomock "github.com/golang/mock/gomock" ) @@ -50,6 +51,21 @@ func (mr *MockInteractorMockRecorder) CreateEvent(ctx, e interface{}) *gomock.Ca return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateEvent", reflect.TypeOf((*MockInteractor)(nil).CreateEvent), ctx, e) } +// Deploy mocks base method. +func (m *MockInteractor) Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *vo.Env) (*ent.Deployment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "Deploy", ctx, u, r, d, env) + ret0, _ := ret[0].(*ent.Deployment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// Deploy indicates an expected call of Deploy. +func (mr *MockInteractorMockRecorder) Deploy(ctx, u, r, d, env interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Deploy", reflect.TypeOf((*MockInteractor)(nil).Deploy), ctx, u, r, d, env) +} + // FindDeploymentByUID mocks base method. func (m *MockInteractor) FindDeploymentByUID(ctx context.Context, uid int64) (*ent.Deployment, error) { m.ctrl.T.Helper() @@ -65,6 +81,36 @@ func (mr *MockInteractorMockRecorder) FindDeploymentByUID(ctx, uid interface{}) return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindDeploymentByUID", reflect.TypeOf((*MockInteractor)(nil).FindDeploymentByUID), ctx, uid) } +// FindRepoByID mocks base method. +func (m *MockInteractor) FindRepoByID(ctx context.Context, id int64) (*ent.Repo, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "FindRepoByID", ctx, id) + ret0, _ := ret[0].(*ent.Repo) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FindRepoByID indicates an expected call of FindRepoByID. +func (mr *MockInteractorMockRecorder) FindRepoByID(ctx, id interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FindRepoByID", reflect.TypeOf((*MockInteractor)(nil).FindRepoByID), ctx, id) +} + +// GetConfig mocks base method. +func (m *MockInteractor) GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*vo.Config, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "GetConfig", ctx, u, r) + ret0, _ := ret[0].(*vo.Config) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetConfig indicates an expected call of GetConfig. +func (mr *MockInteractorMockRecorder) GetConfig(ctx, u, r interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetConfig", reflect.TypeOf((*MockInteractor)(nil).GetConfig), ctx, u, r) +} + // ProduceDeploymentStatisticsOfRepo mocks base method. func (m *MockInteractor) ProduceDeploymentStatisticsOfRepo(ctx context.Context, r *ent.Repo, d *ent.Deployment) (*ent.DeploymentStatistics, error) { m.ctrl.T.Helper() diff --git a/internal/server/hooks/testdata/github.hook.json b/internal/server/hooks/testdata/github.deployment_status.json similarity index 100% rename from internal/server/hooks/testdata/github.hook.json rename to internal/server/hooks/testdata/github.deployment_status.json diff --git a/internal/server/hooks/testdata/github.push.json b/internal/server/hooks/testdata/github.push.json new file mode 100644 index 00000000..f505475c --- /dev/null +++ b/internal/server/hooks/testdata/github.push.json @@ -0,0 +1,156 @@ +{ + "ref": "refs/tags/simple-tag", + "before": "0000000000000000000000000000000000000000", + "after": "6113728f27ae82c7b1a177c8d03f9e96e0adf246", + "created": true, + "deleted": false, + "forced": false, + "base_ref": "refs/heads/main", + "compare": "https://github.com/Codertocat/Hello-World/compare/simple-tag", + "commits": [], + "head_commit": { + "id": "6113728f27ae82c7b1a177c8d03f9e96e0adf246", + "tree_id": "4b825dc642cb6eb9a060e54bf8d69288fbee4904", + "distinct": true, + "message": "Adding a .gitignore file", + "timestamp": "2019-05-15T15:20:41Z", + "url": "https://github.com/Codertocat/Hello-World/commit/6113728f27ae82c7b1a177c8d03f9e96e0adf246", + "author": { + "name": "Codertocat", + "email": "21031067+Codertocat@users.noreply.github.com", + "username": "Codertocat" + }, + "committer": { + "name": "Codertocat", + "email": "21031067+Codertocat@users.noreply.github.com", + "username": "Codertocat" + }, + "added": [ + ".gitignore" + ], + "removed": [], + "modified": [] + }, + "repository": { + "id": 186853002, + "node_id": "MDEwOlJlcG9zaXRvcnkxODY4NTMwMDI=", + "name": "Hello-World", + "full_name": "Codertocat/Hello-World", + "private": false, + "owner": { + "name": "Codertocat", + "email": "21031067+Codertocat@users.noreply.github.com", + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/Codertocat/Hello-World", + "description": null, + "fork": false, + "url": "https://github.com/Codertocat/Hello-World", + "forks_url": "https://api.github.com/repos/Codertocat/Hello-World/forks", + "keys_url": "https://api.github.com/repos/Codertocat/Hello-World/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/Codertocat/Hello-World/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/Codertocat/Hello-World/teams", + "hooks_url": "https://api.github.com/repos/Codertocat/Hello-World/hooks", + "issue_events_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/events{/number}", + "events_url": "https://api.github.com/repos/Codertocat/Hello-World/events", + "assignees_url": "https://api.github.com/repos/Codertocat/Hello-World/assignees{/user}", + "branches_url": "https://api.github.com/repos/Codertocat/Hello-World/branches{/branch}", + "tags_url": "https://api.github.com/repos/Codertocat/Hello-World/tags", + "blobs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/Codertocat/Hello-World/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/Codertocat/Hello-World/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/Codertocat/Hello-World/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/Codertocat/Hello-World/statuses/{sha}", + "languages_url": "https://api.github.com/repos/Codertocat/Hello-World/languages", + "stargazers_url": "https://api.github.com/repos/Codertocat/Hello-World/stargazers", + "contributors_url": "https://api.github.com/repos/Codertocat/Hello-World/contributors", + "subscribers_url": "https://api.github.com/repos/Codertocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/Codertocat/Hello-World/subscription", + "commits_url": "https://api.github.com/repos/Codertocat/Hello-World/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/Codertocat/Hello-World/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/Codertocat/Hello-World/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/Codertocat/Hello-World/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/Codertocat/Hello-World/contents/{+path}", + "compare_url": "https://api.github.com/repos/Codertocat/Hello-World/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/Codertocat/Hello-World/merges", + "archive_url": "https://api.github.com/repos/Codertocat/Hello-World/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/Codertocat/Hello-World/downloads", + "issues_url": "https://api.github.com/repos/Codertocat/Hello-World/issues{/number}", + "pulls_url": "https://api.github.com/repos/Codertocat/Hello-World/pulls{/number}", + "milestones_url": "https://api.github.com/repos/Codertocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/Codertocat/Hello-World/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/Codertocat/Hello-World/labels{/name}", + "releases_url": "https://api.github.com/repos/Codertocat/Hello-World/releases{/id}", + "deployments_url": "https://api.github.com/repos/Codertocat/Hello-World/deployments", + "created_at": 1557933565, + "updated_at": "2019-05-15T15:20:41Z", + "pushed_at": 1557933657, + "git_url": "git://github.com/Codertocat/Hello-World.git", + "ssh_url": "git@github.com:Codertocat/Hello-World.git", + "clone_url": "https://github.com/Codertocat/Hello-World.git", + "svn_url": "https://github.com/Codertocat/Hello-World", + "homepage": null, + "size": 0, + "stargazers_count": 0, + "watchers_count": 0, + "language": "Ruby", + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": true, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "master", + "stargazers": 0, + "master_branch": "master" + }, + "pusher": { + "name": "Codertocat", + "email": "21031067+Codertocat@users.noreply.github.com" + }, + "sender": { + "login": "Codertocat", + "id": 21031067, + "node_id": "MDQ6VXNlcjIxMDMxMDY3", + "avatar_url": "https://avatars1.githubusercontent.com/u/21031067?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/Codertocat", + "html_url": "https://github.com/Codertocat", + "followers_url": "https://api.github.com/users/Codertocat/followers", + "following_url": "https://api.github.com/users/Codertocat/following{/other_user}", + "gists_url": "https://api.github.com/users/Codertocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/Codertocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/Codertocat/subscriptions", + "organizations_url": "https://api.github.com/users/Codertocat/orgs", + "repos_url": "https://api.github.com/users/Codertocat/repos", + "events_url": "https://api.github.com/users/Codertocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/Codertocat/received_events", + "type": "User", + "site_admin": false + } +} \ No newline at end of file diff --git a/internal/server/slack/deploy.go b/internal/server/slack/deploy.go index 7d1bc9de..bf25c168 100644 --- a/internal/server/slack/deploy.go +++ b/internal/server/slack/deploy.go @@ -287,12 +287,6 @@ func (s *Slack) interactDeploy(c *gin.Context) { return } - if err := env.Eval(&vo.EvalValues{}); err != nil { - postBotMessage(cu, "It has failed to eval variables in the config.") - c.Status(http.StatusOK) - return - } - d, err := s.i.Deploy(ctx, cu.Edges.User, cb.Edges.Repo, &ent.Deployment{ Type: deployment.Type(sm.Type), diff --git a/internal/server/slack/rollback.go b/internal/server/slack/rollback.go index 9b29dc3e..ef378823 100644 --- a/internal/server/slack/rollback.go +++ b/internal/server/slack/rollback.go @@ -226,12 +226,6 @@ func (s *Slack) interactRollback(c *gin.Context) { return } - if err := env.Eval(&vo.EvalValues{IsRollback: true}); err != nil { - postBotMessage(cu, "It has failed to eval variables in the config.") - c.Status(http.StatusOK) - return - } - d, err = s.i.Deploy(ctx, cu.Edges.User, cb.Edges.Repo, &ent.Deployment{ Type: deployment.Type(d.Type), Ref: d.Ref, diff --git a/vo/config.go b/vo/config.go index 38d76f52..f2d0d055 100644 --- a/vo/config.go +++ b/vo/config.go @@ -29,6 +29,8 @@ type ( // DeployableRef validates the ref is deployable or not. DeployableRef *string `json:"deployable_ref" yaml:"deployable_ref"` + // AutoDeployOn deploys automatically when the pattern is matched. + AutoDeployOn *string `json:"auto_deploy_on" yaml:"auto_deploy_on"` // Review is the configuration of Review, // It is disabled when it is empty. @@ -105,6 +107,20 @@ func (e *Env) IsDeployableRef(ref string) (bool, error) { return matched, nil } +// IsAutoDeployOn validate the ref is matched with 'auto_deploy_on'. +func (e *Env) IsAutoDeployOn(ref string) (bool, error) { + if e.AutoDeployOn == nil { + return false, nil + } + + matched, err := regexp.MatchString(*e.AutoDeployOn, ref) + if err != nil { + return false, eutil.NewError(eutil.ErrorCodeConfigRegexpError, err) + } + + return matched, nil +} + // HasReview check whether the review is enabled or not. func (e *Env) HasReview() bool { return e.Review != nil && e.Review.Enabled