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") }