diff --git a/.github/workflows/backend-pr-ci.yml b/.github/workflows/backend-pr-ci.yml new file mode 100644 index 0000000..64f18b9 --- /dev/null +++ b/.github/workflows/backend-pr-ci.yml @@ -0,0 +1,101 @@ +name: backend-pr-ci + +on: + pull_request: + paths: + - "backend/**" + - ".github/workflows/backend-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: backend-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +env: + GO_VERSION: "1.24" + +jobs: + lint: + name: Lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Install golangci-lint + run: go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest + + - name: Run golangci-lint + run: golangci-lint run --timeout=5m + + build: + name: Build + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Build WASM + run: GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o checklist.wasm . + + test: + name: Tests + runs-on: ubuntu-latest + timeout-minutes: 15 + + defaults: + run: + working-directory: backend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: backend/go.sum + + - name: Run tests (race detector) + run: go test -race -timeout 60s -coverprofile=coverage.out $(go list ./...) + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: backend/coverage.out + retention-days: 7 diff --git a/.github/workflows/frontend-pr-ci.yml b/.github/workflows/frontend-pr-ci.yml new file mode 100644 index 0000000..8798bad --- /dev/null +++ b/.github/workflows/frontend-pr-ci.yml @@ -0,0 +1,53 @@ +name: frontend-pr-ci + +on: + pull_request: + paths: + - "frontend/**" + - ".github/workflows/frontend-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: frontend-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + web-quality: + name: Typecheck and build frontend + runs-on: ubuntu-latest + timeout-minutes: 20 + + defaults: + run: + working-directory: frontend + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.23" + + - name: Cache Bun packages + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-${{ hashFiles('frontend/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build production bundle + run: bun run build diff --git a/.github/workflows/mcp-pr-ci.yml b/.github/workflows/mcp-pr-ci.yml new file mode 100644 index 0000000..1429198 --- /dev/null +++ b/.github/workflows/mcp-pr-ci.yml @@ -0,0 +1,53 @@ +name: mcp-pr-ci + +on: + pull_request: + paths: + - "mcp/**" + - ".github/workflows/mcp-pr-ci.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: mcp-pr-ci-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + mcp-quality: + name: Typecheck and build MCP server + runs-on: ubuntu-latest + timeout-minutes: 10 + + defaults: + run: + working-directory: mcp + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: "1.2.23" + + - name: Cache Bun packages + uses: actions/cache@v4 + with: + path: ~/.bun/install/cache + key: ${{ runner.os }}-bun-mcp-${{ hashFiles('mcp/bun.lock') }} + restore-keys: | + ${{ runner.os }}-bun-mcp- + + - name: Install dependencies + run: bun install --frozen-lockfile + + - name: Typecheck + run: bun run typecheck + + - name: Build + run: bun run build diff --git a/backend/checklists.go b/backend/checklists.go index 0d4c4aa..6c7c74a 100644 --- a/backend/checklists.go +++ b/backend/checklists.go @@ -162,6 +162,8 @@ func (p *checklistPlugin) createChecklist(req *plugin.Request, res *plugin.Respo CreatedAt: now, UpdatedAt: now, } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist.created", + map[string]any{"title": b.Title, "_description": "created checklist: \"" + b.Title + "\""}) created(res, cl) } @@ -230,6 +232,8 @@ func (p *checklistPlugin) updateChecklist(req *plugin.Request, res *plugin.Respo CreatedAt: sc.str("created_at"), UpdatedAt: now, } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist.updated", + map[string]any{"title": *b.Title, "_description": "renamed checklist to \"" + *b.Title + "\""}) ok(res, cl) } @@ -243,6 +247,23 @@ func (p *checklistPlugin) deleteChecklist(req *plugin.Request, res *plugin.Respo return } + // Fetch title before deletion so it can be included in the activity record. + titleResult, err := p.db.Query( + `SELECT title FROM task_checklists WHERE id = $1 AND task_id = $2`, + checklistID, taskID, + ) + if err != nil { + p.log.Error("deleteChecklist title fetch: " + err.Error()) + res.Error(500, "failed to delete checklist") + return + } + if len(titleResult.Rows) == 0 { + res.Error(404, "checklist not found") + return + } + titleSC := newRowScanner(titleResult.Columns, titleResult.Rows[0]) + clTitle := titleSC.str("title") + affected, err := p.db.Exec( `DELETE FROM task_checklists WHERE id = $1 AND task_id = $2`, checklistID, taskID, @@ -256,6 +277,8 @@ func (p *checklistPlugin) deleteChecklist(req *plugin.Request, res *plugin.Respo res.Error(404, "checklist not found") return } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist.deleted", + map[string]any{"title": clTitle, "_description": "deleted checklist: \"" + clTitle + "\""}) res.NoContent() } diff --git a/backend/go.mod b/backend/go.mod index 85fad87..80a998f 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -2,6 +2,6 @@ module github.com/Paca-AI/first-party/checklist go 1.24 -require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3 +require github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5 require github.com/google/uuid v1.6.0 diff --git a/backend/go.sum b/backend/go.sum index 664e182..344bb56 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,4 @@ -github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3 h1:2MFHmdVIKVo1/Cyo7GL3S/LsAFxpsqhJSAbTao+xNEw= -github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.3/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw= +github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5 h1:xrEPnJLM2sdwtY/wRaGOdfeDPv5zPUpR8QZJ/w2nQVU= +github.com/Paca-AI/plugin-sdk-go v0.2.0-rc.5/go.mod h1:5WeC6cSEf2wM1ovICZbDaVky9oi5id/Qpdfc5LDAQnw= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/backend/items.go b/backend/items.go index 5c87945..95cd9a1 100644 --- a/backend/items.go +++ b/backend/items.go @@ -67,6 +67,8 @@ func (p *checklistPlugin) createItem(req *plugin.Request, res *plugin.Response) CreatedAt: now, UpdatedAt: now, } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist_item.created", + map[string]any{"text": b.Title, "_description": "added checklist item: \"" + b.Title + "\""}) created(res, item) } @@ -161,6 +163,26 @@ func (p *checklistPlugin) updateItem(req *plugin.Request, res *plugin.Response) CreatedAt: createdAt, UpdatedAt: now, } + // Build change list for the activity record. + changes := []map[string]any{} + oldTitle := sc.str("title") + if b.Title != nil && *b.Title != oldTitle { + changes = append(changes, map[string]any{"field": "title", "old": oldTitle, "new": *b.Title}) + } + oldChecked := sc.boolVal("is_checked") + if b.IsChecked != nil && *b.IsChecked != oldChecked { + changes = append(changes, map[string]any{"field": "is_checked", "old": oldChecked, "new": *b.IsChecked}) + } + oldAssignee := sc.strPtr("assignee_id") + if b.AssigneeID != nil && (oldAssignee == nil || *oldAssignee != *b.AssigneeID) { + var oldAssigneeActivity any + if oldAssignee != nil { + oldAssigneeActivity = *oldAssignee + } + changes = append(changes, map[string]any{"field": "assignee_id", "old": oldAssigneeActivity, "new": *b.AssigneeID}) + } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist_item.updated", + map[string]any{"text": updTitle, "changes": changes, "_description": "updated checklist item: \"" + updTitle + "\""}) ok(res, item) } @@ -175,6 +197,23 @@ func (p *checklistPlugin) deleteItem(req *plugin.Request, res *plugin.Response) return } + // Fetch item title before deletion for activity record. + titleResult, err := p.db.Query( + `SELECT title FROM task_checklist_items WHERE id = $1 AND checklist_id = $2`, + itemID, checklistID, + ) + if err != nil { + p.log.Error("deleteItem title fetch: " + err.Error()) + res.Error(500, "failed to delete item") + return + } + if len(titleResult.Rows) == 0 { + res.Error(404, "item not found") + return + } + itemTitleSC := newRowScanner(titleResult.Columns, titleResult.Rows[0]) + itemTitle := itemTitleSC.str("title") + affected, err := p.db.Exec( `DELETE FROM task_checklist_items WHERE id = $1 AND checklist_id = $2`, itemID, checklistID, @@ -188,5 +227,7 @@ func (p *checklistPlugin) deleteItem(req *plugin.Request, res *plugin.Response) res.Error(404, "item not found") return } + plugin.RecordActivity(taskID, projectID, req.Caller.UserID, "task.checklist_item.deleted", + map[string]any{"text": itemTitle, "_description": "removed checklist item: \"" + itemTitle + "\""}) res.NoContent() } diff --git a/plugin.json b/plugin.json index 02e39ae..d221b69 100644 --- a/plugin.json +++ b/plugin.json @@ -2,7 +2,7 @@ "id": "com.paca.checklist", "displayName": "Checklist", "description": "Adds named checklists with checkable items to tasks.", - "version": "0.1.0", + "version": "0.2.1", "permissions": ["db.read", "db.write", "events.subscribe"], "backend": { "eventSubscriptions": ["task.deleted"],