diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..73457d5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,83 @@ +name: CI + +on: + push: + branches: + - main + - development + pull_request: + branches: + - main + - development + workflow_dispatch: + +jobs: + build: + name: 'Build & Test' + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + check-latest: true + cache: true + + - name: Install dependencies + run: go mod download + + - name: Install tools + run: | + go install mvdan.cc/gofumpt@latest + go install github.com/daixiang0/gci@latest + go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest + + - name: Check formatting (gofumpt) + run: | + if [ -n "$(gofumpt -d .)" ]; then + echo "❌ Code is not formatted. Run: gofumpt -w ." + gofumpt -d . + exit 1 + fi + echo "✅ Code formatting is clean" + + - name: Check imports (gci) + run: | + if [ -n "$(gci diff -s standard -s default -s 'prefix(github.com/mindfiredigital/DeepScanBot)' .)" ]; then + echo "❌ Imports are not sorted. Run: gci write -s standard -s default -s 'prefix(github.com/mindfiredigital/DeepScanBot)' ." + gci diff -s standard -s default -s 'prefix(github.com/mindfiredigital/DeepScanBot)' . + exit 1 + fi + echo "✅ Imports are clean" + + - name: Run golangci-lint + run: | + golangci-lint run --timeout 5m ./... + echo "✅ Lint passed" + + - name: Build + run: | + go build -o deepscanbot ./apps/cli + echo "✅ Build successful" + + - name: Run tests + run: | + go test -v -count=1 ./... + echo "✅ All tests passed" + + - name: Verify tidy + run: | + go mod tidy + if [ -n "$(git diff --name-only go.mod go.sum)" ]; then + echo "❌ go.mod/go.sum are not tidy. Run: go mod tidy" + git diff go.mod go.sum + exit 1 + fi + echo "✅ go.mod is tidy" \ No newline at end of file diff --git a/.github/workflows/release-docs.yml b/.github/workflows/release-docs.yml new file mode 100644 index 0000000..6804869 --- /dev/null +++ b/.github/workflows/release-docs.yml @@ -0,0 +1,70 @@ +name: Release docs Workflow + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + name: 'Build Docusaurus Documentation' + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 24 + + - name: Install dependencies + run: | + cd apps/docs + npm ci + + - name: Build Docusaurus + run: | + cd apps/docs + npm run build + + - name: Set Git user + run: | + git config --local user.email "github-actions@github.com" + git config --local user.name "GitHub Actions" + + - name: Deploy to gh-pages branch + run: | + # Store the build artifacts + mkdir -p /tmp/docusaurus-build + cp -r apps/docs/build/* /tmp/docusaurus-build/ + + # Stash any changes to prevent checkout conflicts + git stash push --include-untracked || true + + # Switch to gh-pages branch, creating it if it doesn't exist + git checkout gh-pages || git checkout -b gh-pages + + # Remove all existing files + rm -rf * + + # Copy the build artifacts + cp -r /tmp/docusaurus-build/* . + + # Check if there are any changes before committing + if [[ -n "$(git status --porcelain)" ]]; then + git add . -f + git commit -m "chore(docs): update documentation build" --no-verify + git push origin gh-pages --force + echo "✅ Documentation deployed to gh-pages" + else + echo "No changes to commit" + fi + + # Switch back to original branch + git checkout - \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..0b6a980 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,68 @@ +name: Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + packages: write + +jobs: + goreleaser: + name: Release Binaries + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Go + uses: actions/setup-go@v5 + with: + go-version: '1.22' + check-latest: true + cache: true + + - name: Install dependencies + run: go mod download + + - name: Run tests + run: go test -v -count=1 ./... + + - name: Build binaries + run: | + mkdir -p dist + GOOS=linux GOARCH=amd64 go build -o dist/deepscanbot-linux-amd64 ./apps/cli + GOOS=linux GOARCH=arm64 go build -o dist/deepscanbot-linux-arm64 ./apps/cli + GOOS=darwin GOARCH=amd64 go build -o dist/deepscanbot-darwin-amd64 ./apps/cli + GOOS=darwin GOARCH=arm64 go build -o dist/deepscanbot-darwin-arm64 ./apps/cli + GOOS=windows GOARCH=amd64 go build -o dist/deepscanbot-windows-amd64.exe ./apps/cli + echo "✅ Binaries built" + + - name: Generate checksums + run: | + cd dist + sha256sum * > checksums.txt + echo "✅ Checksums generated" + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + name: Release ${{ github.ref_name }} + tag_name: ${{ github.ref_name }} + body_path: CHANGELOG.md + draft: false + prerelease: false + files: | + dist/deepscanbot-linux-amd64 + dist/deepscanbot-linux-arm64 + dist/deepscanbot-darwin-amd64 + dist/deepscanbot-darwin-arm64 + dist/deepscanbot-windows-amd64.exe + dist/checksums.txt + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index eb1e8f1..b96b2de 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -193,6 +193,7 @@ Use conventional commits format: ### Commit Messages Follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. Use `cocogitto` or a custom commit hook to validate commit messages: + ```bash # Using cocogitto cog commit diff --git a/README.md b/README.md index 33ce156..351c873 100644 --- a/README.md +++ b/README.md @@ -250,10 +250,10 @@ The JSON report contains a detailed summary and two URL lists: Text output shows one URL per line with optional metadata in brackets: ``` -[https://example.com [status=200] [result=passed] -[https://example.com/about [status=200] [result=passed] -[https://example.com/not-found [status=404] [result=failed] [error=bad status code: 404] -[https://external.com/page [result=skipped] [skipped=outside domain scope] +[https://example.com] [status=200] [result=passed] +[https://example.com/about] [status=200] [result=passed] +[https://example.com/not-found] [status=404] [result=failed] [error=bad status code: 404] +[https://external.com/page] [result=skipped] [skipped=outside domain scope] // With -s flag: [href] https://example.com diff --git a/apps/cli/tests/crawler_test.go b/apps/cli/tests/crawler_test.go index 8a1c5aa..1c5cda3 100644 --- a/apps/cli/tests/crawler_test.go +++ b/apps/cli/tests/crawler_test.go @@ -15,8 +15,15 @@ import ( ) func TestCrawlerStartReturnsResultsWithoutWritingFiles(t *testing.T) { - //nolint:govet // testing.Chdir requires Go 1.24+, using available version - t.Chdir(t.TempDir()) + origDir, _ := os.Getwd() + + tmpDir := t.TempDir() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir to temp dir: %v", err) + } + + t.Cleanup(func() { _ = os.Chdir(origDir) }) server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html") diff --git a/apps/cli/tests/storage_test.go b/apps/cli/tests/storage_test.go index d61e4b1..780d461 100644 --- a/apps/cli/tests/storage_test.go +++ b/apps/cli/tests/storage_test.go @@ -14,8 +14,15 @@ import ( func TestTextOutputIsTruncatedForEachStorageInstance(t *testing.T) { const filename = "crawler_results.txt" - //nolint:govet // testing.Chdir requires Go 1.24+, using available version - t.Chdir(t.TempDir()) + origDir, _ := os.Getwd() + + tmpDir := t.TempDir() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir to temp dir: %v", err) + } + + t.Cleanup(func() { _ = os.Chdir(origDir) }) if err := os.WriteFile(filename, []byte("result from a previous crawl\n"), 0o644); err != nil { t.Fatalf("seed previous output: %v", err) @@ -41,8 +48,15 @@ func TestTextOutputIsTruncatedForEachStorageInstance(t *testing.T) { func TestTextOutputIsFlushedOnClose(t *testing.T) { const filename = "crawler_results.txt" - //nolint:govet // testing.Chdir requires Go 1.24+, using available version - t.Chdir(t.TempDir()) + origDir, _ := os.Getwd() + + tmpDir := t.TempDir() + + if err := os.Chdir(tmpDir); err != nil { + t.Fatalf("chdir to temp dir: %v", err) + } + + t.Cleanup(func() { _ = os.Chdir(origDir) }) pageStorage := storage.NewPageStorage() pageStorage.StoreContent("https://example.com/one")