diff --git a/.github/CICD.md b/.github/CICD.md index 7e56f59f1..841499ddb 100644 --- a/.github/CICD.md +++ b/.github/CICD.md @@ -15,7 +15,7 @@ The ODE monorepo uses GitHub Actions for continuous integration and deployment. #### Triggers - **Push to `main`**: Builds and publishes release images -- **Push to `develop`**: Builds and publishes pre-release images +- **Push to `dev`**: Builds and publishes pre-release images - **Push to feature branches**: Builds and publishes branch-specific images - **Pull Requests**: Builds but does not publish (validation only) - **Manual Dispatch**: Allows manual triggering with optional version tag @@ -37,7 +37,7 @@ Images are published to **GitHub Container Registry (GHCR)**: | Branch/Event | Tags Generated | Description | |--------------|----------------|-------------| | `main` | `latest`, `main-{sha}` | Latest stable release | -| `develop` | `develop`, `develop-{sha}` | Development pre-release | +| `dev` | `dev`, `dev-{sha}` | Development pre-release | | Feature branches | `{branch-name}`, `{branch-name}-{sha}` | Feature-specific builds | | Pull Requests | `pr-{number}` | PR validation builds (not pushed) | | Manual with version | `v{version}`, `v{major}.{minor}`, `latest` | Versioned release | @@ -76,7 +76,7 @@ docker pull ghcr.io/opendataensemble/synkronus:v1.0.0 ### Pull Development Build ```bash -docker pull ghcr.io/opendataensemble/synkronus:develop +docker pull ghcr.io/opendataensemble/synkronus:dev ``` ### Pull Feature Branch Build @@ -129,6 +129,155 @@ echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin 3. Select **synkronus** 4. View all published tags and their details +## Code Formatting & Linting + +All workflows enforce formatting and linting checks before builds. Ensure your code is properly formatted before pushing to avoid CI failures. + +### Go Projects (synkronus, synkronus-cli) + +#### Format Go Code + +Format all Go files in a project: + +```bash +# For synkronus +cd synkronus +go fmt ./... + +# Or using gofmt directly +gofmt -s -w . + +# For synkronus-cli +cd synkronus-cli +go fmt ./... +``` + +#### Check Go Formatting (without modifying files) + +```bash +# Check if files are formatted +gofmt -s -l . + +# View what would change +gofmt -s -d . +``` + +#### Run Linting + +```bash +# Install golangci-lint (if not already installed) +# macOS: brew install golangci-lint +# Linux: curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.54.2 + +# Run linting for synkronus +cd synkronus +golangci-lint run + +# Run linting for synkronus-cli +cd synkronus-cli +golangci-lint run +``` + +### TypeScript/JavaScript Projects + +#### Synkronus Portal + +```bash +cd synkronus-portal + +# Format code +npm run format + +# Check formatting (no writes) +npm run format:check + +# Run linting +npm run lint +``` + +#### Formulus (React Native) + +```bash +cd formulus + +# Format code +npm run format + +# Check formatting (no writes) +npm run format:check + +# Run linting +npm run lint + +# Run linting with auto-fix +npm run lint:fix +``` + +#### Formulus Formplayer (React Web) + +```bash +cd formulus-formplayer + +# Format code +npm run format + +# Check formatting (no writes) +npm run format:check + +# Run linting +npm run lint + +# Run linting with auto-fix +npm run lint:fix +``` + +### What CI Enforces + +#### Go Projects + +- **gofmt**: All Go files must be formatted. CI will fail if any files are unformatted. +- **golangci-lint**: Runs comprehensive linting checks using the `.golangci.yml` configuration. + +#### TypeScript/JavaScript Projects + +- **Prettier**: All source files must be formatted according to Prettier rules. +- **ESLint**: Code must pass ESLint checks with no errors. + +### Pre-commit Checklist + +Before pushing your code, run these commands: + +```bash +# For Go projects +cd synkronus # or synkronus-cli +go fmt ./... +golangci-lint run + +# For TypeScript/JavaScript projects +cd synkronus-portal # or formulus, or formulus-formplayer +npm run format:check +npm run lint +``` + +### Auto-formatting with Git Hooks (Optional) + +You can set up a pre-commit hook to automatically format code: + +```bash +# Create .git/hooks/pre-commit +cat > .git/hooks/pre-commit << 'EOF' +#!/bin/bash +# Format Go files +find . -name "*.go" -not -path "./vendor/*" -exec gofmt -s -w {} \; +# Format TypeScript/JavaScript files +cd synkronus-portal && npm run format +cd ../formulus && npm run format +cd ../formulus-formplayer && npm run format +EOF + +chmod +x .git/hooks/pre-commit +``` + ## Troubleshooting ### Build Fails on Push @@ -163,7 +312,7 @@ echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin ### For Deployments 1. **Pin versions in production**: Use specific version tags, not `latest` -2. **Test pre-releases**: Use `develop` tag for staging environments +2. **Test pre-releases**: Use `dev` tag for staging environments 3. **Monitor image sizes**: Keep images lean for faster deployments 4. **Use health checks**: Always configure health checks in deployments @@ -175,7 +324,6 @@ Potential improvements to the CI/CD pipeline: - [ ] Implement security scanning (Trivy, Snyk) - [ ] Add deployment to staging environment - [ ] Create release notes automation -- [ ] Add Slack/Discord notifications - [ ] Implement rollback mechanisms - [ ] Add performance benchmarking diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc5818051..b819c44bf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -169,28 +169,27 @@ jobs: - name: Download dependencies run: go mod download - - name: Run gofmt (warning) + - name: Run gofmt run: | UNFORMATTED_FILES=$(git ls-files '*.go' | xargs gofmt -s -l) if [ -n "$UNFORMATTED_FILES" ]; then - echo "⚠️ Code is not formatted. Consider running 'go fmt ./...' or 'gofmt -s -w .'" + echo "❌ Code is not formatted. Please run 'go fmt ./...' or 'gofmt -s -w .'" echo "Unformatted files:" echo "$UNFORMATTED_FILES" echo "Diffs:" echo "$UNFORMATTED_FILES" | xargs gofmt -s -d + exit 1 else echo "✅ All Go files are formatted." fi - - name: Run golangci-lint (if available) - run: | - if command -v golangci-lint &> /dev/null; then - golangci-lint run - else - echo "golangci-lint not installed, skipping..." - fi - continue-on-error: true + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v2.0.0 + working-directory: ./synkronus + args: --timeout=5m - name: Run tests run: go test -v -race -coverprofile=coverage.out ./... @@ -226,20 +225,29 @@ jobs: - name: Download dependencies run: go mod download - - name: Run gofmt (warning) + - name: Run gofmt run: | UNFORMATTED_FILES=$(git ls-files '*.go' | xargs gofmt -s -l) if [ -n "$UNFORMATTED_FILES" ]; then - echo "⚠️ Code is not formatted. Consider running 'go fmt ./...' or 'gofmt -s -w .'" + echo "❌ Code is not formatted. Please run 'go fmt ./...' or 'gofmt -s -w .'" echo "Unformatted files:" echo "$UNFORMATTED_FILES" echo "Diffs:" echo "$UNFORMATTED_FILES" | xargs gofmt -s -d + exit 1 else echo "✅ All Go files are formatted." fi + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v2.0.0 + working-directory: ./synkronus-cli + args: --timeout=5m + args: --timeout=5m + - name: Run tests run: go test -v -race -coverprofile=coverage.out ./... diff --git a/.github/workflows/synkronus-cli.yml b/.github/workflows/synkronus-cli.yml index fc1e38e35..48d7eb11f 100644 --- a/.github/workflows/synkronus-cli.yml +++ b/.github/workflows/synkronus-cli.yml @@ -16,9 +16,62 @@ on: types: [published] jobs: + format-check: + name: Check Go formatting + if: github.event_name != 'release' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.x' + cache-dependency-path: synkronus-cli/go.sum + + - name: Check formatting + working-directory: synkronus-cli + run: | + UNFORMATTED_FILES=$(git ls-files '*.go' | xargs gofmt -s -l) + + if [ -n "$UNFORMATTED_FILES" ]; then + echo "❌ Code is not formatted. Please run 'go fmt ./...' or 'gofmt -s -w .'" + echo "Unformatted files:" + echo "$UNFORMATTED_FILES" + echo "" + echo "Diffs:" + echo "$UNFORMATTED_FILES" | xargs gofmt -s -d + exit 1 + else + echo "✅ All Go files are formatted." + fi + + lint: + name: Lint with golangci-lint + if: github.event_name != 'release' + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24.x' + cache-dependency-path: synkronus-cli/go.sum + + - name: golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v2.0.0 + working-directory: synkronus-cli + args: --timeout=5m + build-cli: name: Build synkronus-cli (CI) if: github.event_name != 'release' + needs: [format-check, lint] runs-on: ubuntu-latest strategy: diff --git a/.github/workflows/synkronus-docker.yml b/.github/workflows/synkronus-docker.yml index c4dec91b3..bf2d38bc3 100644 --- a/.github/workflows/synkronus-docker.yml +++ b/.github/workflows/synkronus-docker.yml @@ -38,6 +38,33 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' + cache-dependency-path: synkronus/go.sum + + - name: Check Go formatting + working-directory: ./synkronus + run: | + UNFORMATTED_FILES=$(git ls-files '*.go' | xargs gofmt -s -l) + + if [ -n "$UNFORMATTED_FILES" ]; then + echo "❌ Code is not formatted. Please run 'go fmt ./...' or 'gofmt -s -w .'" + echo "Unformatted files:" + echo "$UNFORMATTED_FILES" + exit 1 + else + echo "✅ All Go files are formatted." + fi + + - name: Run golangci-lint + uses: golangci/golangci-lint-action@v6 + with: + version: v2.0.0 + working-directory: ./synkronus + args: --timeout=5m + - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: diff --git a/.github/workflows/synkronus-portal-docker.yml b/.github/workflows/synkronus-portal-docker.yml index 0f820c4c3..5096adf59 100644 --- a/.github/workflows/synkronus-portal-docker.yml +++ b/.github/workflows/synkronus-portal-docker.yml @@ -38,6 +38,29 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: | + synkronus-portal/package-lock.json + packages/tokens/package-lock.json + + - name: Install dependencies + run: | + cd packages/tokens && npm ci + cd ../components && npm install || true + cd ../../synkronus-portal && npm ci + + - name: Run ESLint + working-directory: synkronus-portal + run: npm run lint + + - name: Check Prettier formatting + working-directory: synkronus-portal + run: npm run format:check + - name: Set up QEMU uses: docker/setup-qemu-action@v3 with: diff --git a/synkronus-cli/.golangci.yml b/synkronus-cli/.golangci.yml new file mode 100644 index 000000000..cd817ac70 --- /dev/null +++ b/synkronus-cli/.golangci.yml @@ -0,0 +1,16 @@ +version: "2" + +linters: + disable-all: true + enable: + - staticcheck # Best static analysis tool + - govet # Catches common Go mistakes + - errcheck # Ensures errors are handled + - unused # Finds unused code + - gocritic # Opinionated code analysis + +issues: + exclude-rules: + - path: _test\.go$ # More relaxed rules for tests + linters: + - gocritic diff --git a/synkronus-cli/internal/auth/auth.go b/synkronus-cli/internal/auth/auth.go index 99487436a..5dc8530b3 100644 --- a/synkronus-cli/internal/auth/auth.go +++ b/synkronus-cli/internal/auth/auth.go @@ -46,7 +46,7 @@ func Login(username, password string) (*TokenResponse, error) { if err != nil { return nil, fmt.Errorf("login request failed for endpoint %s: %w", loginURL, err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Check response status if resp.StatusCode != http.StatusOK { @@ -73,7 +73,7 @@ func Login(username, password string) (*TokenResponse, error) { viper.Set("auth.token", tokenResp.Token) viper.Set("auth.refresh_token", tokenResp.RefreshToken) viper.Set("auth.expires_at", tokenResp.ExpiresAt) - viper.WriteConfig() + _ = viper.WriteConfig() return &tokenResp, nil } @@ -98,7 +98,7 @@ func RefreshToken() (*TokenResponse, error) { if err != nil { return nil, fmt.Errorf("refresh request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() // Check response status if resp.StatusCode != http.StatusOK { @@ -122,7 +122,7 @@ func RefreshToken() (*TokenResponse, error) { viper.Set("auth.token", tokenResp.Token) viper.Set("auth.refresh_token", tokenResp.RefreshToken) viper.Set("auth.expires_at", tokenResp.ExpiresAt) - viper.WriteConfig() + _ = viper.WriteConfig() return &tokenResp, nil } diff --git a/synkronus-cli/internal/cmd/auth.go b/synkronus-cli/internal/cmd/auth.go index 4e05e5824..4395bf7fb 100644 --- a/synkronus-cli/internal/cmd/auth.go +++ b/synkronus-cli/internal/cmd/auth.go @@ -26,7 +26,7 @@ func init() { if username == "" { fmt.Print("Username: ") - fmt.Scanln(&username) + _, _ = fmt.Scanln(&username) } fmt.Print("Password: ") @@ -96,7 +96,7 @@ func init() { utils.PrintHeading("Authentication Status") fmt.Printf("%s\n", utils.FormatKeyValue("Username", claims.Username)) fmt.Printf("%s\n", utils.FormatKeyValue("Role", claims.Role)) - fmt.Printf("%s\n", utils.FormatKeyValue("Expires at", claims.RegisteredClaims.ExpiresAt.Time)) + fmt.Printf("%s\n", utils.FormatKeyValue("Expires at", claims.ExpiresAt.Time)) return nil }, } diff --git a/synkronus-cli/internal/cmd/health.go b/synkronus-cli/internal/cmd/health.go index 19ad396d8..70d9ce1f8 100644 --- a/synkronus-cli/internal/cmd/health.go +++ b/synkronus-cli/internal/cmd/health.go @@ -29,30 +29,32 @@ func init() { if err != nil { return fmt.Errorf("connection failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() duration := time.Since(start) // Print status with appropriate color based on status code - if resp.StatusCode >= 200 && resp.StatusCode < 300 { + switch { + case resp.StatusCode >= 200 && resp.StatusCode < 300: utils.PrintSuccess("API responded with status: %s", resp.Status) - } else if resp.StatusCode >= 400 && resp.StatusCode < 500 { + case resp.StatusCode >= 400 && resp.StatusCode < 500: utils.PrintWarning("API responded with status: %s", resp.Status) - } else if resp.StatusCode >= 500 { + case resp.StatusCode >= 500: utils.PrintError("API responded with status: %s", resp.Status) - } else { + default: fmt.Printf("%s\n", utils.FormatKeyValue("API status", resp.Status)) } // Format response time with color based on duration respTimeStr := duration.String() - if duration < 100*time.Millisecond { + switch { + case duration < 100*time.Millisecond: respTimeStr = utils.Success(respTimeStr) - } else if duration < 500*time.Millisecond { + case duration < 500*time.Millisecond: respTimeStr = utils.Info(respTimeStr) - } else if duration < 1*time.Second { + case duration < 1*time.Second: respTimeStr = utils.Warning(respTimeStr) - } else { + default: respTimeStr = utils.Error(respTimeStr) } diff --git a/synkronus-cli/internal/cmd/root.go b/synkronus-cli/internal/cmd/root.go index 71bf0b5a6..67068f4c0 100644 --- a/synkronus-cli/internal/cmd/root.go +++ b/synkronus-cli/internal/cmd/root.go @@ -76,8 +76,8 @@ func init() { rootCmd.PersistentFlags().String("api-url", "http://localhost:8080", "Synkronus API URL") rootCmd.PersistentFlags().String("api-version", "1.0.0", "API version to use") - viper.BindPFlag("api.url", rootCmd.PersistentFlags().Lookup("api-url")) - viper.BindPFlag("api.version", rootCmd.PersistentFlags().Lookup("api-version")) + _ = viper.BindPFlag("api.url", rootCmd.PersistentFlags().Lookup("api-url")) + _ = viper.BindPFlag("api.version", rootCmd.PersistentFlags().Lookup("api-version")) // Add completion command rootCmd.AddCommand(completionCmd) @@ -125,7 +125,7 @@ func initConfig() { // If a config file is found, read it in if err := viper.ReadInConfig(); err == nil { - //fmt.Printf("# Using config file: %s\n", viper.ConfigFileUsed()) + // fmt.Printf("# Using config file: %s\n", viper.ConfigFileUsed()) } else { // Create default config if it doesn't exist if _, ok := err.(viper.ConfigFileNotFoundError); ok { @@ -135,7 +135,7 @@ func initConfig() { if configPath != "" { configDir := filepath.Dir(configPath) if _, err := os.Stat(configDir); os.IsNotExist(err) { - os.MkdirAll(configDir, 0755) + _ = os.MkdirAll(configDir, 0755) } viper.SetConfigFile(configPath) } else { @@ -143,7 +143,7 @@ func initConfig() { legacyPath := filepath.Join(os.Getenv("HOME"), ".synkronus.yaml") configDir := filepath.Dir(legacyPath) if _, err := os.Stat(configDir); os.IsNotExist(err) { - os.MkdirAll(configDir, 0755) + _ = os.MkdirAll(configDir, 0755) } viper.SetConfigFile(legacyPath) } @@ -151,7 +151,7 @@ func initConfig() { for k, v := range defaultConfig { viper.Set(k, v) } - viper.WriteConfig() + _ = viper.WriteConfig() } } } diff --git a/synkronus-cli/internal/cmd/sync.go b/synkronus-cli/internal/cmd/sync.go index ab36d9393..46acf8bf3 100644 --- a/synkronus-cli/internal/cmd/sync.go +++ b/synkronus-cli/internal/cmd/sync.go @@ -117,7 +117,7 @@ Examples: pullCmd.Flags().StringSlice("schema-types", []string{}, "Comma-separated list of schema types to filter") pullCmd.Flags().Int("limit", 0, "Maximum number of records to return") pullCmd.Flags().String("page-token", "", "Pagination token from previous response") - pullCmd.MarkFlagRequired("client-id") + _ = pullCmd.MarkFlagRequired("client-id") syncCmd.AddCommand(pullCmd) // Push command diff --git a/synkronus-cli/internal/cmd/user.go b/synkronus-cli/internal/cmd/user.go index 1a7d84cad..02a0c6783 100644 --- a/synkronus-cli/internal/cmd/user.go +++ b/synkronus-cli/internal/cmd/user.go @@ -124,19 +124,19 @@ func init() { createUserCmd.Flags().String("username", "", "Username for the new user") createUserCmd.Flags().String("password", "", "Password for the new user") createUserCmd.Flags().String("role", "read-only", "Role for the new user (read-only, read-write, admin)") - createUserCmd.MarkFlagRequired("username") - createUserCmd.MarkFlagRequired("password") - createUserCmd.MarkFlagRequired("role") + _ = createUserCmd.MarkFlagRequired("username") + _ = createUserCmd.MarkFlagRequired("password") + _ = createUserCmd.MarkFlagRequired("role") resetPasswordCmd.Flags().String("username", "", "Username of the user whose password to reset") resetPasswordCmd.Flags().String("new-password", "", "New password for the user") - resetPasswordCmd.MarkFlagRequired("username") - resetPasswordCmd.MarkFlagRequired("new-password") + _ = resetPasswordCmd.MarkFlagRequired("username") + _ = resetPasswordCmd.MarkFlagRequired("new-password") changePasswordCmd.Flags().String("old-password", "", "Current password") changePasswordCmd.Flags().String("new-password", "", "New password") - changePasswordCmd.MarkFlagRequired("old-password") - changePasswordCmd.MarkFlagRequired("new-password") + _ = changePasswordCmd.MarkFlagRequired("old-password") + _ = changePasswordCmd.MarkFlagRequired("new-password") userCmd.AddCommand(listUsersCmd) userCmd.AddCommand(createUserCmd) diff --git a/synkronus-cli/pkg/client/attachments.go b/synkronus-cli/pkg/client/attachments.go index fcff85f81..65ec47e20 100644 --- a/synkronus-cli/pkg/client/attachments.go +++ b/synkronus-cli/pkg/client/attachments.go @@ -17,7 +17,7 @@ func (c *Client) UploadAttachment(attachmentID string, filePath string) (map[str if err != nil { return nil, fmt.Errorf("error opening file: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() // Create a buffer to store the request body body := &bytes.Buffer{} @@ -36,7 +36,7 @@ func (c *Client) UploadAttachment(attachmentID string, filePath string) (map[str } // Close the writer to finalize the form - writer.Close() + _ = writer.Close() // Create request url := fmt.Sprintf("%s/attachments/%s", c.BaseURL, attachmentID) @@ -53,7 +53,7 @@ func (c *Client) UploadAttachment(attachmentID string, filePath string) (map[str if err != nil { return nil, fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -82,7 +82,7 @@ func (c *Client) DownloadAttachment(attachmentID string, outputPath string) erro if err != nil { return fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -94,7 +94,7 @@ func (c *Client) DownloadAttachment(attachmentID string, outputPath string) erro if err != nil { return fmt.Errorf("error creating output file: %w", err) } - defer out.Close() + defer func() { _ = out.Close() }() // Copy response body to file _, err = io.Copy(out, resp.Body) @@ -119,7 +119,7 @@ func (c *Client) AttachmentExists(attachmentID string) (bool, error) { if err != nil { return false, fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() switch resp.StatusCode { case http.StatusOK: diff --git a/synkronus-cli/pkg/client/client.go b/synkronus-cli/pkg/client/client.go index 8862ce3cd..f4e8a29cf 100644 --- a/synkronus-cli/pkg/client/client.go +++ b/synkronus-cli/pkg/client/client.go @@ -86,7 +86,7 @@ func (c *Client) GetVersion() (*SystemVersionInfo, error) { if err != nil { return nil, fmt.Errorf("version request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var errResp struct { @@ -135,7 +135,7 @@ func (c *Client) GetAppBundleManifest() (map[string]interface{}, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -162,7 +162,7 @@ func (c *Client) GetAppBundleVersions() (map[string]interface{}, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -200,7 +200,7 @@ func (c *Client) GetAppBundleChanges(currentVersion, targetVersion string) (*App if err != nil { return nil, fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -232,7 +232,7 @@ func (c *Client) DownloadAppBundleFile(path, destPath string, preview bool) erro if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -250,7 +250,7 @@ func (c *Client) DownloadAppBundleFile(path, destPath string, preview bool) erro if err != nil { return err } - defer out.Close() + defer func() { _ = out.Close() }() // Copy response body to file _, err = io.Copy(out, resp.Body) @@ -274,7 +274,7 @@ func (c *Client) DownloadParquetExport(destPath string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -292,7 +292,7 @@ func (c *Client) DownloadParquetExport(destPath string) error { if err != nil { return err } - defer out.Close() + defer func() { _ = out.Close() }() // Copy response body to file _, err = io.Copy(out, resp.Body) @@ -312,7 +312,7 @@ func (c *Client) UploadAppBundle(bundlePath string) (map[string]interface{}, err if err != nil { return nil, err } - defer file.Close() + defer func() { _ = file.Close() }() // Create multipart form body := &bytes.Buffer{} @@ -350,7 +350,7 @@ func (c *Client) UploadAppBundle(bundlePath string) (map[string]interface{}, err if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -378,7 +378,7 @@ func (c *Client) SwitchAppBundleVersion(version string) (map[string]interface{}, if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -457,7 +457,7 @@ func (c *Client) SyncPull(clientID string, currentVersion int64, schemaTypes []s if err != nil { return nil, fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) @@ -501,7 +501,7 @@ func (c *Client) SyncPush(clientID string, transmissionID string, records []map[ if err != nil { return nil, fmt.Errorf("request failed: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) diff --git a/synkronus-cli/pkg/client/user.go b/synkronus-cli/pkg/client/user.go index f5829d0ed..2dd2bdb6f 100644 --- a/synkronus-cli/pkg/client/user.go +++ b/synkronus-cli/pkg/client/user.go @@ -42,7 +42,7 @@ func (c *Client) CreateUser(reqBody UserCreateRequest) (map[string]interface{}, if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusForbidden { return nil, fmt.Errorf("only admin can create users") } @@ -69,7 +69,7 @@ func (c *Client) DeleteUser(username string) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var apiErr map[string]interface{} _ = json.NewDecoder(resp.Body).Decode(&apiErr) @@ -94,7 +94,7 @@ func (c *Client) ResetUserPassword(reqBody UserResetPasswordRequest) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var apiErr map[string]interface{} _ = json.NewDecoder(resp.Body).Decode(&apiErr) @@ -119,7 +119,7 @@ func (c *Client) ChangeOwnPassword(reqBody UserChangePasswordRequest) error { if err != nil { return err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var apiErr map[string]interface{} _ = json.NewDecoder(resp.Body).Decode(&apiErr) @@ -139,7 +139,7 @@ func (c *Client) ListUsers() ([]map[string]interface{}, error) { if err != nil { return nil, err } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { var apiErr map[string]interface{} _ = json.NewDecoder(resp.Body).Decode(&apiErr) diff --git a/synkronus-cli/pkg/validation/bundle.go b/synkronus-cli/pkg/validation/bundle.go index c8652474d..2f4bf10f6 100644 --- a/synkronus-cli/pkg/validation/bundle.go +++ b/synkronus-cli/pkg/validation/bundle.go @@ -25,7 +25,7 @@ func ValidateBundle(bundlePath string) error { if err != nil { return fmt.Errorf("failed to open bundle: %w", err) } - defer zipFile.Close() + defer func() { _ = zipFile.Close() }() // Track required top-level directories hasAppDir := false @@ -181,7 +181,9 @@ func validateFormRendererReferences(zipReader *zip.Reader) error { // Parse the schema var schema map[string]interface{} err = json.NewDecoder(f).Decode(&schema) - f.Close() // Close the file immediately after reading + if closeErr := f.Close(); closeErr != nil { + return fmt.Errorf("failed to close file: %w", closeErr) + } if err != nil { return fmt.Errorf("failed to parse form schema: %w", err) } @@ -268,7 +270,7 @@ func validateJSONFile(file *zip.File) error { if err != nil { return fmt.Errorf("failed to open file: %w", err) } - defer rc.Close() + defer func() { _ = rc.Close() }() var jsonData interface{} decoder := json.NewDecoder(rc) @@ -295,7 +297,7 @@ func GetBundleInfo(bundlePath string) (map[string]interface{}, error) { if err != nil { return nil, fmt.Errorf("failed to open bundle: %w", err) } - defer zipFile.Close() + defer func() { _ = zipFile.Close() }() // Count files and directories fileCount := 0 diff --git a/synkronus-cli/pkg/validation/bundle_test.go b/synkronus-cli/pkg/validation/bundle_test.go index e123dc680..eeee9fc3b 100644 --- a/synkronus-cli/pkg/validation/bundle_test.go +++ b/synkronus-cli/pkg/validation/bundle_test.go @@ -33,7 +33,7 @@ func createTestBundle(t *testing.T, files map[string]string) string { if err != nil { t.Fatalf("failed to create temp file: %v", err) } - defer tmpFile.Close() + defer func() { _ = tmpFile.Close() }() _, err = tmpFile.Write(buf.Bytes()) if err != nil { @@ -189,7 +189,7 @@ func TestValidateBundle(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bundlePath := createTestBundle(t, tt.files) - defer os.Remove(bundlePath) + defer func() { _ = os.Remove(bundlePath) }() err := ValidateBundle(bundlePath) if tt.wantErr { @@ -200,10 +200,8 @@ func TestValidateBundle(t *testing.T) { if tt.errMsg != "" && !contains(err.Error(), tt.errMsg) { t.Errorf("ValidateBundle() error = %v, want error containing %q", err, tt.errMsg) } - } else { - if err != nil { - t.Errorf("ValidateBundle() unexpected error = %v", err) - } + } else if err != nil { + t.Errorf("ValidateBundle() unexpected error = %v", err) } }) } @@ -221,7 +219,7 @@ func TestGetBundleInfo(t *testing.T) { } bundlePath := createTestBundle(t, files) - defer os.Remove(bundlePath) + defer func() { _ = os.Remove(bundlePath) }() info, err := GetBundleInfo(bundlePath) if err != nil { diff --git a/synkronus-portal/package-lock.json b/synkronus-portal/package-lock.json index 834f47a97..e322664bb 100644 --- a/synkronus-portal/package-lock.json +++ b/synkronus-portal/package-lock.json @@ -25,6 +25,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-react-refresh": "^0.4.24", "globals": "^16.5.0", + "prettier": "^3.0.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", "vite": "^7.2.4" @@ -2825,6 +2826,22 @@ "node": ">= 0.8.0" } }, + "node_modules/prettier": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", diff --git a/synkronus-portal/package.json b/synkronus-portal/package.json index c6f270c11..c20849234 100644 --- a/synkronus-portal/package.json +++ b/synkronus-portal/package.json @@ -7,6 +7,8 @@ "dev": "vite", "build": "tsc -b && vite build", "lint": "eslint .", + "format": "prettier \"src/**/*.{ts,tsx,json,css,md}\" --write", + "format:check": "prettier \"src/**/*.{ts,tsx,json,css,md}\" --check", "preview": "vite preview" }, "dependencies": { @@ -29,6 +31,7 @@ "globals": "^16.5.0", "typescript": "~5.9.3", "typescript-eslint": "^8.46.4", + "prettier": "^3.0.0", "vite": "^7.2.4" } } diff --git a/synkronus-portal/src/App.tsx b/synkronus-portal/src/App.tsx index 21ff768d8..eca85a905 100644 --- a/synkronus-portal/src/App.tsx +++ b/synkronus-portal/src/App.tsx @@ -1,7 +1,7 @@ -import { AuthProvider } from './contexts/AuthContext' -import { ProtectedRoute } from './components/ProtectedRoute' -import { Dashboard } from './pages/Dashboard' -import './App.css' +import { AuthProvider } from './contexts/AuthContext'; +import { ProtectedRoute } from './components/ProtectedRoute'; +import { Dashboard } from './pages/Dashboard'; +import './App.css'; function App() { return ( @@ -10,7 +10,7 @@ function App() { - ) + ); } -export default App +export default App; diff --git a/synkronus-portal/src/components/Login.css b/synkronus-portal/src/components/Login.css index 02b53fa12..c35d4a577 100644 --- a/synkronus-portal/src/components/Login.css +++ b/synkronus-portal/src/components/Login.css @@ -19,20 +19,25 @@ left: -50%; width: 200%; height: 200%; - background: + background: radial-gradient(circle at 25% 35%, var(--color-brand-primary-500, #4f7f4e) 0%, transparent 35%), - radial-gradient(circle at 75% 65%, var(--color-brand-secondary-500, #e9b85b) 0%, transparent 35%); + radial-gradient( + circle at 75% 65%, + var(--color-brand-secondary-500, #e9b85b) 0%, + transparent 35% + ); opacity: var(--opacity-6, 0.06); animation: backgroundPulse 25s ease-in-out infinite; pointer-events: none; } @keyframes backgroundPulse { - 0%, 100% { + 0%, + 100% { transform: translate(0, 0) scale(1); opacity: var(--opacity-6, 0.06); } - 50% { + 50% { transform: translate(3%, 3%) scale(1.05); opacity: var(--opacity-8, 0.08); } @@ -46,13 +51,14 @@ padding: var(--spacing-8, 32px); width: 100%; max-width: 420px; - box-shadow: + box-shadow: 0 8px 32px rgba(0, 0, 0, var(--opacity-40, 0.4)), inset 0 1px 0 rgba(255, 255, 255, var(--opacity-5, 0.05)); border: var(--border-width-thin, 1px) solid rgba(79, 127, 78, var(--opacity-20, 0.2)); position: relative; z-index: 1; - animation: slideUpFade var(--duration-slower, 500ms) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); + animation: slideUpFade var(--duration-slower, 500ms) + var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); } @keyframes slideUpFade { @@ -79,7 +85,8 @@ height: var(--logo-sm, 80px); object-fit: contain; filter: drop-shadow(0 4px 12px rgba(79, 127, 78, var(--opacity-30, 0.3))); - transition: filter var(--duration-normal, 250ms) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); + transition: filter var(--duration-normal, 250ms) + var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); } .login-logo:hover { @@ -195,7 +202,8 @@ font-weight: var(--font-weight-medium, 500); font-size: var(--font-size-sm, 14px); backdrop-filter: blur(10px); - animation: slideDown var(--duration-normal, 250ms) var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); + animation: slideDown var(--duration-normal, 250ms) + var(--easing-ease-out, cubic-bezier(0, 0, 0.2, 1)); } @keyframes slideDown { diff --git a/synkronus-portal/src/components/Login.tsx b/synkronus-portal/src/components/Login.tsx index 21483aeaf..f0f5cfaa5 100644 --- a/synkronus-portal/src/components/Login.tsx +++ b/synkronus-portal/src/components/Login.tsx @@ -1,33 +1,33 @@ -import { useState } from 'react' -import type { FormEvent } from 'react' -import { useAuth } from '../contexts/AuthContext' -import { Button, Input } from "@ode/components/react-web"; +import { useState } from 'react'; +import type { FormEvent } from 'react'; +import { useAuth } from '../contexts/AuthContext'; +import { Button, Input } from '@ode/components/react-web'; -import odeLogo from '../assets/ode_logo.png' -import './Login.css' +import odeLogo from '../assets/ode_logo.png'; +import './Login.css'; export function Login() { - const [username, setUsername] = useState('') - const [password, setPassword] = useState('') - const [error, setError] = useState(null) - const [loading, setLoading] = useState(false) - const { login } = useAuth() + const [username, setUsername] = useState(''); + const [password, setPassword] = useState(''); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + const { login } = useAuth(); const handleSubmit = async (e?: FormEvent) => { if (e) { - e.preventDefault() + e.preventDefault(); } - setError(null) - setLoading(true) + setError(null); + setLoading(true); try { - await login({ username, password }) + await login({ username, password }); } catch (err) { - setError(err instanceof Error ? err.message : 'Login failed') + setError(err instanceof Error ? err.message : 'Login failed'); } finally { - setLoading(false) + setLoading(false); } - } + }; return (
@@ -37,9 +37,9 @@ export function Login() {

Synkronus Portal

Sign In

- + {error &&
{error}
} - +
- +
- +
+ - +