Unverified Commit 12e2205c authored by memoclaw's avatar memoclaw Committed by GitHub

chore(backend): update Go toolchain and dependencies (#5730)

parent 6f5f0d94
...@@ -15,7 +15,7 @@ concurrency: ...@@ -15,7 +15,7 @@ concurrency:
cancel-in-progress: true cancel-in-progress: true
env: env:
GO_VERSION: "1.25.7" GO_VERSION: "1.26.1"
jobs: jobs:
static-checks: static-checks:
...@@ -40,7 +40,7 @@ jobs: ...@@ -40,7 +40,7 @@ jobs:
- name: Run golangci-lint - name: Run golangci-lint
uses: golangci/golangci-lint-action@v9 uses: golangci/golangci-lint-action@v9
with: with:
version: v2.4.0 version: v2.11.3
args: --timeout=3m args: --timeout=3m
tests: tests:
......
...@@ -9,7 +9,7 @@ on: ...@@ -9,7 +9,7 @@ on:
# Environment variables for build configuration # Environment variables for build configuration
env: env:
GO_VERSION: "1.25.7" GO_VERSION: "1.26.1"
NODE_VERSION: "24" NODE_VERSION: "24"
PNPM_VERSION: "10" PNPM_VERSION: "10"
ARTIFACT_RETENTION_DAYS: 60 ARTIFACT_RETENTION_DAYS: 60
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Self-hosted note-taking tool. Go 1.25 backend (Echo v5, Connect RPC + gRPC-Gateway), React 18 + TypeScript 5.9 + Vite 7 frontend, Protocol Buffers API, SQLite/MySQL/PostgreSQL. Self-hosted note-taking tool. Go 1.26 backend (Echo v5, Connect RPC + gRPC-Gateway), React 18 + TypeScript 5.9 + Vite 7 frontend, Protocol Buffers API, SQLite/MySQL/PostgreSQL.
## Commands ## Commands
...@@ -96,7 +96,7 @@ web/src/ ...@@ -96,7 +96,7 @@ web/src/
## CI/CD ## CI/CD
- **backend-tests.yml:** Go 1.25.7, golangci-lint v2.4.0, tests parallelized by group (store, server, plugin, other) - **backend-tests.yml:** Go 1.26.1, golangci-lint v2.4.0, tests parallelized by group (store, server, plugin, other)
- **frontend-tests.yml:** Node 24, pnpm 10, lint + build - **frontend-tests.yml:** Node 24, pnpm 10, lint + build
- **proto-linter.yml:** buf lint + format check - **proto-linter.yml:** buf lint + format check
- **Docker:** Multi-stage (`scripts/Dockerfile`), Alpine 3.21, non-root user, port 5230, multi-arch (amd64/arm64/arm/v7) - **Docker:** Multi-stage (`scripts/Dockerfile`), Alpine 3.21, non-root user, port 5230, multi-arch (amd64/arm64/arm/v7)
This diff is collapsed.
This diff is collapsed.
package version package version
import ( import (
"sort" "slices"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"golang.org/x/mod/semver"
) )
func TestIsVersionGreaterOrEqualThan(t *testing.T) { func TestIsVersionGreaterOrEqualThan(t *testing.T) {
...@@ -97,7 +98,9 @@ func TestSortVersion(t *testing.T) { ...@@ -97,7 +98,9 @@ func TestSortVersion(t *testing.T) {
}, },
} }
for _, test := range tests { for _, test := range tests {
sort.Sort(SortVersion(test.versionList)) slices.SortFunc(test.versionList, func(a, b string) int {
return semver.Compare("v"+a, "v"+b)
})
assert.Equal(t, test.versionList, test.want) assert.Equal(t, test.versionList, test.want)
} }
} }
...@@ -10,6 +10,20 @@ import ( ...@@ -10,6 +10,20 @@ import (
"time" "time"
) )
func waitFor(t *testing.T, timeout time.Duration, fn func() bool) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if fn() {
return
}
time.Sleep(time.Millisecond)
}
t.Fatal("condition not met before timeout")
}
func appendingJob(slice *[]int, value int) Job { func appendingJob(slice *[]int, value int) Job {
var m sync.Mutex var m sync.Mutex
return FuncJob(func() { return FuncJob(func() {
...@@ -104,7 +118,8 @@ func TestChainDelayIfStillRunning(t *testing.T) { ...@@ -104,7 +118,8 @@ func TestChainDelayIfStillRunning(t *testing.T) {
var j countJob var j countJob
wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j) wrappedJob := NewChain(DelayIfStillRunning(DiscardLogger)).Then(&j)
go wrappedJob.Run() go wrappedJob.Run()
time.Sleep(2 * time.Millisecond) // Give the job 2ms to complete.
waitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 1 })
if c := j.Done(); c != 1 { if c := j.Done(); c != 1 {
t.Errorf("expected job run once, immediately, got %d", c) t.Errorf("expected job run once, immediately, got %d", c)
} }
...@@ -118,7 +133,8 @@ func TestChainDelayIfStillRunning(t *testing.T) { ...@@ -118,7 +133,8 @@ func TestChainDelayIfStillRunning(t *testing.T) {
time.Sleep(time.Millisecond) time.Sleep(time.Millisecond)
go wrappedJob.Run() go wrappedJob.Run()
}() }()
time.Sleep(3 * time.Millisecond) // Give both jobs 3ms to complete.
waitFor(t, 100*time.Millisecond, func() bool { return j.Done() == 2 })
if c := j.Done(); c != 2 { if c := j.Done(); c != 2 {
t.Errorf("expected job run twice, immediately, got %d", c) t.Errorf("expected job run twice, immediately, got %d", c)
} }
...@@ -134,16 +150,13 @@ func TestChainDelayIfStillRunning(t *testing.T) { ...@@ -134,16 +150,13 @@ func TestChainDelayIfStillRunning(t *testing.T) {
go wrappedJob.Run() go wrappedJob.Run()
}() }()
// After 5ms, the first job is still in progress, and the second job was waitFor(t, 100*time.Millisecond, func() bool { return j.Started() == 1 })
// run but should be waiting for it to finish.
time.Sleep(5 * time.Millisecond)
started, done := j.Started(), j.Done() started, done := j.Started(), j.Done()
if started != 1 || done != 0 { if done != 0 {
t.Error("expected first job started, but not finished, got", started, done) t.Error("expected first job started, but not finished, got", started, done)
} }
// Verify that the second job completes. waitFor(t, 200*time.Millisecond, func() bool { return j.Done() == 2 })
time.Sleep(25 * time.Millisecond)
started, done = j.Started(), j.Done() started, done = j.Started(), j.Done()
if started != 2 || done != 2 { if started != 2 || done != 2 {
t.Error("expected both jobs done, got", started, done) t.Error("expected both jobs done, got", started, done)
......
...@@ -2,7 +2,7 @@ package cron ...@@ -2,7 +2,7 @@ package cron
import ( import (
"context" "context"
"sort" "slices"
"sync" "sync"
"time" "time"
) )
...@@ -74,25 +74,6 @@ type Entry struct { ...@@ -74,25 +74,6 @@ type Entry struct {
// Valid returns true if this is not the zero entry. // Valid returns true if this is not the zero entry.
func (e Entry) Valid() bool { return e.ID != 0 } func (e Entry) Valid() bool { return e.ID != 0 }
// byTime is a wrapper for sorting the entry array by time
// (with zero time at the end).
type byTime []*Entry
func (s byTime) Len() int { return len(s) }
func (s byTime) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
func (s byTime) Less(i, j int) bool {
// Two zero times should return false.
// Otherwise, zero is "greater" than any other time.
// (To sort it at the end of the list.)
if s[i].Next.IsZero() {
return false
}
if s[j].Next.IsZero() {
return true
}
return s[i].Next.Before(s[j].Next)
}
// New returns a new Cron job runner, modified by the given options. // New returns a new Cron job runner, modified by the given options.
// //
// Available Settings // Available Settings
...@@ -248,7 +229,22 @@ func (c *Cron) runScheduler() { ...@@ -248,7 +229,22 @@ func (c *Cron) runScheduler() {
for { for {
// Determine the next entry to run. // Determine the next entry to run.
sort.Sort(byTime(c.entries)) slices.SortFunc(c.entries, func(a, b *Entry) int {
switch {
case a.Next.IsZero() && b.Next.IsZero():
return 0
case a.Next.IsZero():
return 1
case b.Next.IsZero():
return -1
case a.Next.Before(b.Next):
return -1
case b.Next.Before(a.Next):
return 1
default:
return 0
}
})
var timer *time.Timer var timer *time.Timer
if len(c.entries) == 0 || c.entries[0].Next.IsZero() { if len(c.entries) == 0 || c.entries[0].Next.IsZero() {
......
...@@ -38,29 +38,29 @@ func (m *Message) Format(fromEmail, fromName string) string { ...@@ -38,29 +38,29 @@ func (m *Message) Format(fromEmail, fromName string) string {
// From header // From header
if fromName != "" { if fromName != "" {
sb.WriteString(fmt.Sprintf("From: %s <%s>\r\n", fromName, fromEmail)) fmt.Fprintf(&sb, "From: %s <%s>\r\n", fromName, fromEmail)
} else { } else {
sb.WriteString(fmt.Sprintf("From: %s\r\n", fromEmail)) fmt.Fprintf(&sb, "From: %s\r\n", fromEmail)
} }
// To header // To header
sb.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(m.To, ", "))) fmt.Fprintf(&sb, "To: %s\r\n", strings.Join(m.To, ", "))
// Cc header (optional) // Cc header (optional)
if len(m.Cc) > 0 { if len(m.Cc) > 0 {
sb.WriteString(fmt.Sprintf("Cc: %s\r\n", strings.Join(m.Cc, ", "))) fmt.Fprintf(&sb, "Cc: %s\r\n", strings.Join(m.Cc, ", "))
} }
// Reply-To header (optional) // Reply-To header (optional)
if m.ReplyTo != "" { if m.ReplyTo != "" {
sb.WriteString(fmt.Sprintf("Reply-To: %s\r\n", m.ReplyTo)) fmt.Fprintf(&sb, "Reply-To: %s\r\n", m.ReplyTo)
} }
// Subject header // Subject header
sb.WriteString(fmt.Sprintf("Subject: %s\r\n", m.Subject)) fmt.Fprintf(&sb, "Subject: %s\r\n", m.Subject)
// Date header (RFC 5322 format) // Date header (RFC 5322 format)
sb.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().Format(time.RFC1123Z))) fmt.Fprintf(&sb, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
// MIME headers // MIME headers
sb.WriteString("MIME-Version: 1.0\r\n") sb.WriteString("MIME-Version: 1.0\r\n")
......
...@@ -176,7 +176,7 @@ func rewriteNumericLogicalOperand(expr, op string) string { ...@@ -176,7 +176,7 @@ func rewriteNumericLogicalOperand(expr, op string) string {
} }
if i > signStart { if i > signStart {
numLiteral := expr[signStart:i] numLiteral := expr[signStart:i]
builder.WriteString(fmt.Sprintf("(%s != 0)", numLiteral)) fmt.Fprintf(&builder, "(%s != 0)", numLiteral)
} else { } else {
builder.WriteString(expr[signStart:i]) builder.WriteString(expr[signStart:i])
} }
......
...@@ -287,6 +287,8 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error) ...@@ -287,6 +287,8 @@ func (s *service) GenerateSnippet(content []byte, maxLength int) (string, error)
case *mast.TagNode: case *mast.TagNode:
buf.WriteByte('#') buf.WriteByte('#')
buf.Write(node.Tag) buf.Write(node.Tag)
default:
// Ignore other node types.
} }
// Stop walking if we've exceeded double the max length // Stop walking if we've exceeded double the max length
......
...@@ -8,7 +8,6 @@ import ( ...@@ -8,7 +8,6 @@ import (
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config" "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
"github.com/aws/aws-sdk-go-v2/service/s3" "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/pkg/errors" "github.com/pkg/errors"
...@@ -43,23 +42,16 @@ func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client, ...@@ -43,23 +42,16 @@ func NewClient(ctx context.Context, s3Config *storepb.StorageS3Config) (*Client,
// UploadObject uploads an object to S3. // UploadObject uploads an object to S3.
func (c *Client) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) { func (c *Client) UploadObject(ctx context.Context, key string, fileType string, content io.Reader) (string, error) {
uploader := manager.NewUploader(c.Client)
putInput := s3.PutObjectInput{ putInput := s3.PutObjectInput{
Bucket: c.Bucket, Bucket: c.Bucket,
Key: aws.String(key), Key: aws.String(key),
ContentType: aws.String(fileType), ContentType: aws.String(fileType),
Body: content, Body: content,
} }
result, err := uploader.Upload(ctx, &putInput) if _, err := c.Client.PutObject(ctx, &putInput); err != nil {
if err != nil {
return "", err return "", err
} }
return key, nil
resultKey := result.Key
if resultKey == nil || *resultKey == "" {
return "", errors.New("failed to get file key")
}
return *resultKey, nil
} }
// PresignGetObject presigns an object in S3. // PresignGetObject presigns an object in S3.
...@@ -81,16 +73,19 @@ func (c *Client) PresignGetObject(ctx context.Context, key string) (string, erro ...@@ -81,16 +73,19 @@ func (c *Client) PresignGetObject(ctx context.Context, key string) (string, erro
// GetObject retrieves an object from S3. // GetObject retrieves an object from S3.
func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) { func (c *Client) GetObject(ctx context.Context, key string) ([]byte, error) {
downloader := manager.NewDownloader(c.Client) output, err := c.Client.GetObject(ctx, &s3.GetObjectInput{
buffer := manager.NewWriteAtBuffer([]byte{})
_, err := downloader.Download(ctx, buffer, &s3.GetObjectInput{
Bucket: c.Bucket, Bucket: c.Bucket,
Key: aws.String(key), Key: aws.String(key),
}) })
if err != nil { if err != nil {
return nil, errors.Wrap(err, "failed to download object") return nil, errors.Wrap(err, "failed to download object")
} }
return buffer.Bytes(), nil defer output.Body.Close()
data, err := io.ReadAll(output.Body)
if err != nil {
return nil, errors.Wrap(err, "failed to read object body")
}
return data, nil
} }
// GetObjectStream retrieves an object from S3 as a stream. // GetObjectStream retrieves an object from S3 as a stream.
......
...@@ -2,5 +2,5 @@ ...@@ -2,5 +2,5 @@
version: v2 version: v2
deps: deps:
- name: buf.build/googleapis/googleapis - name: buf.build/googleapis/googleapis
commit: 61b203b9a9164be9a834f58c37be6f62 commit: 004180b77378443887d3b55cabc00384
digest: b5:7811a98b35bd2e4ae5c3ac73c8b3d9ae429f3a790da15de188dc98fc2b77d6bb10e45711f14903af9553fa9821dff256054f2e4b7795789265bc476bec2f088c digest: b5:e8f475fe3330f31f5fd86ac689093bcd274e19611a09db91f41d637cb9197881ce89882b94d13a58738e53c91c6e4bae7dc1feba85f590164c975a89e25115dc
FROM --platform=$BUILDPLATFORM golang:1.25.7-alpine AS backend FROM --platform=$BUILDPLATFORM golang:1.26.1-alpine AS backend
WORKDIR /backend-build WORKDIR /backend-build
# Install build dependencies # Install build dependencies
......
...@@ -121,6 +121,8 @@ func convertInstanceSettingFromStore(setting *storepb.InstanceSetting) *v1pb.Ins ...@@ -121,6 +121,8 @@ func convertInstanceSettingFromStore(setting *storepb.InstanceSetting) *v1pb.Ins
instanceSetting.Value = &v1pb.InstanceSetting_MemoRelatedSetting_{ instanceSetting.Value = &v1pb.InstanceSetting_MemoRelatedSetting_{
MemoRelatedSetting: convertInstanceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()), MemoRelatedSetting: convertInstanceMemoRelatedSettingFromStore(setting.GetMemoRelatedSetting()),
} }
default:
// Leave Value unset for unsupported setting variants.
} }
return instanceSetting return instanceSetting
} }
......
...@@ -85,11 +85,11 @@ func (*MCPService) handleCapturePrompt(_ context.Context, req mcp.GetPromptReque ...@@ -85,11 +85,11 @@ func (*MCPService) handleCapturePrompt(_ context.Context, req mcp.GetPromptReque
var sb strings.Builder var sb strings.Builder
sb.WriteString("Save the following as a new memo using the create_memo tool.\n\n") sb.WriteString("Save the following as a new memo using the create_memo tool.\n\n")
sb.WriteString(fmt.Sprintf("Visibility: %s\n\n", visibility)) fmt.Fprintf(&sb, "Visibility: %s\n\n", visibility)
sb.WriteString("Content:\n") sb.WriteString("Content:\n")
sb.WriteString(content) sb.WriteString(content)
if tags != "" { if tags != "" {
sb.WriteString(fmt.Sprintf("\n\nAppend these tags inline using #tag syntax: %s", tags)) fmt.Fprintf(&sb, "\n\nAppend these tags inline using #tag syntax: %s", tags)
} }
sb.WriteString("\n\nAfter creating the memo, confirm by showing the memo resource name (e.g. memo://memos/<uid>) so it can be referenced later.") sb.WriteString("\n\nAfter creating the memo, confirm by showing the memo resource name (e.g. memo://memos/<uid>) so it can be referenced later.")
......
...@@ -3,7 +3,7 @@ package mcp ...@@ -3,7 +3,7 @@ package mcp
import ( import (
"context" "context"
"fmt" "fmt"
"sort" "slices"
"github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/mcp"
mcpserver "github.com/mark3labs/mcp-go/server" mcpserver "github.com/mark3labs/mcp-go/server"
...@@ -53,11 +53,21 @@ func (s *MCPService) handleListTags(ctx context.Context, _ mcp.CallToolRequest) ...@@ -53,11 +53,21 @@ func (s *MCPService) handleListTags(ctx context.Context, _ mcp.CallToolRequest)
for tag, count := range counts { for tag, count := range counts {
entries = append(entries, tagEntry{Tag: tag, Count: count}) entries = append(entries, tagEntry{Tag: tag, Count: count})
} }
sort.Slice(entries, func(i, j int) bool { slices.SortFunc(entries, func(a, b tagEntry) int {
if entries[i].Count != entries[j].Count { if a.Count != b.Count {
return entries[i].Count > entries[j].Count if a.Count > b.Count {
return -1
}
return 1
}
switch {
case a.Tag < b.Tag:
return -1
case a.Tag > b.Tag:
return 1
default:
return 0
} }
return entries[i].Tag < entries[j].Tag
}) })
out, err := marshalJSON(entries) out, err := marshalJSON(entries)
......
...@@ -8,7 +8,7 @@ import ( ...@@ -8,7 +8,7 @@ import (
"io/fs" "io/fs"
"log/slog" "log/slog"
"path/filepath" "path/filepath"
"sort" "slices"
"strconv" "strconv"
"strings" "strings"
...@@ -142,7 +142,7 @@ func (s *Store) applyMigrations(ctx context.Context, currentSchemaVersion, targe ...@@ -142,7 +142,7 @@ func (s *Store) applyMigrations(ctx context.Context, currentSchemaVersion, targe
if err != nil { if err != nil {
return errors.Wrap(err, "failed to read migration files") return errors.Wrap(err, "failed to read migration files")
} }
sort.Strings(filePaths) slices.Sort(filePaths)
// Start a transaction to apply migrations atomically // Start a transaction to apply migrations atomically
tx, err := s.driver.GetDB().Begin() tx, err := s.driver.GetDB().Begin()
...@@ -275,7 +275,7 @@ func (s *Store) seed(ctx context.Context) error { ...@@ -275,7 +275,7 @@ func (s *Store) seed(ctx context.Context) error {
} }
// Sort seed files by name. This is important to ensure that seed files are applied in order. // Sort seed files by name. This is important to ensure that seed files are applied in order.
sort.Strings(filenames) slices.Sort(filenames)
// Start a transaction to apply the seed files. // Start a transaction to apply the seed files.
tx, err := s.driver.GetDB().Begin() tx, err := s.driver.GetDB().Begin()
if err != nil { if err != nil {
...@@ -303,7 +303,7 @@ func (s *Store) GetCurrentSchemaVersion() (string, error) { ...@@ -303,7 +303,7 @@ func (s *Store) GetCurrentSchemaVersion() (string, error) {
return "", errors.Wrap(err, "failed to read migration files") return "", errors.Wrap(err, "failed to read migration files")
} }
sort.Strings(filePaths) slices.Sort(filePaths)
if len(filePaths) == 0 { if len(filePaths) == 0 {
return fmt.Sprintf("%s.0", minorVersion), nil return fmt.Sprintf("%s.0", minorVersion), nil
} }
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment