diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 920af76f..ac44b4f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -11,25 +11,6 @@ jobs: build: runs-on: ubuntu-22.04 - services: - postgres: - image: postgres:14 - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: dnote_test - POSTGRES_PORT: 5432 - # Wait until postgres has started - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - - # Expose port to the host - ports: - - 5432:5432 - steps: - uses: actions/checkout@v5 - uses: actions/setup-go@v6 diff --git a/.github/workflows/release-server.yml b/.github/workflows/release-server.yml new file mode 100644 index 00000000..75657e1e --- /dev/null +++ b/.github/workflows/release-server.yml @@ -0,0 +1,91 @@ +name: Release Server + +on: + push: + tags: + - 'server-v*' + +jobs: + release: + runs-on: ubuntu-22.04 + permissions: + contents: write + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-go@v6 + with: + go-version: '>=1.25.0' + - uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Extract version from tag + id: version + run: | + TAG=${GITHUB_REF#refs/tags/server-v} + echo "version=$TAG" >> $GITHUB_OUTPUT + echo "Releasing version: $TAG" + + - name: Install dependencies + run: make install + + - name: Run tests + run: make test + + - name: Build server + run: make version=${{ steps.version.outputs.version }} build-server + + - name: Prepare Docker build context + run: | + VERSION="${{ steps.version.outputs.version }}" + cp build/server/dnote_server_${VERSION}_linux_amd64.tar.gz host/docker/ + + - name: Login to Docker Hub + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: ./host/docker + push: true + tags: | + dnote/dnote:${{ steps.version.outputs.version }} + dnote/dnote:latest + build-args: | + tarballName=dnote_server_${{ steps.version.outputs.version }}_linux_amd64.tar.gz + + - name: Create GitHub release + env: + GH_TOKEN: ${{ github.token }} + run: | + VERSION="${{ steps.version.outputs.version }}" + TAG="server-v${VERSION}" + + # Determine if prerelease (version not matching major.minor.patch) + FLAGS="" + if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + FLAGS="--prerelease" + fi + + gh release create "$TAG" \ + build/server/*.tar.gz \ + build/server/*_checksums.txt \ + $FLAGS \ + --title="$TAG" \ + --notes="Please see the [CHANGELOG](https://github.com/dnote/dnote/blob/master/CHANGELOG.md)" \ + --draft + + - name: Push to Docker Hub + env: + DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} + DOCKER_TOKEN: ${{ secrets.DOCKER_TOKEN }} + run: | + VERSION="${{ steps.version.outputs.version }}" + + echo "$DOCKER_TOKEN" | docker login -u "$DOCKER_USERNAME" --password-stdin + docker push dnote/dnote:${VERSION} + docker push dnote/dnote:latest diff --git a/SELF_HOSTING.md b/SELF_HOSTING.md index 5d3db690..0c52733c 100644 --- a/SELF_HOSTING.md +++ b/SELF_HOSTING.md @@ -19,17 +19,18 @@ mv ./dnote-server /usr/local/bin 3. Run Dnote ```bash -APP_ENV=PRODUCTION \ -WebURL=$webURL \ -DisableRegistration=false \ - dnote-server start +dnote-server start --webUrl=$webURL ``` Replace `$webURL` with the full URL to your server, without a trailing slash (e.g. `https://your.server`). -Set `DisableRegistration=true` if you would like to disable user registrations. +Additional flags: +- `--port`: Server port (default: `3000`) +- `--disableRegistration`: Disable user registration (default: `false`) +- `--logLevel`: Log level: `debug`, `info`, `warn`, or `error` (default: `info`) +- `--appEnv`: environment (default: `PRODUCTION`) -By default, dnote server will run on the port 3000. +You can also use environment variables: `PORT`, `WebURL`, `DisableRegistration`, `LOG_LEVEL`, `APP_ENV`. ## Configuration @@ -111,9 +112,7 @@ User=$user Restart=always RestartSec=3 WorkingDirectory=/home/$user -ExecStart=/usr/local/bin/dnote-server start -Environment=APP_ENV=PRODUCTION -Environment=WebURL=$WebURL +ExecStart=/usr/local/bin/dnote-server start --webUrl=$WebURL [Install] WantedBy=multi-user.target @@ -121,7 +120,7 @@ WantedBy=multi-user.target Replace `$user` and `$WebURL` with the actual values. -By default, the database will be stored at `$XDG_DATA_HOME/dnote/server.db` (typically `~/.local/share/dnote/server.db`). To use a custom location, add `Environment=DBPath=/path/to/database.db` to the service file. +By default, the database will be stored at `$XDG_DATA_HOME/dnote/server.db` (typically `~/.local/share/dnote/server.db`). To use a custom location, add `--dbPath=/path/to/database.db` to the `ExecStart` command. 2. Reload the change by running `sudo systemctl daemon-reload`. 3. Enable the Daemon by running `sudo systemctl enable dnote`.` @@ -150,7 +149,7 @@ The following is an example configuration: ```yaml editor: nvim -apiEndpoint: https://api.getdnote.com +apiEndpoint: https://localhost:3000/api ``` Simply change the value for `apiEndpoint` to a full URL to the self-hosted instance, followed by '/api', and save the configuration file. diff --git a/pkg/e2e/server_test.go b/pkg/e2e/server_test.go index 5e48e12a..52a2d311 100644 --- a/pkg/e2e/server_test.go +++ b/pkg/e2e/server_test.go @@ -35,8 +35,9 @@ import ( var testServerBinary string func init() { - // Use absolute path - testServerBinary = "/home/device10/development/dnote/pkg/e2e/tmp/.dnote/test-server" + // Build server binary in temp directory + tmpDir := os.TempDir() + testServerBinary = fmt.Sprintf("%s/dnote-test-server", tmpDir) buildCmd := exec.Command("go", "build", "-tags", "fts5", "-o", testServerBinary, "../server") if out, err := buildCmd.CombinedOutput(); err != nil { panic(fmt.Sprintf("failed to build server: %v\n%s", err, out)) diff --git a/pkg/server/config/config.go b/pkg/server/config/config.go index 51def03b..916bae4e 100644 --- a/pkg/server/config/config.go +++ b/pkg/server/config/config.go @@ -75,6 +75,7 @@ type Config struct { DBPath string AssetBaseURL string HTTP500Page []byte + LogLevel string } // Params are the configuration parameters for creating a new Config @@ -84,6 +85,7 @@ type Params struct { WebURL string DBPath string DisableRegistration bool + LogLevel string } // New constructs and returns a new validated config. @@ -95,6 +97,7 @@ func New(p Params) (Config, error) { WebURL: getOrEnv(p.WebURL, "WebURL", ""), DBPath: getOrEnv(p.DBPath, "DBPath", DefaultDBPath), DisableRegistration: p.DisableRegistration || readBoolEnv("DisableRegistration"), + LogLevel: getOrEnv(p.LogLevel, "LOG_LEVEL", "info"), AssetBaseURL: "/static", HTTP500Page: assets.MustGetHTTP500ErrorPage(), } diff --git a/pkg/server/database/migrate.go b/pkg/server/database/migrate.go index 068e2a30..4250fd80 100644 --- a/pkg/server/database/migrate.go +++ b/pkg/server/database/migrate.go @@ -143,9 +143,12 @@ func migrate(db *gorm.DB, fsys fs.FS) error { log.WithFields(log.Fields{ "version": version, - "files": filenames, }).Info("Database schema version.") + log.WithFields(log.Fields{ + "files": filenames, + }).Debug("Database migration files.") + // Apply pending migrations for _, m := range migrations { if m.version <= version { @@ -178,6 +181,5 @@ func migrate(db *gorm.DB, fsys fs.FS) error { }).Info("Migrate success.") } - log.Info("Migration complete.") return nil } diff --git a/pkg/server/log/log.go b/pkg/server/log/log.go index 78f737bd..89eeb719 100644 --- a/pkg/server/log/log.go +++ b/pkg/server/log/log.go @@ -32,9 +32,19 @@ const ( fieldKeyTimestamp = "ts" fieldKeyUnixTimestamp = "ts_unix" - levelInfo = "info" - levelWarn = "warn" - levelError = "error" + // LevelDebug represents debug log level + LevelDebug = "debug" + // LevelInfo represents info log level + LevelInfo = "info" + // LevelWarn represents warn log level + LevelWarn = "warn" + // LevelError represents error log level + LevelError = "error" +) + +var ( + // currentLevel is the currently configured log level + currentLevel = LevelInfo ) // Fields represents a set of information to be included in the log @@ -58,19 +68,50 @@ func WithFields(fields Fields) Entry { return newEntry(fields) } +// SetLevel sets the global log level +func SetLevel(level string) { + currentLevel = level +} + +// levelPriority returns a numeric priority for comparison +func levelPriority(level string) int { + switch level { + case LevelDebug: + return 0 + case LevelInfo: + return 1 + case LevelWarn: + return 2 + case LevelError: + return 3 + default: + return 1 + } +} + +// shouldLog returns true if the given level should be logged based on currentLevel +func shouldLog(level string) bool { + return levelPriority(level) >= levelPriority(currentLevel) +} + +// Debug logs the given entry at a debug level +func (e Entry) Debug(msg string) { + e.write(LevelDebug, msg) +} + // Info logs the given entry at an info level func (e Entry) Info(msg string) { - e.write(levelInfo, msg) + e.write(LevelInfo, msg) } // Warn logs the given entry at a warning level func (e Entry) Warn(msg string) { - e.write(levelWarn, msg) + e.write(LevelWarn, msg) } // Error logs the given entry at an error level func (e Entry) Error(msg string) { - e.write(levelError, msg) + e.write(LevelError, msg) } // ErrorWrap logs the given entry with the error message annotated by the given message @@ -106,6 +147,10 @@ func (e Entry) formatJSON(level, msg string) []byte { } func (e Entry) write(level, msg string) { + if !shouldLog(level) { + return + } + serialized := e.formatJSON(level, msg) _, err := fmt.Fprintln(os.Stderr, string(serialized)) @@ -114,6 +159,11 @@ func (e Entry) write(level, msg string) { } } +// Debug logs a debug message without additional fields +func Debug(msg string) { + newEntry(Fields{}).Debug(msg) +} + // Info logs an info message without additional fields func Info(msg string) { newEntry(Fields{}).Info(msg) diff --git a/pkg/server/mailer/backend.go b/pkg/server/mailer/backend.go index ff425e64..0abe51d8 100644 --- a/pkg/server/mailer/backend.go +++ b/pkg/server/mailer/backend.go @@ -19,11 +19,10 @@ package mailer import ( - "fmt" - "log" "os" "strconv" + "github.com/dnote/dnote/pkg/server/log" "github.com/pkg/errors" "gopkg.in/gomail.v2" ) @@ -104,9 +103,12 @@ func NewDefaultBackend(enabled bool) (*DefaultBackend, error) { func (b *DefaultBackend) Queue(subject, from string, to []string, contentType, body string) error { // If not enabled, just log the email if !b.Enabled { - log.Println("Not sending email because backend is disabled.") - log.Printf("Subject: %s, to: %s, from: %s", subject, to, from) - fmt.Println(body) + log.WithFields(log.Fields{ + "subject": subject, + "to": to, + "from": from, + "body": body, + }).Info("Not sending email because email backend is not configured.") return nil } diff --git a/pkg/server/main.go b/pkg/server/main.go index 61f47de7..4e36b96d 100644 --- a/pkg/server/main.go +++ b/pkg/server/main.go @@ -21,7 +21,6 @@ package main import ( "flag" "fmt" - "log" "net/http" "os" @@ -31,6 +30,7 @@ import ( "github.com/dnote/dnote/pkg/server/config" "github.com/dnote/dnote/pkg/server/controllers" "github.com/dnote/dnote/pkg/server/database" + "github.com/dnote/dnote/pkg/server/log" "github.com/dnote/dnote/pkg/server/mailer" "github.com/pkg/errors" "gorm.io/gorm" @@ -51,7 +51,7 @@ func initApp(cfg config.Config) app.App { if err != nil { emailBackend = &mailer.DefaultBackend{Enabled: false} } else { - log.Printf("Email backend configured") + log.Info("Email backend configured") } return app.App{ @@ -85,6 +85,7 @@ Flags: webURL := startFlags.String("webUrl", "", "Full URL to server without trailing slash (env: WebURL, example: https://example.com)") dbPath := startFlags.String("dbPath", "", "Path to SQLite database file (env: DBPath, default: $XDG_DATA_HOME/dnote/server.db)") disableRegistration := startFlags.Bool("disableRegistration", false, "Disable user registration (env: DisableRegistration, default: false)") + logLevel := startFlags.String("logLevel", "", "Log level: debug, info, warn, or error (env: LOG_LEVEL, default: info)") startFlags.Parse(args) @@ -94,6 +95,7 @@ Flags: WebURL: *webURL, DBPath: *dbPath, DisableRegistration: *disableRegistration, + LogLevel: *logLevel, }) if err != nil { fmt.Printf("Error: %s\n\n", err) @@ -101,6 +103,9 @@ Flags: os.Exit(1) } + // Set log level + log.SetLevel(cfg.LogLevel) + app := initApp(cfg) defer func() { sqlDB, err := app.DB.DB() @@ -121,8 +126,15 @@ Flags: panic(errors.Wrap(err, "initializing router")) } - log.Printf("Dnote version %s is running on port %s", buildinfo.Version, cfg.Port) - log.Fatalln(http.ListenAndServe(fmt.Sprintf(":%s", cfg.Port), r)) + log.WithFields(log.Fields{ + "version": buildinfo.Version, + "port": cfg.Port, + }).Info("Dnote server starting") + + if err := http.ListenAndServe(fmt.Sprintf(":%s", cfg.Port), r); err != nil { + log.ErrorWrap(err, "server failed") + os.Exit(1) + } } func versionCmd() {