diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..63851c0
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,515 @@
+name: CI/CD
+
+on:
+ push:
+ branches: [main, master]
+ # semantic version tags + rc/beta snapshots
+ tags:
+ - 'v*'
+ - 'v*.*.*'
+ - 'v*.*.*-rc*'
+ - 'v*.*.*-beta*'
+ - 'test-*'
+ pull_request:
+ types: [opened, synchronize, reopened, ready_for_review, closed]
+ branches: [main, master]
+ release:
+ types: [published]
+ workflow_dispatch:
+ inputs:
+ mode:
+ description: "Pipeline mode"
+ required: true
+ default: "lint-fix"
+ type: choice
+ options:
+ - lint-fix
+ - build
+ - release-major
+ - release-minor
+ - release-patch
+ - release-test
+ - release-rc
+ - release-alpha
+ - monthly-maintenance
+ release_version_override:
+ description: "Optional explicit release version (for example 2.4.0 or 2.4.0-rc.2)"
+ required: false
+ default: ""
+ type: string
+ allow_prs:
+ description: "Allow automation to open pull requests"
+ required: false
+ default: true
+ type: boolean
+ schedule:
+ # preferred heavy monthly run (quota reset strategy)
+ - cron: '17 3 1 * *'
+ # optional nightly lightweight checks
+ - cron: '41 2 * * *'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }}
+ cancel-in-progress: true
+
+permissions:
+ contents: write
+ discussions: write
+ pull-requests: write
+ checks: write
+ packages: write
+ security-events: write
+
+jobs:
+ route:
+ name: Route event
+ runs-on: ubuntu-latest
+ outputs:
+ run_code_checks: ${{ steps.route.outputs.run_code_checks }}
+ run_pr_meta_checks: ${{ steps.route.outputs.run_pr_meta_checks }}
+ run_cleanup: ${{ steps.route.outputs.run_cleanup }}
+ run_release: ${{ steps.route.outputs.run_release }}
+ is_monthly: ${{ steps.route.outputs.is_monthly }}
+ is_nightly: ${{ steps.route.outputs.is_nightly }}
+ steps:
+ - id: route
+ shell: bash
+ run: |
+ set -euo pipefail
+
+ run_code_checks=false
+ run_pr_meta_checks=false
+ run_cleanup=false
+ run_release=false
+ is_monthly=false
+ is_nightly=false
+
+ case "${{ github.event_name }}" in
+ push)
+ run_code_checks=true
+ ;;
+ pull_request)
+ if [[ "${{ github.event.action }}" == "closed" ]]; then
+ run_cleanup=true
+ else
+ run_pr_meta_checks=true
+ # In practice, also run code checks on PRs so lint/fmt/vet/test
+ # show up directly in the PR UI. Use concurrency to collapse churn.
+ run_code_checks=true
+ fi
+ ;;
+ release)
+ run_release=true
+ ;;
+ workflow_dispatch)
+ run_code_checks=true
+ if [[ "${{ inputs.mode }}" == release-* ]]; then
+ run_release=true
+ fi
+ if [[ "${{ inputs.mode }}" == "monthly-maintenance" ]]; then
+ is_monthly=true
+ fi
+ if [[ "${{ inputs.mode }}" == "lint-fix" ]]; then
+ # Manual lint-fix acts as an on-demand nightly-style maintenance pass.
+ is_nightly=true
+ fi
+ ;;
+ schedule)
+ run_code_checks=true
+ if [[ "${{ github.event.schedule }}" == "17 3 1 * *" ]]; then
+ is_monthly=true
+ fi
+ if [[ "${{ github.event.schedule }}" == "41 2 * * *" ]]; then
+ is_nightly=true
+ fi
+ ;;
+ esac
+
+ echo "run_code_checks=$run_code_checks" >> "$GITHUB_OUTPUT"
+ echo "run_pr_meta_checks=$run_pr_meta_checks" >> "$GITHUB_OUTPUT"
+ echo "run_cleanup=$run_cleanup" >> "$GITHUB_OUTPUT"
+ echo "run_release=$run_release" >> "$GITHUB_OUTPUT"
+ echo "is_monthly=$is_monthly" >> "$GITHUB_OUTPUT"
+ echo "is_nightly=$is_nightly" >> "$GITHUB_OUTPUT"
+
+ prepare-release-tag:
+ name: Prepare release tag
+ needs: [route]
+ if: ${{ github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-') }}
+ runs-on: ubuntu-latest
+ outputs:
+ release_tag: ${{ steps.tag.outputs.release_tag }}
+ next_version: ${{ steps.tag.outputs.next_version }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Setup git-tag-inc
+ uses: arran4/git-tag-inc-action@v1
+ with:
+ mode: install
+ # Do not also run `go install github.com/arran4/git-tag-inc/...` in this job.
+ # Using both is redundant and has caused avoidable CI drift.
+ - id: tag
+ shell: bash
+ run: |
+ set -euo pipefail
+ git config --global user.name "github-actions[bot]"
+ git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com"
+ MODE="${{ inputs.mode }}"
+ OVERRIDE="${{ inputs.release_version_override }}"
+
+ if [[ -n "$OVERRIDE" ]]; then
+ # Accept "1.2.3" or "v1.2.3" override input.
+ OVERRIDE="${OVERRIDE#v}"
+ next_tag="v$OVERRIDE"
+ else
+ case "$MODE" in
+ release-major) level="major"; suffix="" ;;
+ release-minor) level="minor"; suffix="" ;;
+ release-patch) level="patch"; suffix="" ;;
+ release-test) level="patch"; suffix="test" ;;
+ release-rc) level="patch"; suffix="rc" ;;
+ release-alpha) level="patch"; suffix="alpha" ;;
+ *) echo "Unsupported release mode: $MODE"; exit 1 ;;
+ esac
+ if command -v git-tag-inc >/dev/null 2>&1; then
+ # git-tag-inc uses positional commands (patch/major/minor/test/rc...)
+ # and NOT flag forms like -patch.
+ level="${level#-}"
+ args=(-print-version-only "$level")
+ [[ -n "$suffix" ]] && args+=("$suffix")
+ next_tag=$(git-tag-inc "${args[@]}")
+ else
+ # Fallback implementation when git-tag-inc is not available.
+ git fetch --tags --force
+ latest=$(git tag -l 'v*' | sed 's/^v//' | sort -V | tail -n 1)
+ [[ -z "$latest" ]] && latest='0.0.0'
+
+ # Prefer npx semver if available (same pattern used in g2 fixes).
+ if command -v npx >/dev/null 2>&1; then
+ case "$level" in
+ major) bumped=$(npx --yes semver "$latest" -i major) ;;
+ minor) bumped=$(npx --yes semver "$latest" -i minor) ;;
+ *) bumped=$(npx --yes semver "$latest" -i patch) ;;
+ esac
+ next_tag="v${bumped}"
+ else
+ base="${latest%%-*}"
+ IFS='.' read -r maj min pat <<< "$base"
+ case "$level" in
+ major) maj=$((maj+1)); min=0; pat=0 ;;
+ minor) min=$((min+1)); pat=0 ;;
+ *) pat=$((pat+1)) ;;
+ esac
+ next_tag="v${maj}.${min}.${pat}"
+ fi
+
+ if [[ -n "$suffix" ]]; then
+ next_tag="${next_tag}-${suffix}.1"
+ fi
+ fi
+ fi
+
+ # Tagging safety guards to avoid duplicate/invalid release states.
+ [[ "$next_tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.]+)?$ ]] || {
+ echo "Invalid tag format: $next_tag" >&2
+ exit 1
+ }
+ git fetch --tags --force
+ if git rev-parse "$next_tag" >/dev/null 2>&1; then
+ echo "Tag already exists: $next_tag" >&2
+ echo "Choose a new mode or set release_version_override." >&2
+ exit 1
+ fi
+
+ echo "release_tag=$next_tag" >> "$GITHUB_OUTPUT"
+ clean_tag="${next_tag#v}"; clean_tag="${clean_tag%%-*}"
+ IFS='.' read -r maj min pat <<< "$clean_tag"
+ echo "next_version=${maj:-0}.${min:-0}.$(( ${pat:-0} + 1 ))-SNAPSHOT" >> "$GITHUB_OUTPUT"
+
+ discover:
+ name: Discover capabilities and cost profile
+ needs: route
+ runs-on: ubuntu-latest
+ outputs:
+ profile: ${{ steps.profile.outputs.profile }}
+ has_go: ${{ steps.detect.outputs.has_go }}
+ has_node: ${{ steps.detect.outputs.has_node }}
+ has_dart: ${{ steps.detect.outputs.has_dart }}
+ has_flutter: ${{ steps.detect.outputs.has_flutter }}
+ has_qt_cpp: ${{ steps.detect.outputs.has_qt_cpp }}
+ has_make_c: ${{ steps.detect.outputs.has_make_c }}
+ has_docker: ${{ steps.detect.outputs.has_docker }}
+ has_goreleaser: ${{ steps.detect.outputs.has_goreleaser }}
+ has_dart_or_flutter_tests: ${{ steps.detect.outputs.has_dart_or_flutter_tests }}
+ has_packaging: ${{ steps.detect.outputs.has_packaging }}
+ steps:
+ - uses: actions/checkout@v4
+
+ # Template-time toggles (set these once for the repo; avoid broad auto-detection)
+ # EXPECT_GO=true
+ # EXPECT_NODE=false
+ # EXPECT_DART=false
+ # EXPECT_FLUTTER=false
+ # EXPECT_QT_CPP=false
+ # EXPECT_MAKE_C=false
+ # EXPECT_DOCKER=false
+ # EXPECT_GORELEASER=true
+
+ - id: detect
+ shell: bash
+ run: |
+ set -euo pipefail
+ # Keep this minimal: most language choices should be decided at workflow install/customization time.
+ # Runtime checks stay for optional tests and packaging folders.
+
+ echo "has_go=${EXPECT_GO:-true}" >> "$GITHUB_OUTPUT"
+ echo "has_node=${EXPECT_NODE:-false}" >> "$GITHUB_OUTPUT"
+ echo "has_dart=${EXPECT_DART:-false}" >> "$GITHUB_OUTPUT"
+ echo "has_flutter=${EXPECT_FLUTTER:-false}" >> "$GITHUB_OUTPUT"
+ echo "has_qt_cpp=${EXPECT_QT_CPP:-false}" >> "$GITHUB_OUTPUT"
+ echo "has_make_c=${EXPECT_MAKE_C:-false}" >> "$GITHUB_OUTPUT"
+ echo "has_docker=${EXPECT_DOCKER:-true}" >> "$GITHUB_OUTPUT"
+ echo "has_goreleaser=${EXPECT_GORELEASER:-true}" >> "$GITHUB_OUTPUT"
+
+ ([[ -d test ]] || [[ -d tests ]] || [[ -f pubspec.yaml ]]) && echo "has_dart_or_flutter_tests=true" >> "$GITHUB_OUTPUT" || echo "has_dart_or_flutter_tests=false" >> "$GITHUB_OUTPUT"
+ ([[ -d packaging ]] || [[ -d pkg ]] || [[ -f debian/control ]]) && echo "has_packaging=true" >> "$GITHUB_OUTPUT" || echo "has_packaging=false" >> "$GITHUB_OUTPUT"
+
+ - id: profile
+ shell: bash
+ run: |
+ set -euo pipefail
+ # repo visibility is authoritative
+ if [[ "${{ github.event.repository.private }}" == "true" ]]; then
+ echo "profile=private" >> "$GITHUB_OUTPUT"
+ else
+ echo "profile=public" >> "$GITHUB_OUTPUT"
+ fi
+
+ gitleaks:
+ name: Secret scan
+ needs: [route, discover]
+ if: ${{ needs.route.outputs.run_cleanup != 'true' && (needs.route.outputs.is_nightly == 'true' || needs.route.outputs.is_monthly == 'true') }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: gitleaks/gitleaks-action@v2
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+
+ dependency-review:
+ name: Dependency review (public/full)
+ needs: [route, discover]
+ if: ${{ needs.discover.outputs.profile == 'public' && github.event_name == 'pull_request' && github.event.action != 'closed' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/dependency-review-action@v4
+
+ golangci:
+ name: lint
+ needs: [route, discover]
+ if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - name: golangci-lint
+ uses: golangci/golangci-lint-action@v6
+ with:
+ version: latest
+
+ go-test:
+ name: Go lint/test (${{ matrix.os }})
+ needs: [route, discover, golangci]
+ if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' }}
+ runs-on: ${{ matrix.os }}
+ strategy:
+ fail-fast: false
+ matrix:
+ # Cost-aware default: Ubuntu only.
+ # Add windows-latest/macos-latest only for true platform-specific behavior.
+ os: [ubuntu-latest]
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+ - name: Test
+ run: go test ./... -v
+
+ go-vet:
+ name: Go vet
+ needs: [route, discover]
+ if: ${{ needs.discover.outputs.has_go == 'true' && needs.route.outputs.run_code_checks == 'true' && needs.discover.outputs.profile == 'public' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ cache: true
+ - run: go vet ./...
+
+
+ docker-build:
+ name: Docker build
+ needs: [route, discover]
+ if: ${{ needs.discover.outputs.has_docker == 'true' && needs.route.outputs.run_release == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - uses: docker/setup-qemu-action@v3
+ - uses: docker/setup-buildx-action@v3
+ - uses: docker/build-push-action@v6
+ with:
+ context: .
+ file: ${{ hashFiles('Dockerfile.goreleaser') != '' && 'Dockerfile.goreleaser' || 'Dockerfile' }}
+ push: false
+ tags: ghcr.io/${{ github.repository }}:ci-${{ github.run_id }}
+
+ autofix:
+ name: Auto-format and open PR
+ needs: [route, discover]
+ if: ${{ github.event_name == 'workflow_dispatch' && inputs.mode == 'lint-fix' && inputs.allow_prs == true }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Setup Go (if needed)
+ if: ${{ needs.discover.outputs.has_go == 'true' }}
+ uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+
+ - name: Run autofix formatters
+ shell: bash
+ run: |
+ set -euo pipefail
+ if [[ "${{ needs.discover.outputs.has_go }}" == "true" ]]; then
+ go fix ./... || true
+ go fmt ./... || true
+ fi
+
+ - name: Create PR if changes exist
+ env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ shell: bash
+ run: |
+ set -euo pipefail
+ if git diff --quiet; then
+ echo "No changes; exiting."
+ exit 0
+ fi
+
+ git config user.name "github-actions[bot]"
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
+
+ PARENT_PR="${{ github.event.pull_request.number || 'none' }}"
+ BRANCH="ci/autofix/${{ github.run_id }}-parent-${PARENT_PR}"
+
+ git checkout -b "$BRANCH"
+ git add -A
+ git commit -m "ci: automated formatting fixes"
+ git push origin "$BRANCH"
+
+ gh pr create \
+ --title "ci: automated formatting fixes" \
+ --body "Automated formatting pass. Parent-PR: ${PARENT_PR}" \
+ --base main \
+ --head "$BRANCH" \
+ --label "ci-autofix"
+
+ cleanup-autofix-prs:
+ name: Cleanup autofix PRs on parent close
+ needs: [route]
+ if: ${{ needs.route.outputs.run_cleanup == 'true' }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ - env:
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ PARENT_PR: ${{ github.event.pull_request.number }}
+ run: |
+ set -euo pipefail
+ gh pr list --state open --search "label:ci-autofix in:title" --json number,headRefName,body | \
+ jq -r '.[] | select(.body | contains("Parent-PR: '"$PARENT_PR"'")) | [.number, .headRefName] | @tsv' | \
+ while IFS=$'\t' read -r pr branch; do
+ gh pr close "$pr" --comment "Closing auto-fix PR because parent PR #$PARENT_PR was closed."
+ git push origin --delete "$branch" || true
+ done
+
+ goreleaser:
+ name: GoReleaser
+ needs: [route, discover, go-test, prepare-release-tag]
+ if: ${{ !failure() && !cancelled() && needs.discover.outputs.has_go == 'true' && needs.discover.outputs.has_goreleaser == 'true' && (((github.event_name == 'push') && startsWith(github.ref, 'refs/tags/v')) || (github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-'))) }}
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - uses: actions/setup-go@v5
+ with:
+ go-version-file: go.mod
+ - name: Tag commit for release (workflow_dispatch)
+ if: ${{ github.event_name == 'workflow_dispatch' && startsWith(inputs.mode, 'release-') }}
+ run: git tag ${{ needs.prepare-release-tag.outputs.release_tag }}
+
+ - name: Determine tag details
+ id: tag_info
+ run: |
+ if [[ "${{ github.event_name }}" == "workflow_dispatch" && startsWith("${{ inputs.mode }}", "release-") ]]; then
+ TAG_NAME="${{ needs.prepare-release-tag.outputs.release_tag }}"
+ else
+ TAG_NAME="${GITHUB_REF#refs/tags/}"
+ fi
+ echo "tag=${TAG_NAME}" >> "$GITHUB_OUTPUT"
+ if [[ "$TAG_NAME" == *"-"* ]]; then
+ echo "pre_release=true" >> "$GITHUB_OUTPUT"
+ echo "LATEST_TAG=next" >> "$GITHUB_ENV"
+ else
+ echo "pre_release=false" >> "$GITHUB_OUTPUT"
+ echo "LATEST_TAG=latest" >> "$GITHUB_ENV"
+ fi
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+ - name: Login to Docker Hub
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_USERNAME }}
+ password: ${{ secrets.DOCKER_PASSWORD }}
+ - name: Env setup
+ run: |
+ sudo apt-get update
+ sudo apt-get install -y build-essential
+ - name: Build gobookmarks binary for Docker smoke test
+ run: go build -o gobookmarks ./cmd/gobookmarks
+ - name: Build Docker image (smoke test)
+ run: docker build --tag gobookmarks:test .
+ - name: Smoke test Docker image
+ run: docker run --rm gobookmarks:test version
+ - name: Clean up Docker smoke test artifacts
+ run: |
+ rm -f gobookmarks
+ docker image rm gobookmarks:test || true
+
+ - name: Run GoReleaser
+ uses: goreleaser/goreleaser-action@v6
+ with:
+ distribution: goreleaser
+ version: latest
+ args: >-
+ release --clean
+ ${{ (github.event_name == 'workflow_dispatch' && (inputs.mode == 'release-test' || inputs.mode == 'release-rc' || inputs.mode == 'release-alpha')) && '--snapshot' || '' }}
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ GORELEASER_CURRENT_TAG: ${{ github.event_name == 'workflow_dispatch' && needs.prepare-release-tag.outputs.release_tag || github.ref_name }}
diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml
deleted file mode 100644
index 8598cf7..0000000
--- a/.github/workflows/goreleaser.yml
+++ /dev/null
@@ -1,72 +0,0 @@
-name: goreleaser
-
-on:
- push:
- tags:
- - 'v*.*.*'
- - 'v*.*.*-*'
-
-permissions:
- contents: write
- packages: write
-
-jobs:
- goreleaser:
- runs-on: ubuntu-latest
- steps:
- - name: Checkout
- uses: actions/checkout@v4
- with:
- fetch-depth: 0
- - name: Set up Go
- uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
-
- - name: Determine tag details
- id: tag_info
- run: |
- TAG_NAME="${GITHUB_REF#refs/tags/}"
- echo "tag=${TAG_NAME}" >> "$GITHUB_OUTPUT"
- if [[ "$TAG_NAME" == *"-"* ]]; then
- echo "pre_release=true" >> "$GITHUB_OUTPUT"
- echo "LATEST_TAG=next" >> "$GITHUB_ENV"
- else
- echo "pre_release=false" >> "$GITHUB_OUTPUT"
- echo "LATEST_TAG=latest" >> "$GITHUB_ENV"
- fi
- - name: Login to GitHub Container Registry
- uses: docker/login-action@v3
- with:
- registry: ghcr.io
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
- - name: Login to Docker Hub
- uses: docker/login-action@v3
- with:
- username: ${{ secrets.DOCKER_USERNAME }}
- password: ${{ secrets.DOCKER_PASSWORD }}
- - name: Env setup
- run: |
- sudo apt-get update
- sudo apt-get install -y build-essential
- - name: Test
- run: go test ./...
- - name: Build gobookmarks binary for Docker smoke test
- run: go build -o gobookmarks ./cmd/gobookmarks
- - name: Build Docker image (smoke test)
- run: docker build --tag gobookmarks:test .
- - name: Smoke test Docker image
- run: docker run --rm gobookmarks:test version
- - name: Clean up Docker smoke test artifacts
- run: |
- rm -f gobookmarks
- docker image rm gobookmarks:test || true
- - name: Run GoReleaser
- uses: goreleaser/goreleaser-action@v6
- with:
- distribution: goreleaser
- version: latest
- args: release --clean
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
deleted file mode 100644
index 961abfe..0000000
--- a/.github/workflows/test.yml
+++ /dev/null
@@ -1,18 +0,0 @@
-name: test
-
-on:
- push:
- branches: [ main ]
- pull_request:
- branches: [ main ]
-
-jobs:
- test:
- runs-on: ubuntu-latest
- steps:
- - uses: actions/checkout@v4
- - uses: actions/setup-go@v5
- with:
- go-version-file: go.mod
- - run: go test ./...
- - run: go vet ./...
diff --git a/authHandlers.go b/authHandlers.go
index ddfb755..9f51b87 100644
--- a/authHandlers.go
+++ b/authHandlers.go
@@ -106,11 +106,6 @@ func LoginWithProvider(w http.ResponseWriter, r *http.Request) error {
func Oauth2CallbackPage(w http.ResponseWriter, r *http.Request) error {
- type ErrorData struct {
- *CoreData
- Error string
- }
-
session, err := getSession(w, r)
if session, err = sanitizeSession(w, r, session, err); err != nil {
return fmt.Errorf("session error: %w", err)
diff --git a/autoRefreshPage.go b/autoRefreshPage.go
index e80036e..1cc9ca6 100644
--- a/autoRefreshPage.go
+++ b/autoRefreshPage.go
@@ -23,9 +23,3 @@ func TaskDoneAutoRefreshPage(w http.ResponseWriter, r *http.Request) error {
}
return nil
}
-
-func taskRedirectWithoutQueryArgs(w http.ResponseWriter, r *http.Request) {
- u := r.URL
- u.RawQuery = ""
- http.Redirect(w, r, u.String(), http.StatusSeeOther)
-}
diff --git a/bookmark_model.go b/bookmark_model.go
index 9504c08..60aac4a 100644
--- a/bookmark_model.go
+++ b/bookmark_model.go
@@ -563,20 +563,3 @@ func FindPageBySha(tabs BookmarkList, sha string) *BookmarkPage {
return nil
}
-// indexAfterColumn returns the global index after the last category in the specified column.
-func indexAfterColumn(tabs BookmarkList, page *BookmarkPage, colIdx int) int {
- idx := 0
- for _, t := range tabs {
- for _, p := range t.Pages {
- for _, b := range p.Blocks {
- for ci, col := range b.Columns {
- idx += len(col.Categories)
- if p == page && ci == colIdx {
- return idx
- }
- }
- }
- }
- }
- return idx
-}
diff --git a/cmd/gobookmarks/help_command.go b/cmd/gobookmarks/help_command.go
index 1386593..d901ca1 100644
--- a/cmd/gobookmarks/help_command.go
+++ b/cmd/gobookmarks/help_command.go
@@ -40,7 +40,7 @@ func (c *HelpCommand) Execute(args []string) error {
}
}
}
- c.FlagSet().Parse(args)
+ _ = c.FlagSet().Parse(args)
c.FlagSet().Usage = func() {}
printHelp(target, nil)
return nil
diff --git a/cmd/gobookmarks/serve.go b/cmd/gobookmarks/serve.go
index dc1e3ae..8818cba 100644
--- a/cmd/gobookmarks/serve.go
+++ b/cmd/gobookmarks/serve.go
@@ -475,12 +475,12 @@ func runHandlerChain(chain ...any) func(http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
for _, each := range chain {
switch each := each.(type) {
- case http.Handler:
- each.ServeHTTP(w, r)
case http.HandlerFunc:
each(w, r)
case func(http.ResponseWriter, *http.Request):
each(w, r)
+ case http.Handler:
+ each.ServeHTTP(w, r)
case func(http.ResponseWriter, *http.Request) error:
if err := each(w, r); err != nil {
if errors.Is(err, ErrHandled) {
diff --git a/cmd/gobookmarks/test_verification_template_command.go b/cmd/gobookmarks/test_verification_template_command.go
index 23f9b3f..1b9724a 100644
--- a/cmd/gobookmarks/test_verification_template_command.go
+++ b/cmd/gobookmarks/test_verification_template_command.go
@@ -149,9 +149,6 @@ https://example.com Example Link
if input.Bookmarks != "" {
bookmarksStr = input.Bookmarks
}
- } else {
- // Just to debug if set is true or not
- // fmt.Println("DEBUG: DataFromJsonFile is NOT set")
}
// Create a dummy request to build the context
@@ -303,14 +300,14 @@ https://example.com Example Link
// For serving, we need to handle main.css and favicon too, otherwise the page looks broken
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write(output)
+ _, _ = w.Write(output)
})
mux.HandleFunc("/main.css", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/css")
- w.Write(GetMainCSSData())
+ _, _ = w.Write(GetMainCSSData())
})
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
- w.Write(GetFavicon())
+ _, _ = w.Write(GetFavicon())
})
// Also proxy/favicon if possible, but that might require internet or network
mux.HandleFunc("/proxy/favicon", func(w http.ResponseWriter, r *http.Request) {
diff --git a/config.go b/config.go
index 4f365a8..f54304d 100644
--- a/config.go
+++ b/config.go
@@ -4,7 +4,6 @@ import (
"bufio"
"encoding/json"
"fmt"
- "io/ioutil"
"log"
"os"
"path/filepath"
@@ -93,7 +92,7 @@ func LoadConfigFile(path string) (Configuration, bool, error) {
log.Printf("attempting to load config from %s", path)
- data, err := ioutil.ReadFile(path)
+ data, err := os.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
log.Printf("config file %s not found", path)
diff --git a/favicon_proxy_test.go b/favicon_proxy_test.go
index a26ec33..25112e7 100644
--- a/favicon_proxy_test.go
+++ b/favicon_proxy_test.go
@@ -12,13 +12,13 @@ func newFaviconServer(t *testing.T, icon []byte) (*httptest.Server, *int) {
hits := 0
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
- w.Write([]byte(""))
+ _, _ = w.Write([]byte(""))
})
mux.HandleFunc("/favicon.ico", func(w http.ResponseWriter, r *http.Request) {
hits++
w.Header().Set("Cache-Control", "max-age=1")
w.Header().Set("Content-Type", "image/png")
- w.Write(icon)
+ _, _ = w.Write(icon)
})
return httptest.NewServer(mux), &hits
}
diff --git a/funcs.go b/funcs.go
index d703c54..15e5c11 100644
--- a/funcs.go
+++ b/funcs.go
@@ -221,10 +221,10 @@ func NewFuncs(r *http.Request) template.FuncMap {
ref := r.URL.Query().Get("ref")
bookmarks, _, err := GetBookmarks(r.Context(), login, ref, token)
- var bookmark = defaultBookmarks
+ var bookmark string
if err != nil {
if errors.Is(err, ErrRepoNotFound) {
- bookmark = ""
+ bookmark = defaultBookmarks
} else {
return nil, fmt.Errorf("bookmarkPages: %w", err)
}
@@ -250,10 +250,10 @@ func NewFuncs(r *http.Request) template.FuncMap {
ref := r.URL.Query().Get("ref")
bookmarks, _, err := GetBookmarks(r.Context(), login, ref, token)
- var bookmark = defaultBookmarks
+ var bookmark string
if err != nil {
if errors.Is(err, ErrRepoNotFound) {
- bookmark = ""
+ bookmark = defaultBookmarks
} else {
return nil, fmt.Errorf("bookmarkTabs: %w", err)
}
@@ -290,10 +290,10 @@ func NewFuncs(r *http.Request) template.FuncMap {
ref := r.URL.Query().Get("ref")
bookmarks, _, err := GetBookmarks(r.Context(), login, ref, token)
- var bookmark = defaultBookmarks
+ var bookmark string
if err != nil {
if errors.Is(err, ErrRepoNotFound) {
- bookmark = ""
+ bookmark = defaultBookmarks
} else {
return nil, fmt.Errorf("bookmarkTabsWithPages: %w", err)
}
@@ -332,10 +332,10 @@ func NewFuncs(r *http.Request) template.FuncMap {
}
bookmarks, _, err := GetBookmarks(r.Context(), login, r.URL.Query().Get("ref"), token)
- var bookmark = defaultBookmarks
+ var bookmark string
if err != nil {
if errors.Is(err, ErrRepoNotFound) {
- bookmark = ""
+ bookmark = defaultBookmarks
} else {
return ""
}
@@ -364,10 +364,10 @@ func NewFuncs(r *http.Request) template.FuncMap {
}
bookmarks, _, err := GetBookmarks(r.Context(), login, r.URL.Query().Get("ref"), token)
- var bookmark = defaultBookmarks
+ var bookmark string
if err != nil {
if errors.Is(err, ErrRepoNotFound) {
- bookmark = ""
+ bookmark = defaultBookmarks
} else {
return nil, fmt.Errorf("bookmarkColumns: %w", err)
}
diff --git a/go.mod b/go.mod
index 38d4873..2210923 100644
--- a/go.mod
+++ b/go.mod
@@ -1,6 +1,6 @@
module github.com/arran4/gobookmarks
-go 1.25.0
+go 1.24.0
require (
github.com/PuerkitoBio/goquery v1.8.1
@@ -15,7 +15,7 @@ require (
github.com/mattn/go-sqlite3 v1.14.17
github.com/xanzy/go-gitlab v0.115.0
golang.org/x/crypto v0.45.0
- golang.org/x/image v0.38.0
+ golang.org/x/image v0.24.0
golang.org/x/oauth2 v0.27.0
)
diff --git a/go.sum b/go.sum
index c0a7b06..9e4fead 100644
--- a/go.sum
+++ b/go.sum
@@ -110,8 +110,8 @@ golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
-golang.org/x/image v0.38.0 h1:5l+q+Y9JDC7mBOMjo4/aPhMDcxEptsX+Tt3GgRQRPuE=
-golang.org/x/image v0.38.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
+golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
+golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
@@ -147,8 +147,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
-golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
+golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
+golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
diff --git a/provider_access.go b/provider_access.go
index ac7a8e7..c1f041e 100644
--- a/provider_access.go
+++ b/provider_access.go
@@ -52,12 +52,6 @@ func getCachedBookmarks(user, ref string) (string, string, bool) {
return entry.bookmarks, entry.sha, true
}
-func setCachedBookmarks(user, ref, bookmarks, sha string) {
- key := cacheKey(user, ref)
- bookmarksCache.Lock()
- bookmarksCache.data[key] = &bookmarkCacheEntry{bookmarks: bookmarks, sha: sha, expiry: time.Now().Add(time.Minute)}
- bookmarksCache.Unlock()
-}
func invalidateBookmarkCache(user string) {
bookmarksCache.Lock()
diff --git a/provider_github.go b/provider_github.go
index 5078def..6649513 100644
--- a/provider_github.go
+++ b/provider_github.go
@@ -50,7 +50,7 @@ func (GitHubProvider) client(ctx context.Context, token *oauth2.Token) *github.C
if server == "" || server == "https://github.com" {
return github.NewClient(httpClient)
}
- c, err := github.NewEnterpriseClient(server+"/api/v3/", server+"/upload/v3/", httpClient)
+ c, err := github.NewClient(httpClient).WithEnterpriseURLs(server+"/api/v3/", server+"/upload/v3/")
if err != nil {
return github.NewClient(httpClient)
}
@@ -150,7 +150,8 @@ func (p GitHubProvider) GetBookmarks(ctx context.Context, user, ref string, toke
var commitAuthor = &github.CommitAuthor{Name: SP("Gobookmarks"), Email: SP("Gobookmarks@arran.net.au")}
-func (p GitHubProvider) getDefaultBranch(ctx context.Context, user string, client *github.Client, branch string) (string, error) {
+func (p GitHubProvider) getDefaultBranch(ctx context.Context, user string, client *github.Client, _ string) (string, error) {
+ var branch string
rep, resp, err := client.Repositories.Get(ctx, user, Config.GetRepoName())
if resp != nil && resp.StatusCode == 404 {
return "", ErrRepoNotFound
diff --git a/provider_gitlab.go b/provider_gitlab.go
index 9d95a7d..f09e8df 100644
--- a/provider_gitlab.go
+++ b/provider_gitlab.go
@@ -53,6 +53,7 @@ func (GitLabProvider) Config(clientID, clientSecret, redirectURL string) *oauth2
}
}
+//nolint:staticcheck
func (GitLabProvider) client(token *oauth2.Token) (*gitlab.Client, error) {
server := Config.GitlabServer
if server == "" {
@@ -182,7 +183,9 @@ func (GitLabProvider) GetBookmarks(ctx context.Context, user, ref string, token
return string(data), f.LastCommitID, nil
}
-func (GitLabProvider) getDefaultBranch(ctx context.Context, user string, client *gitlab.Client, branch string) (string, error) {
+//nolint:staticcheck
+func (GitLabProvider) getDefaultBranch(ctx context.Context, user string, client *gitlab.Client, _ string) (string, error) {
+ var branch string
p, _, err := client.Projects.GetProject(user+"/"+Config.GetRepoName(), nil)
if err != nil {
if respErr, ok := err.(*gitlab.ErrorResponse); ok {
diff --git a/provider_sql.go b/provider_sql.go
index f604cce..563bd61 100644
--- a/provider_sql.go
+++ b/provider_sql.go
@@ -226,11 +226,11 @@ func (p *SQLProvider) UpdateBookmarks(ctx context.Context, user string, token *o
var curSha sql.NullString
err = tx.QueryRowContext(ctx, "SELECT sha FROM branches WHERE user=? AND name=?", user, branch).Scan(&curSha)
if err != nil && err != sql.ErrNoRows {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
if expectSHA != "" && curSha.Valid && curSha.String != expectSHA {
- tx.Rollback()
+ _ = tx.Rollback()
return errors.New("sha mismatch")
}
@@ -241,14 +241,14 @@ func (p *SQLProvider) UpdateBookmarks(ctx context.Context, user string, token *o
"INSERT INTO history(user, sha, message, text, date) VALUES(?,?,?,?,?)",
user, newSha, "update", text, time.Now(),
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
if _, err := tx.ExecContext(ctx,
"UPDATE bookmarks SET list=? WHERE user=?", text, user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
@@ -260,7 +260,7 @@ func (p *SQLProvider) UpdateBookmarks(ctx context.Context, user string, token *o
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE sha = VALUES(sha)
`, user, branch, newSha); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
case "sqlite3":
@@ -269,11 +269,11 @@ func (p *SQLProvider) UpdateBookmarks(ctx context.Context, user string, token *o
VALUES (?, ?, ?)
ON CONFLICT(user, name) DO UPDATE SET sha = excluded.sha
`, user, branch, newSha); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
default:
- tx.Rollback()
+ _ = tx.Rollback()
return errors.New("unsupported connection provider")
}
@@ -301,7 +301,7 @@ func (p *SQLProvider) CreateBookmarks(ctx context.Context, user string, token *o
"INSERT INTO bookmarks(user, list) VALUES(?, '') ON DUPLICATE KEY UPDATE list=list",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
case "sqlite3":
@@ -309,11 +309,11 @@ func (p *SQLProvider) CreateBookmarks(ctx context.Context, user string, token *o
"INSERT OR IGNORE INTO bookmarks(user, list) VALUES(?, '')",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
default:
- tx.Rollback()
+ _ = tx.Rollback()
return errors.New("unsupported connection provider")
}
@@ -324,14 +324,14 @@ func (p *SQLProvider) CreateBookmarks(ctx context.Context, user string, token *o
"INSERT INTO history(user, sha, message, text, date) VALUES(?,?,?,?,?)",
user, newSha, "create", text, time.Now(),
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
if _, err := tx.ExecContext(ctx,
"UPDATE bookmarks SET list=? WHERE user=?", text, user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
@@ -343,7 +343,7 @@ func (p *SQLProvider) CreateBookmarks(ctx context.Context, user string, token *o
VALUES (?, ?, ?)
ON DUPLICATE KEY UPDATE sha=VALUES(sha)
`, user, branch, newSha); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
case "sqlite3":
@@ -352,11 +352,11 @@ func (p *SQLProvider) CreateBookmarks(ctx context.Context, user string, token *o
VALUES (?, ?, ?)
ON CONFLICT(user, name) DO UPDATE SET sha = excluded.sha
`, user, branch, newSha); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
default:
- tx.Rollback()
+ _ = tx.Rollback()
return errors.New("unsupported connection provider")
}
@@ -381,7 +381,7 @@ func (p *SQLProvider) CreateRepo(ctx context.Context, user string, token *oauth2
"INSERT INTO bookmarks(user, list) VALUES(?, '') ON DUPLICATE KEY UPDATE list=list",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
// default branch
@@ -389,7 +389,7 @@ func (p *SQLProvider) CreateRepo(ctx context.Context, user string, token *oauth2
"INSERT INTO branches(user, name, sha) VALUES(?, 'main', '') ON DUPLICATE KEY UPDATE sha=sha",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
case "sqlite3":
@@ -397,18 +397,18 @@ func (p *SQLProvider) CreateRepo(ctx context.Context, user string, token *oauth2
"INSERT OR IGNORE INTO bookmarks(user, list) VALUES(?, '')",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
if _, err := tx.ExecContext(ctx,
"INSERT OR IGNORE INTO branches(user, name, sha) VALUES(?, 'main', '')",
user,
); err != nil {
- tx.Rollback()
+ _ = tx.Rollback()
return err
}
default:
- tx.Rollback()
+ _ = tx.Rollback()
return errors.New("unsupported connection provider")
}