Automate release

This commit is contained in:
Sung 2025-10-05 16:10:34 -07:00
commit 4734d82f8f
9 changed files with 190 additions and 49 deletions

View file

@ -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

91
.github/workflows/release-server.yml vendored Normal file
View file

@ -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

View file

@ -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.

View file

@ -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))

View file

@ -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(),
}

View file

@ -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
}

View file

@ -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)

View file

@ -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
}

View file

@ -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() {