diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d664665..6297e07 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,47 +4,58 @@ on: push: branches: [main, dev] pull_request: - branches: [main, dev] + branches: [main] permissions: contents: write +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + jobs: - test: - strategy: - matrix: - os: [ubuntu-latest, macos-latest] - runs-on: ${{ matrix.os }} + check: + name: Check + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: go-version-file: go.mod + cache: true + + - name: Check go.mod tidy + run: | + go mod tidy + git diff --exit-code go.mod go.sum || (echo "FAIL: go.mod/go.sum not tidy" && exit 1) + + - name: Check gofmt + run: | + unformatted=$(gofmt -l .) + if [ -n "$unformatted" ]; then + echo "FAIL: files not formatted:" + echo "$unformatted" + exit 1 + fi - name: Build run: go build ./... - - name: Test - run: go test -race -count=1 ./... + - name: Vet + run: go vet ./... - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - uses: actions/setup-go@v5 + - name: Lint + uses: golangci/golangci-lint-action@v7 with: - go-version-file: go.mod + version: v2.11 - - name: golangci-lint - uses: golangci/golangci-lint-action@v6 - with: - version: latest + - name: Test + run: go test -race ./... release: - needs: [test, lint] - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + name: Release + needs: check + if: github.event_name == 'pull_request' || (github.event_name == 'push' && github.ref == 'refs/heads/main') runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -56,11 +67,43 @@ jobs: go-version-file: go.mod cache: true + - name: GoReleaser snapshot + uses: goreleaser/goreleaser-action@v6 + with: + version: "~> v2" + args: release --snapshot --clean + + - name: Validate release archives + run: | + set -euo pipefail + for tarball in dist/*.tar.gz; do + echo "==> Checking $tarball" + + contents=$(tar tzf "$tarball") + + if echo "$contents" | grep -qx '\./$\|^\.'; then + echo "FAIL: $tarball contains a '.' directory entry" + echo "$contents" | head -20 + exit 1 + fi + + for f in paperflow README.md LICENSE; do + if ! echo "$contents" | grep -q "$f"; then + echo "FAIL: $tarball missing $f" + echo "$contents" + exit 1 + fi + done + + echo " OK" + done + - name: Auto-tag release id: tag + if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | VERSION="v$(cat VERSION)" - if git rev-parse "$VERSION" >/dev/null 2>&1; then + if git tag -l "$VERSION" | grep -q . || git ls-remote --tags origin "$VERSION" | grep -q .; then echo "Tag $VERSION already exists, skipping." echo "created=false" >> "$GITHUB_OUTPUT" else @@ -92,3 +135,46 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} AUR_KEY: ${{ env.AUR_KEY_PATH }} + + nix: + name: Nix + needs: release + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: main + + - uses: DeterminateSystems/nix-installer-action@main + + - name: Verify or update vendorHash + run: | + set +e + output=$(nix build .#paperflow 2>&1) + rc=$? + set -e + + if [ $rc -eq 0 ]; then + echo "vendorHash is up to date" + exit 0 + fi + + new_hash=$(echo "$output" | grep "got:" | awk '{print $2}') + if [ -z "$new_hash" ]; then + echo "::error::Nix build failed but could not extract new hash" + echo "$output" + exit 1 + fi + + echo "Updating vendorHash to $new_hash" + sed -i "s|vendorHash = \".*\"|vendorHash = \"$new_hash\"|" default.nix + + nix build .#paperflow + + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add default.nix + git commit -m "chore: update vendorHash for v$(cat VERSION)" + git push diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 924877e..0000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Release - -on: - push: - tags: - - "v*.*.*" - -permissions: - contents: write - -jobs: - goreleaser: - name: GoReleaser - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - uses: actions/setup-go@v5 - with: - go-version-file: go.mod - cache: true - - - name: Write AUR SSH key - run: | - mkdir -p ~/.ssh - echo "$AUR_KEY_CONTENT" > ~/.ssh/aur_key - chmod 600 ~/.ssh/aur_key - ssh-keyscan -t rsa aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null || true - echo "AUR_KEY_PATH=$HOME/.ssh/aur_key" >> "$GITHUB_ENV" - env: - AUR_KEY_CONTENT: ${{ secrets.AUR_SSH_KEY }} - - - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v6 - with: - version: "~> v2" - args: release --clean - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - HOMEBREW_TAP_GITHUB_TOKEN: ${{ secrets.HOMEBREW_TAP_TOKEN }} - AUR_KEY: ${{ env.AUR_KEY_PATH }} diff --git a/cmd/paperflow/init_cmd.go b/cmd/paperflow/init_cmd.go index 8216a29..2841020 100644 --- a/cmd/paperflow/init_cmd.go +++ b/cmd/paperflow/init_cmd.go @@ -86,7 +86,7 @@ func runInit(f flags) error { paperlessURL, _ = reader.ReadString('\n') paperlessURL = strings.TrimSpace(paperlessURL) if paperlessURL == "" { - return fmt.Errorf("Paperless URL is required for API ingestion") + return fmt.Errorf("paperless URL is required for API ingestion") } fmt.Print("Paperless API token: ") diff --git a/go.mod b/go.mod index 86fce6b..08e4e0f 100644 --- a/go.mod +++ b/go.mod @@ -2,9 +2,9 @@ module github.com/alcxyz/paperflow go 1.24 -require github.com/BurntSushi/toml v1.5.0 - require ( - github.com/fsnotify/fsnotify v1.9.0 // indirect - golang.org/x/sys v0.13.0 // indirect + github.com/BurntSushi/toml v1.5.0 + github.com/fsnotify/fsnotify v1.9.0 ) + +require golang.org/x/sys v0.13.0 // indirect diff --git a/internal/config/config.go b/internal/config/config.go index dfa9301..d9d64d0 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -51,8 +51,8 @@ type ExcludeConfig struct { // DefaultConfig returns a config with sensible defaults. func DefaultConfig() *Config { return &Config{ - WatchDir: "~/Documents", - Ingest: "none", + WatchDir: "~/Documents", + Ingest: "none", IngestDir: "~/paperless-ingest", IngestArchiveAfter: "5m", Notifications: NotificationsConfig{ diff --git a/internal/ingest/api.go b/internal/ingest/api.go index 412cd24..97c9369 100644 --- a/internal/ingest/api.go +++ b/internal/ingest/api.go @@ -24,7 +24,7 @@ func CheckAPI(paperlessURL string, token string) error { if err != nil { return fmt.Errorf("connecting to paperless: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode == http.StatusUnauthorized || resp.StatusCode == http.StatusForbidden { return fmt.Errorf("authentication failed (HTTP %d)", resp.StatusCode) @@ -42,7 +42,7 @@ func IngestAPI(filePath string, paperlessURL string, token string) error { if err != nil { return fmt.Errorf("opening file: %w", err) } - defer file.Close() + defer func() { _ = file.Close() }() filename := filepath.Base(filePath) ext := filepath.Ext(filename) @@ -78,7 +78,7 @@ func IngestAPI(filePath string, paperlessURL string, token string) error { if err != nil { return fmt.Errorf("uploading to paperless: %w", err) } - defer resp.Body.Close() + defer func() { _ = resp.Body.Close() }() if resp.StatusCode != http.StatusOK { body, _ := io.ReadAll(resp.Body) diff --git a/internal/ingest/api_test.go b/internal/ingest/api_test.go index 085f099..63d544e 100644 --- a/internal/ingest/api_test.go +++ b/internal/ingest/api_test.go @@ -92,7 +92,7 @@ func TestIngestAPI_Success(t *testing.T) { if err != nil { t.Fatalf("FormFile: %v", err) } - defer file.Close() + defer func() { _ = file.Close() }() receivedFilename = header.Filename receivedContent, _ = io.ReadAll(file) diff --git a/internal/ingest/archiver.go b/internal/ingest/archiver.go index 18b51c8..f6da23e 100644 --- a/internal/ingest/archiver.go +++ b/internal/ingest/archiver.go @@ -107,13 +107,13 @@ func moveFile(src, dst string) error { if err != nil { return err } - defer in.Close() + defer func() { _ = in.Close() }() out, err := os.Create(dst) if err != nil { return err } - defer out.Close() + defer func() { _ = out.Close() }() if _, err := io.Copy(out, in); err != nil { return err diff --git a/internal/ingest/directory.go b/internal/ingest/directory.go index 549f2f0..8fe4154 100644 --- a/internal/ingest/directory.go +++ b/internal/ingest/directory.go @@ -26,13 +26,13 @@ func IngestDirectory(path string, ingestDir string) (string, error) { if err != nil { return "", err } - defer in.Close() + defer func() { _ = in.Close() }() out, err := os.Create(destPath) if err != nil { return "", err } - defer out.Close() + defer func() { _ = out.Close() }() if _, err := io.Copy(out, in); err != nil { return "", err diff --git a/internal/notify/notify.go b/internal/notify/notify.go index bccaedd..b5e2a7e 100644 --- a/internal/notify/notify.go +++ b/internal/notify/notify.go @@ -75,7 +75,6 @@ func (n *Notifier) Send(title, body string) { } } - // Close flushes any remaining pending results and stops the timer. func (n *Notifier) Close() { if !n.enabled { diff --git a/internal/organizer/organizer.go b/internal/organizer/organizer.go index d994ac1..09e8dbb 100644 --- a/internal/organizer/organizer.go +++ b/internal/organizer/organizer.go @@ -108,13 +108,13 @@ func moveFile(src, dst string) error { if err != nil { return err } - defer in.Close() + defer func() { _ = in.Close() }() out, err := os.Create(dst) if err != nil { return err } - defer out.Close() + defer func() { _ = out.Close() }() if _, err := io.Copy(out, in); err != nil { return err diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 48614f7..8616592 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -57,7 +57,7 @@ func (w *Watcher) Run() error { if err != nil { return err } - defer fsw.Close() + defer func() { _ = fsw.Close() }() if err := fsw.Add(w.config.WatchDir); err != nil { w.notifier.Send("Failed to start", fmt.Sprintf("Cannot watch %s: %v", w.config.WatchDir, err))