Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.CR_PAT }}
- name: Build and push
uses: docker/build-push-action@v6
with:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ jobs:
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
password: ${{ secrets.CR_PAT }}

- name: Build and push Docker image
uses: docker/build-push-action@v6
Expand Down
4 changes: 4 additions & 0 deletions internal/commands/help.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ func handleHelp(ctx *Context) error {
cmds += " /standup team set timezone <tz>\n"
cmds += " /standup submit — start daily standup\n"
cmds += " /standup status — check submission status\n"
cmds += " /standup cancel — cancel in-progress standup\n"
cmds += " /standup list — list your teams\n"
cmds += " /standup report — view latest report\n"
cmds += " /standup help — show this message"

Expand All @@ -31,6 +33,8 @@ func sendHelpForCommand(ctx *Context, cmd string) error {
help := map[string]string{
"submit": "Start your daily standup. The bot will DM you with questions.",
"status": "Check if you've submitted your standup today.",
"cancel": "Cancel an in-progress standup submission.",
"list": "List teams you belong to.",
"report": "Post the latest standup report to the team channel.",
}

Expand Down
7 changes: 7 additions & 0 deletions internal/commands/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,13 @@ func (r *Registry) checkPermission(ctx *Context, required Permission) error {
if ctx.Username == ctx.Config.MainAdmin {
return nil
}
teams, err := ctx.Store.GetTeamsForUser(ctx.UserID)
if err != nil {
return fmt.Errorf("Error checking membership.")
}
if len(teams) == 0 {
return fmt.Errorf("You are not a member of any team.")
}
return nil
default:
return nil
Expand Down
139 changes: 124 additions & 15 deletions internal/commands/standup.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,60 @@ func RegisterStandupCommands(r *Registry) {
r.Register("submit", handleSubmit, PermissionMember)
r.Register("status", handleStatus, PermissionMember)
r.Register("report", handleReport, PermissionAny)
r.Register("cancel", handleCancel, PermissionMember)
r.Register("list", handleList, PermissionAny)
}

func resolveSubmitTeam(ctx *Context, teams []*store.Team) (*store.Team, string, error) {
if len(teams) == 1 {
return teams[0], "", nil
}

for i, a := range ctx.Args {
if (a == "--team" || a == "-t") && i+1 < len(ctx.Args) {
name := ctx.Args[i+1]
for _, t := range teams {
if strings.EqualFold(t.Name, name) {
return t, "", nil
}
}
return nil, "", fmt.Errorf("You are not a member of team %q.", name)
}
}

var names []string
for _, t := range teams {
names = append(names, t.Name)
}
return nil, "", fmt.Errorf(
"You are in multiple teams. Use `--team \"<name>\"`: %s",
strings.Join(names, ", "),
)
}

func activeOrNewSession(ctx *Context, teamID, teamName string) (string, error) {
date := time.Now().Format("2006-01-02")

session, err := ctx.Store.GetActiveSession(teamID, date)
if err == nil {
submitted, _ := ctx.Store.HasSubmitted(session.ID, ctx.UserID)
if submitted {
return "", fmt.Errorf("You have already submitted your standup for %s today.", teamName)
}
return session.ID, nil
}

sessionID := fmt.Sprintf("sess-%d-%s", time.Now().UnixMilli(), teamID)
session = &store.StandupSession{
ID: sessionID,
TeamID: teamID,
Date: date,
Status: "open",
}
if err := ctx.Store.CreateSession(session); err != nil {
return "", fmt.Errorf("Failed to create session: %v", err)
}
return sessionID, nil
}

func handleSubmit(ctx *Context) error {
Expand All @@ -25,26 +79,20 @@ func handleSubmit(ctx *Context) error {
"You are not a member of any team.")
}

team, _, err := resolveSubmitTeam(ctx, teams)
if err != nil {
return send(ctx.Messenger, ctx.RoomID, err.Error())
}

dmRoomID, err := ensureDMRoom(ctx)
if err != nil {
return send(ctx.Messenger, ctx.RoomID,
fmt.Sprintf("Could not start DM: %v", err))
}

team := teams[0]
date := time.Now().Format("2006-01-02")

sessionID := fmt.Sprintf("sess-%d-%s", time.Now().UnixMilli(), team.ID)

session := &store.StandupSession{
ID: sessionID,
TeamID: team.ID,
Date: date,
Status: "open",
}
if err := ctx.Store.CreateSession(session); err != nil {
return send(ctx.Messenger, ctx.RoomID,
fmt.Sprintf("Failed to create session: %v", err))
sessionID, err := activeOrNewSession(ctx, team.ID, team.Name)
if err != nil {
return send(ctx.Messenger, ctx.RoomID, err.Error())
}

questions := strings.Split(team.Questions, "|")
Expand All @@ -68,8 +116,12 @@ func handleStatus(ctx *Context) error {
return send(ctx.Messenger, ctx.RoomID, "You are not a member of any team.")
}

team, _, err := resolveSubmitTeam(ctx, teams)
if err != nil {
return send(ctx.Messenger, ctx.RoomID, err.Error())
}

date := time.Now().Format("2006-01-02")
team := teams[0]

sessID := findSessionID(ctx, team.ID, date)
submitted, total, err := ctx.Store.GetSessionStatus(team.ID, date)
Expand All @@ -90,6 +142,63 @@ func handleStatus(ctx *Context) error {
team.Name, statusLine, submitted, total))
}

func handleCancel(ctx *Context) error {
conv, ok := ctx.ConvState.GetConversation(ctx.UserID)
if !ok {
return send(ctx.Messenger, ctx.RoomID,
"You don't have an active standup submission.")
}

cancelTo := conv.RoomID
if !ctx.IsDM {
cancelTo = ctx.RoomID
}

partial, err := ctx.ConvState.Cancel(ctx.UserID)
if err != nil {
return send(ctx.Messenger, cancelTo,
"You don't have an active standup submission.")
}

if partial == "" {
partial = "No answers recorded."
}

return send(ctx.Messenger, cancelTo,
fmt.Sprintf("❌ *Standup cancelled.* Your partial answers:\n%s", partial))
}

func handleList(ctx *Context) error {
var teams []*store.Team
var err error

if ctx.Username == ctx.Config.MainAdmin {
teams, err = ctx.Store.ListTeams()
} else {
teams, err = ctx.Store.GetTeamsForUser(ctx.UserID)
}
if err != nil {
return send(ctx.Messenger, ctx.RoomID,
"Error looking up teams.")
}
if len(teams) == 0 {
return send(ctx.Messenger, ctx.RoomID, "No teams found.")
}

var lines []string
for _, t := range teams {
isLead, _ := ctx.Store.IsTeamLead(t.ID, ctx.UserID)
role := ""
if isLead {
role = " (lead)"
}
lines = append(lines, fmt.Sprintf(" • **%s**%s", t.Name, role))
}

return send(ctx.Messenger, ctx.RoomID,
fmt.Sprintf("Teams:\n%s", strings.Join(lines, "\n")))
}

func handleReport(ctx *Context) error {
team, err := resolveTeam(ctx)
if err != nil {
Expand Down
61 changes: 61 additions & 0 deletions internal/commands/standup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -201,6 +201,67 @@ func TestStandupReportNonLeadDenied(t *testing.T) {
}
}

func TestStandupCancelNoConversation(t *testing.T) {
h := newStandupHarness(t)

h.dispatch("/standup cancel")

msg := strings.Join(h.msgr.sent, " ")
if !contains(msg, "don't have") && !contains(msg, "active") {
t.Logf("Cancel message: %s", msg)
}
}

func TestStandupCancelActive(t *testing.T) {
h := newStandupHarness(t)

h.dispatch("/standup submit")

h.dispatch("/standup cancel")

msg := strings.Join(h.msgr.sent, " ")
if !contains(msg, "cancelled") {
t.Errorf("expected cancellation message, got: %s", msg)
}

_, ok := h.convMgr.GetConversation("alice-id")
if ok {
t.Error("conversation should be cancelled")
}
}

func TestStandupListTeams(t *testing.T) {
h := newStandupHarness(t)

h.dispatch("/standup list")

msg := strings.Join(h.msgr.sent, " ")
if !contains(msg, "Eng") {
t.Errorf("expected team name in list, got: %s", msg)
}
}

func TestStandupSubmitDuplicateSession(t *testing.T) {
h := newStandupHarness(t)

now := time.Now().Format("2006-01-02")
_ = h.s.CreateSession(&store.StandupSession{
ID: "sess-existing", TeamID: "t1", Date: now, Status: "open",
})
_ = h.s.SubmitResponse(&store.StandupResponse{
ID: "resp-existing", SessionID: "sess-existing", UserID: "alice-id",
Username: "alice", Answers: "Fine|Coding|None",
SubmittedAt: time.Now(),
})

h.dispatch("/standup submit")

msg := strings.Join(h.msgr.sent, " ")
if !contains(msg, "already submitted") {
t.Errorf("expected duplicate rejection, got: %s", msg)
}
}

func TestStandupConversationFlow(t *testing.T) {
h := newStandupHarness(t)

Expand Down
Loading