├── .codecov.yml ├── .github └── workflows │ ├── go.yml │ └── release.yml ├── .gitignore ├── .goreleaser.yaml ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── README_zh.md ├── clone.go ├── config ├── .image │ ├── go1.20.14.gocilint.1.61.0.Dockerfile │ ├── go1.22.3.gocilint.1.59.1.Dockerfile │ ├── go1.22.3.pylint.Dockerfile │ ├── go1.23.2.gocilint.1.61.0.Dockerfile │ └── p3c-pmd-2.1.1.Dockerfile ├── config.example.yaml ├── config.go ├── config_test.go └── linters-config │ ├── .golangci.goplus.yml │ └── .golangci.yml ├── deploy ├── config ├── github-known-hosts └── reviewbot.yaml ├── docs ├── codereview-activity │ ├── A.2024-02-21.md │ ├── A.2024-03-08.md │ ├── B.2024-02-23.md │ ├── B.2024-03-01.md │ ├── B.2024-03.08.md │ └── README.md ├── engineering-practice │ ├── code-review.md │ └── git-flow-instructions_zh.md ├── proposal │ ├── custom-runner-v2.md │ └── custom-runner.md ├── static │ ├── ai-details.png │ ├── ci-status.png │ ├── found-unexpected-issue.png │ ├── found-valid-issue.png │ ├── github-check-run-annotations.png │ ├── github-check-run.png │ ├── github-pr-review-comments.png │ ├── issue-comment.png │ └── rebase-suggestion.jpg └── website │ ├── .gitignore │ ├── README.md │ ├── babel.config.js │ ├── docs │ ├── architecture.md │ ├── components │ │ ├── _category_.json │ │ ├── c │ │ │ ├── _category_.json │ │ │ └── cppcheck.md │ │ ├── doc │ │ │ ├── _category_.json │ │ │ └── note-check.md │ │ ├── git │ │ │ ├── _category_.json │ │ │ └── commit-check.md │ │ ├── go │ │ │ ├── _category_.json │ │ │ ├── gofmt.md │ │ │ ├── golangci_lint.md │ │ │ └── gomodcheck.md │ │ ├── img │ │ │ ├── docsVersionDropdown.png │ │ │ └── localeDropdown.png │ │ ├── java │ │ │ ├── _category_.json │ │ │ ├── img │ │ │ │ ├── commentsrule.png │ │ │ │ ├── pmdrulefileset.png │ │ │ │ ├── pmdruleurlset.png │ │ │ │ ├── stylecheckruleconfig.png │ │ │ │ ├── stylecheckrulefileset.png │ │ │ │ └── stylecheckurlset.png │ │ │ ├── pmdcheck.md │ │ │ └── stylecheck.md │ │ ├── lua │ │ │ ├── _category_.json │ │ │ └── luacheck.md │ │ └── shell │ │ │ ├── _category_.json │ │ │ └── shellcheck.md │ ├── configuration.md │ ├── getting-started │ │ ├── _category_.json │ │ ├── development.md │ │ ├── images │ │ │ ├── comments.png │ │ │ └── detail.png │ │ ├── installation.md │ │ └── quickinstall.md │ ├── img │ │ └── arch.png │ └── intro.md │ ├── docusaurus.config.ts │ ├── package-lock.json │ ├── package.json │ ├── sidebars.ts │ ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ └── markdown-page.md │ ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.ico │ │ ├── q.png │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg │ └── tsconfig.json ├── go.mod ├── go.sum ├── internal ├── cache │ ├── issuereference.go │ └── token.go ├── lint │ ├── agent.go │ ├── agent_test.go │ ├── filters.go │ ├── filters_test.go │ ├── hunk.go │ ├── hunk_test.go │ ├── lint.go │ ├── lint_test.go │ ├── provider.go │ ├── providergithub.go │ └── providergitlab.go ├── linters │ ├── .testdata │ │ ├── autogen.go │ │ └── xxx_1.go │ ├── c │ │ └── cppcheck │ │ │ ├── README.md │ │ │ ├── cppcheck.go │ │ │ └── cppcheck_test.go │ ├── doc │ │ └── note-check │ │ │ ├── README.md │ │ │ ├── doc.go │ │ │ ├── note.go │ │ │ ├── note_test.go │ │ │ └── testdata │ │ │ └── note.go │ ├── git-flow │ │ └── commit │ │ │ ├── README.md │ │ │ ├── commit.go │ │ │ └── commit_test.go │ ├── go │ │ ├── gofmt │ │ │ ├── README.md │ │ │ ├── gofmt.go │ │ │ ├── gofmt_test.go │ │ │ └── testdata │ │ │ │ └── gofmt_test.txt │ │ ├── golangci_lint │ │ │ ├── README.md │ │ │ ├── golangci_lint.go │ │ │ ├── golangci_lint_test.go │ │ │ └── testdata │ │ │ │ └── .golangci.yml │ │ ├── gomodcheck │ │ │ ├── README.md │ │ │ ├── gomodcheck.go │ │ │ └── gomodcheck_test.go │ │ └── staticcheck │ │ │ ├── README.md │ │ │ └── staticcheck.go │ ├── java │ │ ├── pmdcheck │ │ │ ├── README.md │ │ │ ├── pmdcheck.go │ │ │ └── pmdcheck_test.go │ │ └── stylecheck │ │ │ ├── README.md │ │ │ ├── stylecheck.go │ │ │ └── stylecheck_test.go │ ├── lua │ │ └── luacheck │ │ │ ├── README.md │ │ │ ├── luacheck.go │ │ │ └── luacheck_test.go │ └── shell │ │ └── shellcheck │ │ ├── README.md │ │ └── shellcheck.go ├── llm │ ├── llm.go │ ├── ollama.go │ └── openapi.go ├── metric │ └── metrics.go ├── runner │ ├── docker.go │ ├── kubernertes.go │ ├── runner.go │ └── runner_test.go ├── storage │ ├── file.go │ ├── git.go │ ├── s3.go │ ├── storage.go │ └── storage_test.go ├── util │ └── util.go └── version │ └── version.go ├── kustomization.yml ├── main.go ├── server.go └── tools ├── linterstars ├── 100stars.txt ├── go.mod ├── go.sum ├── main.go └── stars.txt └── phony ├── README.md ├── main.go └── pr-open.json /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | status: 3 | project: off 4 | patch: off 5 | 6 | github_checks: 7 | annotations: false 8 | -------------------------------------------------------------------------------- /.github/workflows/go.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: Go 5 | 6 | on: 7 | push: 8 | branches: ["master"] 9 | pull_request: 10 | branches: ["master"] 11 | 12 | jobs: 13 | build: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Set up Go 19 | uses: actions/setup-go@v4 20 | with: 21 | go-version: "1.21.5" 22 | - name: Set up java 23 | uses: actions/setup-java@v1 24 | with: 25 | java-version: 1.8 26 | - name: Run go vet 27 | run: go vet ./... 28 | - name: Run go fmt 29 | run: go fmt ./... 30 | - name: Run go build 31 | run: go build ./... 32 | - name: Run go test 33 | run: go test -v -coverprofile="coverage.txt" ./... 34 | - name: Upload coverage to Codecov 35 | uses: codecov/codecov-action@v4 36 | with: 37 | token: ${{ secrets.CODECOV_TOKEN }} 38 | slug: ${{ github.repository }} 39 | codecov_yml_path: .codecov.yml 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: goreleaser 2 | on: 3 | # when you push a tag that matches v*, build binaries 4 | push: 5 | tags: 6 | - v* 7 | 8 | permissions: 9 | contents: write 10 | packages: write 11 | attestations: write 12 | id-token: write 13 | 14 | jobs: 15 | goreleaser: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | - name: Log in to the Container registry 25 | uses: docker/login-action@v3 26 | with: 27 | registry: ghcr.io 28 | username: ${{ github.actor }} 29 | password: ${{ secrets.GITHUB_TOKEN }} 30 | - name: Run GoReleaser 31 | uses: goreleaser/goreleaser-action@v6 32 | with: 33 | distribution: goreleaser 34 | version: 'latest' 35 | args: release --clean 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | reviewbot 2 | .vscode 3 | .idea 4 | .DS_Store 5 | *.jar 6 | 7 | # Local Netlify folder 8 | .netlify 9 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | env: 2 | - GO111MODULE=on 3 | - CGO_ENABLED=0 4 | 5 | metadata: 6 | mod_timestamp: "{{ .CommitTimestamp }}" 7 | 8 | builds: 9 | - env: 10 | - CGO_ENABLED=0 11 | goos: 12 | - linux 13 | - darwin 14 | - windows 15 | goarch: 16 | - amd64 17 | - arm64 18 | mod_timestamp: "{{ .CommitTimestamp }}" 19 | flags: 20 | - -trimpath 21 | ldflags: 22 | - -s -w -X github.com/qiniu/reviewbot/internal/version.version={{.Version}} 23 | dockers: 24 | - image_templates: 25 | [ 26 | "ghcr.io/qiniu/reviewbot:{{ .Version }}", 27 | "ghcr.io/qiniu/reviewbot:latest", 28 | ] 29 | dockerfile: Dockerfile 30 | use: buildx 31 | build_flag_templates: 32 | - "--pull" 33 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/qiniu/reviewbot/master/README.md" 34 | - "--label=org.opencontainers.image.description=Comprehensive linters runner for code review" 35 | - "--label=org.opencontainers.image.created={{.Date}}" 36 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 37 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 38 | - "--label=org.opencontainers.image.version={{.Version}}" 39 | - "--label=org.opencontainers.image.source={{.GitURL}}" 40 | - "--platform=linux/amd64" 41 | - image_templates: 42 | [ 43 | "ghcr.io/qiniu/reviewbot:{{ .Version }}", 44 | "ghcr.io/qiniu/reviewbot:latest", 45 | ] 46 | dockerfile: Dockerfile 47 | use: buildx 48 | build_flag_templates: 49 | - "--pull" 50 | - "--label=io.artifacthub.package.readme-url=https://raw.githubusercontent.com/qiniu/reviewbot/master/README.md" 51 | - "--label=org.opencontainers.image.description=Comprehensive linters runner for code review" 52 | - "--label=org.opencontainers.image.created={{.Date}}" 53 | - "--label=org.opencontainers.image.name={{.ProjectName}}" 54 | - "--label=org.opencontainers.image.revision={{.FullCommit}}" 55 | - "--label=org.opencontainers.image.version={{.Version}}" 56 | - "--label=org.opencontainers.image.source={{.GitURL}}" 57 | - "--platform=linux/arm64" 58 | goarch: arm64 59 | archives: 60 | - format: binary 61 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | # image from qiniu registry, origin: https://hub.docker.com/_/golang 3 | FROM aslan-spock-register.qiniu.io/qa/golang:1.24.1-alpine3.21 4 | 5 | # if you want to install other tools, please add them here. 6 | # Do not install unnecessary tools to reduce image size. 7 | RUN set -eux \ 8 | apk update && \ 9 | apk --no-cache add ca-certificates luacheck cppcheck shellcheck git openssh yarn libpcap-dev curl openjdk11 bash build-base && \ 10 | curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.64.6 11 | 12 | WORKDIR / 13 | # check binary 14 | RUN cppcheck --version \ 15 | && shellcheck --version \ 16 | && luacheck --version \ 17 | && git --version \ 18 | && ssh -V \ 19 | && yarn --version \ 20 | && curl --version \ 21 | && gcc --version \ 22 | && golangci-lint --version \ 23 | && go version 24 | 25 | # install docker 26 | RUN apk add --no-cache docker docker-cli 27 | 28 | RUN java -version 29 | #install pmd 30 | ENV PMD_DOWNLOAD_URL https://github.com/pmd/pmd/releases/download/pmd_releases%2F7.4.0/pmd-dist-7.4.0-bin.zip 31 | ENV PMD_DOWNLOAD_SHA256 1dcbb7784a7fba1fd3c6efbaf13dcb63f05fe069fcf026ad5e2933711ddf5026 32 | RUN curl -fsSL "$PMD_DOWNLOAD_URL" -o pmd.zip \ 33 | && echo "$PMD_DOWNLOAD_SHA256 pmd.zip" | sha256sum -c - \ 34 | && unzip pmd.zip -d /usr/local\ 35 | && rm pmd.zip 36 | 37 | ENV PATH /usr/local/pmd-bin-7.4.0/bin:$PATH 38 | RUN pmd --version 39 | 40 | #install stylecheck 41 | ENV StyleCheck_DOWNLOAD_URL https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.17.0/checkstyle-10.17.0-all.jar 42 | ENV StyleCheck_DOWNLOAD_SHA256 51c34d738520c1389d71998a9ab0e6dabe0d7cf262149f3e01a7294496062e42 43 | RUN curl -fsSL "$StyleCheck_DOWNLOAD_URL" -o checkstyle.jar \ 44 | && echo "$StyleCheck_DOWNLOAD_SHA256 checkstyle.jar" | sha256sum -c - 45 | RUN java -jar checkstyle.jar --version 46 | 47 | # install kubectl 48 | ARG KUBECTL_VERSION=v1.28.3 49 | RUN curl -fsSL -o kubectl https://dl.k8s.io/release/v1.28.3/bin/linux/amd64/kubectl \ 50 | && chmod +x kubectl \ 51 | && mv kubectl /usr/local/bin/ 52 | 53 | COPY reviewbot /reviewbot 54 | 55 | # SSH config 56 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ 57 | COPY deploy/config /root/.ssh/config 58 | COPY deploy/github-known-hosts /github_known_hosts 59 | 60 | EXPOSE 8888 61 | 62 | ENTRYPOINT [ "/reviewbot" ] -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DOCKER_IMAGE ?= aslan-spock-register.qiniu.io/qa/reviewbot 2 | TAG?=$(shell git describe --tag --always) 3 | LDFLAGS=-X 'github.com/qiniu/reviewbot/internal/version.version=$(TAG)' 4 | define check_command 5 | @if [ -z "$$(which $(1))" ]; then \ 6 | echo "No $(1) in $(PATH), consider installing it."; \ 7 | exit 1; \ 8 | fi 9 | endef 10 | 11 | all: fmt vet staticcheck build test 12 | 13 | check-go: 14 | $(call check_command,go) 15 | 16 | check-docker: 17 | $(call check_command,docker) 18 | 19 | check-kubectl: 20 | $(call check_command,kubectl) 21 | 22 | check-staticcheck: 23 | $(call check_command,staticcheck) 24 | 25 | test: check-go 26 | go test -v ./... 27 | fmt: check-go 28 | go fmt ./... 29 | vet: check-go 30 | go vet ./... 31 | 32 | staticcheck: check-staticcheck 33 | staticcheck ./... 34 | 35 | build: check-go 36 | CGO_ENABLED=0 go build -v -ldflags "$(LDFLAGS)" -o ./reviewbot . 37 | 38 | linux-build: check-go 39 | GOOS=linux CGO_ENABLED=0 go build -v -ldflags "$(LDFLAGS)" -o ./reviewbot . 40 | 41 | docker-build-latest: check-docker linux-build 42 | docker builder build --push -t $(DOCKER_IMAGE):$(TAG) -t $(DOCKER_IMAGE):latest . 43 | 44 | docker-dev: check-docker linux-build 45 | docker builder build -t $(DOCKER_IMAGE):$(TAG) . 46 | 47 | kubernetes-deploy: check-kubectl 48 | kubectl apply -k . 49 | -------------------------------------------------------------------------------- /config/.image/go1.20.14.gocilint.1.61.0.Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | FROM golang:1.20.14-alpine3.19 3 | 4 | # if you want to install other tools, please add them here. 5 | # Do not install unnecessary tools to reduce image size. 6 | RUN set -eux \ 7 | apk update && \ 8 | apk --no-cache add ca-certificates git openssh yarn libpcap-dev curl bash build-base 9 | 10 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.61.0 11 | 12 | WORKDIR / 13 | 14 | # SSH config 15 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ \ 16 | && git config --global url."git@github.com:".insteadOf https://github.com/ \ 17 | && git config --global url."git://".insteadOf https:// 18 | COPY deploy/config /root/.ssh/config 19 | COPY deploy/github-known-hosts /github_known_hosts 20 | 21 | # set go proxy and private repo 22 | RUN go env -w GOPROXY=https://goproxy.cn,direct \ 23 | && go env -w GOPRIVATE=github.com/qbox,qiniu.com 24 | 25 | EXPOSE 8888 26 | -------------------------------------------------------------------------------- /config/.image/go1.22.3.gocilint.1.59.1.Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | FROM golang:1.22.3-alpine3.20 3 | 4 | # if you want to install other tools, please add them here. 5 | # Do not install unnecessary tools to reduce image size. 6 | RUN set -eux \ 7 | apk update && \ 8 | apk --no-cache add ca-certificates git openssh yarn libpcap-dev curl openjdk11 bash build-base 9 | 10 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.59.1 11 | 12 | WORKDIR / 13 | 14 | # SSH config 15 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ \ 16 | && git config --global url."git@github.com:".insteadOf https://github.com/ \ 17 | && git config --global url."git://".insteadOf https:// 18 | COPY deploy/config /root/.ssh/config 19 | COPY deploy/github-known-hosts /github_known_hosts 20 | 21 | # set go proxy and private repo 22 | RUN go env -w GOPROXY=https://goproxy.cn,direct \ 23 | && go env -w GOPRIVATE=github.com/qbox,qiniu.com 24 | 25 | EXPOSE 8888 26 | -------------------------------------------------------------------------------- /config/.image/go1.22.3.pylint.Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | FROM golang:1.22.3-alpine3.20 3 | 4 | # if you want to install other tools, please add them here. 5 | # Do not install unnecessary tools to reduce image size. 6 | RUN set -eux \ 7 | apk update && \ 8 | apk --no-cache add ca-certificates git openssh yarn libpcap-dev curl openjdk11 bash build-base 9 | 10 | RUN apk --no-cache add python3 py3-pip 11 | 12 | RUN apk --no-cache add py3-pylint 13 | 14 | WORKDIR / 15 | 16 | # SSH config 17 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ \ 18 | && git config --global url."git@github.com:".insteadOf https://github.com/ \ 19 | && git config --global url."git://".insteadOf https:// 20 | COPY deploy/config /root/.ssh/config 21 | COPY deploy/github-known-hosts /github_known_hosts 22 | 23 | # set go proxy and private repo 24 | RUN go env -w GOPROXY=https://goproxy.cn,direct \ 25 | && go env -w GOPRIVATE=github.com/qbox,qiniu.com 26 | 27 | EXPOSE 8888 28 | -------------------------------------------------------------------------------- /config/.image/go1.23.2.gocilint.1.61.0.Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | FROM golang:1.23.2-alpine3.20 3 | 4 | # if you want to install other tools, please add them here. 5 | # Do not install unnecessary tools to reduce image size. 6 | RUN set -eux \ 7 | apk update && \ 8 | apk --no-cache add ca-certificates git openssh yarn libpcap-dev curl openjdk11 bash build-base 9 | 10 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.61.0 11 | 12 | WORKDIR / 13 | 14 | # SSH config 15 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ \ 16 | && git config --global url."git@github.com:".insteadOf https://github.com/ \ 17 | && git config --global url."git://".insteadOf https:// 18 | COPY deploy/config /root/.ssh/config 19 | COPY deploy/github-known-hosts /github_known_hosts 20 | 21 | # set go proxy and private repo 22 | RUN go env -w GOPROXY=https://goproxy.cn,direct \ 23 | && go env -w GOPRIVATE=github.com/qbox,qiniu.com 24 | 25 | EXPOSE 8888 26 | -------------------------------------------------------------------------------- /config/.image/p3c-pmd-2.1.1.Dockerfile: -------------------------------------------------------------------------------- 1 | # go lint tool dependencies `go list` `gofmt` 2 | FROM golang:1.23.2-alpine3.20 3 | #FROM aslan-spock-register.qiniu.io/golang:1.23.2-alpine3.20 4 | ENV GOPROXY https://goproxy.cn,direct 5 | ENV TimeZone=Asia/Shanghai 6 | # if you want to install other tools, please add them here. 7 | # Do not install unnecessary tools to reduce image size. 8 | RUN set -eux \ 9 | apk update && \ 10 | apk --no-cache add ca-certificates git openssh yarn libpcap-dev curl openjdk11 bash build-base maven 11 | ENV JAVA_HOME=/usr/lib/jvm/java-11-openjdk 12 | ENV PATH=$PATH:$JAVA_HOME/bin 13 | 14 | #RUN update-alternatives --list java 15 | 16 | RUN curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b /usr/local/bin v1.61.0 17 | 18 | RUN mkdir source 19 | 20 | WORKDIR /source 21 | 22 | RUN git clone https://github.com/alibaba/p3c.git 23 | 24 | WORKDIR /source/p3c/p3c-pmd 25 | 26 | RUN mvn clean kotlin:compile package 27 | 28 | 29 | WORKDIR / 30 | 31 | 32 | 33 | # SSH config 34 | RUN mkdir -p /root/.ssh && chown -R root /root/.ssh/ && chgrp -R root /root/.ssh/ \ 35 | && git config --global url."git@github.com:".insteadOf https://github.com/ \ 36 | && git config --global url."git://".insteadOf https:// 37 | COPY deploy/config /root/.ssh/config 38 | COPY deploy/github-known-hosts /github_known_hosts 39 | 40 | # set go proxy and private repo 41 | RUN go env -w GOPROXY=https://goproxy.cn,direct \ 42 | && go env -w GOPRIVATE=github.com/qbox,qiniu.com 43 | 44 | EXPOSE 8888 45 | -------------------------------------------------------------------------------- /config/linters-config/.golangci.goplus.yml: -------------------------------------------------------------------------------- 1 | # This is the recommended config for golangci-lint based on our experience and opinion. 2 | # And it will be continuously updated. 3 | # 4 | # Philosophy: 5 | # 1. Strict but practical: We aim to detect real issues that require fixing 6 | # 2. High quality: We enable carefully selected, industry-proven linters(linters with 100+ stars) 7 | # 3. Best practices: Leverage community-accepted Go best practices 8 | # 4. False positive minimization: Configured to reduce noise while maintaining effectiveness 9 | # 10 | # Feel free to customize the config to your own project. 11 | 12 | run: 13 | # control the resource usage of golangci-lint to avoid OOM 14 | concurrency: 4 15 | # Default: 1m 16 | timeout: 3m 17 | 18 | linters: 19 | disable-all: true 20 | enable: 21 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 22 | - gosimple # specializes in simplifying a code 23 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 24 | - ineffassign # detects when assignments to existing variables are not used 25 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 26 | - unused # checks for unused constants, variables, functions and types 27 | - bidichk # security checks for dangerous unicode character sequences 28 | - bodyclose # checks whether HTTP response body is closed successfully 29 | - copyloopvar # detects places where loop variables are copied (Go 1.22+) 30 | - dupl # tool for code clone detection 31 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 32 | - exhaustive # checks exhaustiveness of enum switch statements 33 | - gocognit # computes and checks the cognitive complexity of functions 34 | - goconst # finds repeated strings that could be replaced by a constant 35 | - gocritic # provides diagnostics that check for bugs, performance and style issues 36 | - gocyclo # computes and checks the cyclomatic complexity of functions 37 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 38 | - gosec # inspects source code for security problems 39 | - nakedret # finds naked returns in functions greater than a specified function length 40 | - noctx # finds sending http request without context.Context 41 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 42 | - sloglint # ensure consistent code style when using log/slog 43 | - stylecheck # is a replacement for golint 44 | - testifylint # checks usage of github.com/stretchr/testify 45 | - unconvert # removes unnecessary type conversions 46 | - unparam # reports unused function parameters 47 | - gci # enforce consistent imports 48 | - misspell # check for spelling mistakes 49 | - prealloc # checks for slice pre-allocation 50 | 51 | issues: 52 | exclude-rules: 53 | - source: "(noinspection|TODO)" 54 | linters: [godot] 55 | - source: "//noinspection" 56 | linters: [gocritic] 57 | - path: "_test\\.go" 58 | linters: 59 | - bodyclose 60 | - dupl 61 | - revive # too strict for test scenarios 62 | - gocognit # no need to check on test files 63 | - errcheck 64 | - funlen 65 | - goconst 66 | - gosec 67 | - noctx 68 | - wrapcheck 69 | 70 | linters-settings: 71 | errcheck: 72 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 73 | # Such cases aren't reported by default. 74 | # Default: false 75 | check-type-assertions: true 76 | exclude-functions: 77 | - (net/http.ResponseWriter).Write 78 | - (net/http.ResponseWriter).WriteHeader 79 | - (net/http.ResponseWriter).Header 80 | - (*flag.FlagSet).Parse 81 | 82 | exhaustive: 83 | # Presence of "default" case in switch statements satisfies exhaustiveness, 84 | # even if all enum members are not listed. 85 | # Default: false 86 | default-signifies-exhaustive: true 87 | 88 | gocritic: 89 | # too many false positives 90 | disabled-checks: 91 | - appendAssign 92 | 93 | govet: 94 | # Enable all analyzers. 95 | # Default: false 96 | enable-all: true 97 | # Disable analyzers by name. 98 | # Run `go tool vet help` to see all analyzers. 99 | # Default: [] 100 | disable: 101 | - fieldalignment # too strict 102 | 103 | sloglint: 104 | # Enforce not using global loggers. 105 | # Values: 106 | # - "": disabled 107 | # - "all": report all global loggers 108 | # - "default": report only the default slog logger 109 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global 110 | # Default: "" 111 | no-global: "all" 112 | # Enforce using methods that accept a context. 113 | # Values: 114 | # - "": disabled 115 | # - "all": report all contextless calls 116 | # - "scope": report only if a context exists in the scope of the outermost function 117 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only 118 | # Default: "" 119 | context: "scope" 120 | 121 | revive: 122 | enable-all-rules: true 123 | rules: 124 | - name: unused-parameter 125 | disabled: true 126 | - name: blank-imports 127 | disabled: true 128 | - name: unexported-return 129 | disabled: true 130 | - name: line-length-limit 131 | disabled: true 132 | - name: cognitive-complexity 133 | arguments: [50] 134 | - name: add-constant 135 | disabled: true 136 | - name: max-public-structs 137 | disabled: true 138 | - name: unused-parameter 139 | disabled: true 140 | - name: import-shadowing 141 | disabled: true 142 | - name: unused-receiver 143 | disabled: true 144 | - name: deep-exit 145 | disabled: true 146 | - name: function-length 147 | disabled: true 148 | - name: cyclomatic 149 | arguments: [50] 150 | # too many false positives 151 | - name: unhandled-error 152 | disabled: true 153 | -------------------------------------------------------------------------------- /config/linters-config/.golangci.yml: -------------------------------------------------------------------------------- 1 | # This is the recommended config for golangci-lint based on our experience and opinion. 2 | # And it will be continuously updated. 3 | # 4 | # Philosophy: 5 | # 1. Strict but practical: We aim to detect real issues that require fixing 6 | # 2. High quality: We enable carefully selected, industry-proven linters(linters with 100+ stars) 7 | # 3. Best practices: Leverage community-accepted Go best practices 8 | # 4. False positive minimization: Configured to reduce noise while maintaining effectiveness 9 | # 10 | # Feel free to customize the config to your own project. 11 | 12 | run: 13 | # control the resource usage of golangci-lint to avoid OOM 14 | concurrency: 4 15 | # Default: 1m 16 | timeout: 3m 17 | 18 | linters: 19 | disable-all: true 20 | enable: 21 | - errcheck # checking for unchecked errors, these unchecked errors can be critical bugs in some cases 22 | - gosimple # specializes in simplifying a code 23 | - govet # reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 24 | - ineffassign # detects when assignments to existing variables are not used 25 | - staticcheck # is a go vet on steroids, applying a ton of static analysis checks 26 | - unused # checks for unused constants, variables, functions and types 27 | - bidichk # security checks for dangerous unicode character sequences 28 | - bodyclose # checks whether HTTP response body is closed successfully 29 | - copyloopvar # detects places where loop variables are copied (Go 1.22+) 30 | - dupl # tool for code clone detection 31 | - errorlint # finds code that will cause problems with the error wrapping scheme introduced in Go 1.13 32 | - exhaustive # checks exhaustiveness of enum switch statements 33 | - gocognit # computes and checks the cognitive complexity of functions 34 | - goconst # finds repeated strings that could be replaced by a constant 35 | - gocritic # provides diagnostics that check for bugs, performance and style issues 36 | - gocyclo # computes and checks the cyclomatic complexity of functions 37 | - goimports # in addition to fixing imports, goimports also formats your code in the same style as gofmt 38 | - gosec # inspects source code for security problems 39 | - nakedret # finds naked returns in functions greater than a specified function length 40 | - noctx # finds sending http request without context.Context 41 | - revive # fast, configurable, extensible, flexible, and beautiful linter for Go, drop-in replacement of golint 42 | - sloglint # ensure consistent code style when using log/slog 43 | - stylecheck # is a replacement for golint 44 | - testifylint # checks usage of github.com/stretchr/testify 45 | - unconvert # removes unnecessary type conversions 46 | - unparam # reports unused function parameters 47 | - gci # enforce consistent imports 48 | - misspell # check for spelling mistakes 49 | - prealloc # checks for slice pre-allocation 50 | 51 | issues: 52 | exclude-rules: 53 | - source: "(noinspection|TODO)" 54 | linters: [godot] 55 | - source: "//noinspection" 56 | linters: [gocritic] 57 | - path: "_test\\.go" 58 | linters: 59 | - bodyclose 60 | - dupl 61 | - revive # too strict for test scenarios 62 | - gocognit # no need to check on test files 63 | - errcheck 64 | - funlen 65 | - goconst 66 | - gosec 67 | - noctx 68 | - wrapcheck 69 | 70 | linters-settings: 71 | errcheck: 72 | # Report about not checking of errors in type assertions: `a := b.(MyStruct)`. 73 | # Such cases aren't reported by default. 74 | # Default: false 75 | check-type-assertions: true 76 | exclude-functions: 77 | - (net/http.ResponseWriter).Write 78 | - (net/http.ResponseWriter).WriteHeader 79 | - (net/http.ResponseWriter).Header 80 | - (*flag.FlagSet).Parse 81 | 82 | exhaustive: 83 | # Presence of "default" case in switch statements satisfies exhaustiveness, 84 | # even if all enum members are not listed. 85 | # Default: false 86 | default-signifies-exhaustive: true 87 | 88 | gocritic: 89 | # too many false positives 90 | disabled-checks: 91 | - appendAssign 92 | 93 | govet: 94 | # Enable all analyzers. 95 | # Default: false 96 | enable-all: true 97 | # Disable analyzers by name. 98 | # Run `go tool vet help` to see all analyzers. 99 | # Default: [] 100 | disable: 101 | - fieldalignment # too strict 102 | 103 | sloglint: 104 | # Enforce not using global loggers. 105 | # Values: 106 | # - "": disabled 107 | # - "all": report all global loggers 108 | # - "default": report only the default slog logger 109 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#no-global 110 | # Default: "" 111 | no-global: "all" 112 | # Enforce using methods that accept a context. 113 | # Values: 114 | # - "": disabled 115 | # - "all": report all contextless calls 116 | # - "scope": report only if a context exists in the scope of the outermost function 117 | # https://github.com/go-simpler/sloglint?tab=readme-ov-file#context-only 118 | # Default: "" 119 | context: "scope" 120 | 121 | revive: 122 | enable-all-rules: true 123 | rules: 124 | - name: line-length-limit 125 | disabled: true 126 | - name: cognitive-complexity 127 | arguments: [30] 128 | - name: add-constant 129 | disabled: true 130 | - name: max-public-structs 131 | disabled: true 132 | - name: unused-parameter 133 | disabled: true 134 | - name: import-shadowing 135 | disabled: true 136 | - name: unused-receiver 137 | disabled: true 138 | - name: deep-exit 139 | disabled: true 140 | - name: function-length 141 | disabled: true 142 | - name: cyclomatic 143 | arguments: [30] 144 | # too many false positives 145 | - name: unhandled-error 146 | disabled: true 147 | -------------------------------------------------------------------------------- /deploy/config: -------------------------------------------------------------------------------- 1 | Host github.com 2 | HostName github.com 3 | User git 4 | UserKnownHostsFile /github_known_hosts 5 | CheckHostIP no 6 | StrictHostKeyChecking no 7 | IdentityFile /root/.ssh/id_rsa -------------------------------------------------------------------------------- /deploy/github-known-hosts: -------------------------------------------------------------------------------- 1 | github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk= 2 | -------------------------------------------------------------------------------- /docs/codereview-activity/A.2024-02-21.md: -------------------------------------------------------------------------------- 1 | ## Code Review Comments Learning 2 | 3 | ## Details 4 | 5 | * https://github.com/goplus/community/pull/153#discussion_r1495351302 6 | * URI的设计非常讲究,需考虑最佳实践 7 | * 一些有价值的实践参考 8 | * Github API: https://docs.github.com/en/rest 9 | * Kubernetes API: https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#api-overview 10 | * Aws s3 API: https://docs.aws.amazon.com/AmazonS3/latest/API/API_Operations_Amazon_Simple_Storage_Service.html 11 | * https://github.com/goplus/community/pull/83#discussion_r1481246832 12 | * 锁的使用,要尤其注意粒度,越小越好,职责要单一 13 | * https://github.com/goplus/community/pull/148#discussion_r1494611359 14 | * 推荐使用统一的log库 15 | * 作为一个集体,工程规范应尽可能统一 16 | * https://github.com/qiniu/x 有很多好用的库 17 | 18 | * https://github.com/goplus/community/pull/148#discussion_r1494609866 19 | * 在生产协作中,代码可读性越高,一般越优雅 20 | * https://github.com/goplus/community/pull/148#discussion_r1494612992 21 | * 单元测试,尽量覆盖所有的分支,使用assert抛出问题 22 | * https://github.com/goplus/community/pull/168/files#r1497894587 23 | * 前端部分代码需要保证本地测试通过 24 | * https://github.com/goplus/community/pull/127#issuecomment-1932663028 25 | * Commit记录,尽量保持清晰,不要包含无关的信息,多用rebase少用merge 26 | * https://github.com/goplus/community/pull/108#discussion_r1478493291 27 | * 保证comment的质量,以及合并的代码comment不用中文 28 | * https://github.com/goplus/community/pull/100#discussion_r1479140319 29 | * 避免无意义的提交,暂存的代码不要提交,可以放本地gitignore 30 | * https://github.com/goplus/community/pull/168#discussion_r1498561221 31 | * 看起来是个测试代码,正式提交时需要注释,或者是需要的代码,但是src部分是写死的 32 | * https://github.com/goplus/community/pull/168#discussion_r1498561761 33 | * push之前需同步 34 | * https://github.com/goplus/community/pull/159#discussion_r1498565877 35 | * 减少打包文件的提交,将用到的文件上传到需要的位置 36 | * https://github.com/goplus/community/pull/164 37 | * 过多的魔法值:"auto",抽出当常量使用 38 | 39 | ## Reference 40 | 41 | * [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) 42 | -------------------------------------------------------------------------------- /docs/codereview-activity/A.2024-03-08.md: -------------------------------------------------------------------------------- 1 | ## Code Review Comments Learning 2 | 3 | ## Details 4 | 5 | * https://github.com/goplus/pkgsite/pull/1#discussion_r1512593188 6 | * golangci-lint 的 typecheck 是指编译阶段遇到了错误 7 | * 要保证提交的是完整可运行的代码 8 | * 导入的包,我们一般会要求分组,标准库放最上面 9 | * 参见 https://go.dev/wiki/CodeReviewComments#imports 10 | 11 | * https://github.com/goplus/pkgsite/pull/1#discussion_r1513925551 12 | * 改动原有逻辑要慎重 13 | * 你的PR(代码)就是你的门面,务必多花些心思 14 | 15 | ## Reference 16 | 17 | * [Go Code Review Comments](https://go.dev/wiki/CodeReviewComments) 18 | -------------------------------------------------------------------------------- /docs/codereview-activity/B.2024-02-23.md: -------------------------------------------------------------------------------- 1 | ## Code Review Comments Learning 2 | 3 | ## Details 4 | 5 | * commit 或提 PR 后自己过一遍,排除不预期的变更提交 6 | - https://github.com/goplus/builder/pull/57#discussion_r1477176062 7 | - https://github.com/goplus/builder/pull/73#discussion_r1479335477 8 | - https://github.com/goplus/builder/pull/95#discussion_r1496854240 9 | * 中间状态的或没有用的代码不要提到要合入的 PR 中 10 | - https://github.com/goplus/builder/pull/58#discussion_r1476919423 11 | * 语义上的偏差往往意味着存在问题,并会导致更多的问题 12 | - https://github.com/goplus/builder/pull/67#discussion_r1480887718 13 | - https://github.com/goplus/builder/pull/79#discussion_r1494242860 14 | - https://github.com/goplus/builder/pull/95#discussion_r1496858557 15 | * 给将来的人提供信息 16 | - https://github.com/goplus/builder/pull/79#discussion_r1494210307 17 | - https://github.com/goplus/builder/pull/79#discussion_r1494250274 18 | * 避免不常规的逻辑,如果不可避免,应当添加注释 19 | - https://github.com/goplus/builder/pull/95#discussion_r1496849853 20 | - https://github.com/goplus/builder/pull/99#discussion_r1495175619 21 | * 一个专门用来“重置组件状态”的 prop 往往意味着缺陷 22 | - https://github.com/goplus/builder/pull/67#discussion_r1481073718 23 | - https://github.com/goplus/builder/pull/95#discussion_r1496861448 24 | 25 | ## Reference 26 | 27 | * [Code Review: Developer](https://github.com/qbox/rmb-web/wiki/Code-Review:-Developer) 28 | -------------------------------------------------------------------------------- /docs/codereview-activity/B.2024-03-01.md: -------------------------------------------------------------------------------- 1 | ## Code Review Comments Learning 2 | 3 | ## Details 4 | 5 | * 尽可能借助工具来做 URL 的拼接 6 | - https://github.com/goplus/builder/pull/126#discussion_r1506853371 7 | * Don't repeat yourself 8 | - https://github.com/goplus/builder/pull/126#discussion_r1506907174 9 | - https://github.com/goplus/builder/pull/126#discussion_r1506909206 10 | * 通过抽象逻辑来提高代码的可读性和可测试性 11 | - https://github.com/goplus/builder/pull/123#discussion_r1505460551 12 | 13 | 19 | -------------------------------------------------------------------------------- /docs/codereview-activity/B.2024-03.08.md: -------------------------------------------------------------------------------- 1 | ## Code Review Comments Learning 2 | 3 | ## Details 4 | 5 | * 通过定义常量或 enum 提升代码的可读性 6 | - https://github.com/goplus/builder/pull/131#discussion_r1512527618 7 | - https://github.com/goplus/builder/pull/156#discussion_r1515463961 8 | * 文案与排版的细节 9 | - https://github.com/goplus/builder/pull/131#discussion_r1512514252 10 | * 维护代码中的文档 11 | - https://github.com/goplus/builder/pull/131#discussion_r1513797339 12 | -------------------------------------------------------------------------------- /docs/codereview-activity/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/codereview-activity/README.md -------------------------------------------------------------------------------- /docs/engineering-practice/git-flow-instructions_zh.md: -------------------------------------------------------------------------------- 1 | # 超实用! 从使用视角的Git协作实战,告别死记硬背 2 | 3 | ## 本文档实战Demo, 参考B站视频: https://www.bilibili.com/video/BV1uv421172t/?spm_id_from=333.788&vd_source=622c53b841a1184f63ca7930d5602ef1 4 | 5 | ## 一切从Fork开始 6 | * Git clone => 将远程仓库下载到本地 7 | * Git remote add => 添加远程仓库地址 8 | 9 | ## 开启一个新功能的开发 10 | * **推荐: 每个任务都对应独立的分支开发** 11 | * 从主分支切出你的开发分支 12 | * Git checkout -b => 切出开发分支 13 | * Git fetch => 拉出远程分支 14 | * Git rebase => 跟主分支保持一致 15 | 16 | ## 如何创建【优雅的】commit? 17 | * 四步走 18 | * Git diff => 先浏览一遍改动符不符合你预期 19 | * Git status => 再看改动了哪些文件(这些文件的路径) 20 | * Git add 文件 => 把文件添加到Git的暂存区 21 | * Git commit => 真正开始提交 22 | 23 | * 什么样的commit是优雅的? 24 | * **颗粒度和可读性非常重要** 25 | * 颗粒度不能太大 26 | * 一行文字能说清 27 | * 为更好的利于别人理解,一般格式为 (): 28 | * 常见的type 分类: 29 | * feat: 新功能、新特性 30 | * fix: 修改 bug 31 | * docs: 文档修改 32 | * chore: 其他修改 33 | * ci: 持续集成相关文件修改 34 | * test: 测试用例新增、修改 35 | * 参考阅读: 36 | * [how to write good commit message](https://cbea.ms/git-commit/) 37 | * [约定式提交 - 一种用于给提交信息增加人机可读含义的规范](https://www.conventionalcommits.org/zh-hans/) 38 | 39 | ## 如何创建【规范的】PR? 40 | * 推荐三步走 41 | * Git fetch 42 | * Git rebase 43 | * Git push 44 | * **为什么推荐git fetch 和 git rebase呢?** 45 | * 检查你的改动跟主分支是否冲突 46 | * 让你的改动添加到主分支上,让合并后的主分支时间线更加的干净直观 47 | * 什么样的PR才是规范的? 48 | * CI 检查必须要通过,除非失败是预期的 49 | * PR Title 清晰,可理解 50 | * 善用 PR 的Conversation区域,补充进一步的信息 51 | * 必要时关联相关Issue 52 | * 要遵守什么样的Code Review礼仪? 53 | * 保持心态: **giving and receiving** 54 | * 参考阅读: [Kubernetes code review 规范](https://github.com/kubernetes/community/blob/master/contributors/guide/contributing.md#code-review) 55 | 56 | --- 57 | > PS: 以下详细操作,请看DEMO 58 | --- 59 | 60 | ## 代码冲突了,怎么解决? 61 | * 先解决冲突 62 | * 再git add 63 | 64 | ## 手滑了,产生了垃圾commit记录,怎么办? 65 | * Git rebase -i => 选择要整理的commit记录 66 | * 记住: 已经合并到主分支的commits不要去动 67 | * Git push -f => 如果已经推到远程仓库了,经过rebase整理后的commit,需要使用force push 才能重新推进去 68 | 69 | ## 提交commit之后还想改动,但又不想产生新的commit记录,怎么办? 70 | * Git commit --amend 71 | 72 | ## 能把代码提交到别人的仓库,集成之后再往主仓库提交吗? 73 | * 可以 74 | * git remote add => 将对方的仓库加入Tracking列表 75 | * git fetch => 拉取对方方库到本地 76 | * git rebase => 将你的改动合并到目标分支的最前面 77 | * git push => 提交到你自己的参考 78 | * 针对对方仓库,正常创建PR 79 | 80 | ## 当前功能还没做完,又需要紧急干另一个任务,该怎么办? 81 | * 可以直接正常提交PR,但 PR Tittle 带上[WIP] 标记 82 | * 当然也可以保存下当前的变动,转而切出新分支干另一个任务 83 | * 基本步骤 84 | * git stash 85 | * git checkout -b 86 | * git fetch 87 | * git rebase 88 | * 如何恢复保存的临时变动? 89 | * git stash apply 90 | 91 | ## 要针对线上版本做紧急Hotfix,该怎么办? 92 | * git checkout 93 | * git switch -c 94 | 95 | ## 更多「该怎么办?」 96 | 可以参考 [Git 飞行规则](https://github.com/k88hudson/git-flight-rules/blob/master/README_zh-CN.md) 97 | -------------------------------------------------------------------------------- /docs/proposal/custom-runner-v2.md: -------------------------------------------------------------------------------- 1 | ## Proposal: 自定义执行节点 2 | 3 | ## 背景 4 | 5 | 在 [提案一](custom-runner.md) 中,提出了给节点打标签的方案。但综合评估发现,这个方案实际上对用户的使用要求较高。用户需要理解各节点的定位,以及在相应的 repo/linter 配置上选择适合的 Label。而在打标签和选择标签的实施过程,实际上可能涉及到两个角色的交互,一是 部署 reviewbot 的人,二是维护 repo/linter 配置的人,无疑增加了交互的复杂度。 6 | 7 | 而在技术实现上,也需要引入额外的节点管理服务,以及节点选择算法,复杂度也是有所增加。 8 | 9 | ## 应对 10 | 11 | 在方案一中,我们提到了一个原则: 12 | 13 | > 不假设运行环境是 docker 或者 k8s,节点可以是任意类型,只要能运行 Reviewbot 的二进制文件即可 14 | 15 | 但考虑到自定义节点应该是少数情况,且自定义节点通常需要自定义运行环境,那把这种自定义环境做成 docker image,然后通过 docker run 来运行,应该是一个比较常见的做法。而这种方式也能满足我们的原始需求,且在技术实现上,也能简化不少。 16 | 17 | ## 具体设计 18 | 19 | 在 linter 结构体中,增加一个字段,用于指定运行时的 docker image。 20 | 21 | ```go 22 | type Linter struct { 23 | // ... 24 | // DockerAsRunner is the docker image to run the linter. 25 | // Optional, if not empty, use the docker image to run the linter. 26 | // e.g. "golang:1.23.4" 27 | DockerAsRunner string `json:"dockerAsRunner,omitempty"` 28 | } 29 | ``` 30 | 31 | 在执行 linter 时,如果 linter 的 DockerImage 字段有值,则使用该字段指定的 docker image 来运行 linter。该 linter 涉及到运行环境的配置,也会同步生效。克隆的仓库代码,也会挂载到 docker image 的 工作目录下,方便 linter 进行处理。 32 | 33 | 处理完成后,linter 的输出,会写入到克隆的仓库代码的根目录下,方便 reviewbot 进行处理。后续流程不变。 34 | 35 | ## 劣势 36 | 37 | 该方案的优点是,能够支持任意类型的自定义运行环境,且在技术实现上,也能简化不少。用户在使用时,只需要关注单个 linter 的配置,而不需要关注节点的选择。 38 | 39 | 但是,可能也有一些问题,比如: 40 | 41 | - 环境依赖: 42 | 43 | - 如果要支持自定义 linter 的运行环境,那 reviewbot 的运行环境,也需要支持 docker。 44 | - reviewbot 的运行环境,需要拉取和保持 docker image,会使 reviewbot 占用的存储空间增加。 45 | 46 | - 运行时资源消耗: 47 | 48 | - 多个 Docker 容器同时运行可能会消耗大量系统资源,特别是在处理大型项目或多个 PR 时。 49 | - 如果 Docker 镜像在执行时需要从网络上拉取,这增加了对网络的依赖,可能影响可靠性。 50 | -------------------------------------------------------------------------------- /docs/proposal/custom-runner.md: -------------------------------------------------------------------------------- 1 | # NOTE:未采纳本方案,如有需要,请移步[最新 Proposal](custom-runner-v2.md) 2 | 3 | ## Proposal: 自定义执行节点 4 | 5 | ## Background 6 | 7 | > see [issue](https://github.com/qiniu/reviewbot/issues/215) 8 | 9 | 随着引入的 linter 越来越多,执行环境需要安装的依赖也越来越多, 此时环境本身的维护成本会越来越高。且如果执行环境是 docker 或者 k8s,image 的 size 也会越来越大, 这必然不利于分发和维护。 10 | 11 | 所以本提案的目标是,提供一种机制,让用户可以自定义执行节点,从而避免上述问题。 12 | 13 | ## 设计原则 14 | 15 | 从简化管理和使用的角度考虑: 16 | 17 | - 不推荐太多节点,只有在必要时才需要拆分节点。比如不同的 org 有不同的业务类型,需要安装不同的基础组件;单个 image 的 size 已超 1G 等等 18 | - 不假设运行环境是 docker 或者 k8s,节点可以是任意类型,只要能运行 Reviewbot 的二进制文件即可 19 | - Reviewbot 本身暂时不负责节点的启、停 20 | - 节点的选择是基于 repo/linter 粒度的 21 | > 当然,如果后续有需要,可以做更灵活的配置,比如基于 repo 粒度,基于 org 粒度,或者基于 linter 粒度 22 | 23 | ## 整体设计 24 | 25 | 不破坏现有部署架构,请求路径仍然是 webhook -> reviewbot 实例. 由 Reviewbot 实例来决策是自己执行还是转发给其他实例执行。 26 | 27 | > 方便起见,Reviewbot 实例下面会简称为节点 28 | 29 | 为支持节点选择,引入一个节点管理服务,用于管理节点。节点从节点管理服务获取到全局的节点列表。 30 | 31 | - 不通过数据库来保存节点信息,是因为节点会有启停变化,需要做心跳保活,并即时感知。 32 | 33 | - 当然也可以用 consul 来实现,但需要做一定的适配,这个后面再考虑。当前认为引入 consul 会增加项目理解复杂度,所以考虑用一个简单小服务来实现 34 | 35 | 节点管理服务需要支持节点注册、保活、心跳上报、节点列表获取等操作。节点管理服务设计为单实例服务,无状态,方便部署和维护。 36 | 37 | 节点在启动阶段,会向节点管理服务注册,并定时上报心跳。类似: 38 | 39 | ```bash 40 | ./reviewbot -discovery.http-addr=http://127.0.0.1:8080/hook -discovery.ws-addr=ws://127.0.0.1:8081 -discovery.node-labels=node1,node2 ... 41 | ``` 42 | 43 | Node Labels 是节点的标签,由部署节点时指定,可以有多个,用于区分不同的节点。其值是自定义的。 44 | 45 | Label 机制的实现将会参考 https://onsi.github.io/ginkgo/#spec-labels. 46 | 47 | 节点管理服务使用 websocket 与节点保持连接,并维护一个全局的节点列表,并定时更新。 48 | 49 | 节点从节点管理服务获取到全局的节点列表,当接受到 Webhook 事件时,会根据 Webhook 的 repo/linter 信息,从全局的节点列表中选择一个节点来执行。 50 | 51 | 节点的选择逻辑如下: 52 | 53 | - 如果 PR 的 repo/linter 信息在配置中配置了 node_labels,那么将选择匹配该规则的节点 54 | - 节点知道自己的 Labels,如果当前节点匹配了 node_labels,则优先在当前节点继续执行 55 | - 如果当前节点不匹配,则通过负载均衡策略从匹配的节点中选择一个节点 56 | - 如果选择的节点不可用,那么会重试,继续选择一个可用的节点 57 | - 如果 PR 的 repo/linter 没有配置强制指定 node_labels,那优先选择没有 Label 的节点执行,如果都没有,则继续在当前节点执行 58 | 59 | 节点会缓存全局的节点列表,当节点管理服务有更新时,会全量更新缓存的节点列表。当节点管理服务不可用时,缓存的节点列表仍然有效。而当节点管理服务重新可用时,会自动重新建立连接。 60 | 61 | 考虑在节点管理服务启动的初始阶段,从节点管理服务获得的节点列表可能少于实际的节点数量,节点侧会只有在从节点管理服务获得的节点数量大于等于当前缓存的节点数量时,才会更新缓存的节点列表。这个逻辑只会在节点管理服务启动的初始阶段生效,当节点管理服务可用时间超过 2 分钟后,会强制更新缓存的节点列表。 62 | 63 | ## 详细设计 64 | 65 | ### 节点管理服务结构 66 | 67 | ```go 68 | type Node struct { 69 | HTTPAddress string 70 | WSConn *websocket.Conn 71 | Tags []string 72 | LastPing time.Time 73 | } 74 | 75 | type NodeManager struct { 76 | // key 是 node 的 HTTPAddress,保证唯一性 77 | nodes map[string]*Node 78 | mu sync.Mutex 79 | } 80 | 81 | // 返回给节点的信息 82 | type NodeInfo struct { 83 | Name string `json:"name"` 84 | Tags []string `json:"tags"` 85 | HTTPAddress string `json:"http_address"` 86 | } 87 | ``` 88 | 89 | ### 90 | -------------------------------------------------------------------------------- /docs/static/ai-details.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/ai-details.png -------------------------------------------------------------------------------- /docs/static/ci-status.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/ci-status.png -------------------------------------------------------------------------------- /docs/static/found-unexpected-issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/found-unexpected-issue.png -------------------------------------------------------------------------------- /docs/static/found-valid-issue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/found-valid-issue.png -------------------------------------------------------------------------------- /docs/static/github-check-run-annotations.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/github-check-run-annotations.png -------------------------------------------------------------------------------- /docs/static/github-check-run.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/github-check-run.png -------------------------------------------------------------------------------- /docs/static/github-pr-review-comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/github-pr-review-comments.png -------------------------------------------------------------------------------- /docs/static/issue-comment.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/issue-comment.png -------------------------------------------------------------------------------- /docs/static/rebase-suggestion.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/static/rebase-suggestion.jpg -------------------------------------------------------------------------------- /docs/website/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /docs/website/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /docs/website/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /docs/website/docs/architecture.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 架构与流程 3 | sidebar_position: 3 4 | --- 5 | 6 | `Reviewbot` 目前主要作为 GitHub Webhook 服务运行,会接受 GitHub Events,然后执行各种检查,若检查出问题,会精确响应到对应代码上。 7 | 8 | ![architecture](./img/arch.png) 9 | 10 | ## 基本流程如下: 11 | 12 | - 事件进来,判断是否是 Pull Request 13 | - 获取代码: 14 | - 获取 PR 影响的代码 15 | - clone 主仓 16 | - 主仓会作为缓存 17 | - checkout PR,并放置在临时目录 18 | - pull 子模块 19 | - 仓库若使用submodule管理则自动拉取代码 20 | - 进入 Linter 执行逻辑 21 | - 筛选 linter 22 | - 默认只要支持的 linter 都对所有仓库适用,除非有单独配置 23 | - 单独配置需要通过配置文件显式指定 24 | - 显式指定的配置会覆盖默认的配置 25 | - 执行 linter 26 | - 通用逻辑 27 | - 执行相应命令,拿到输出结果 28 | - filter 输出的结果,只获取本次 PR 关心的部分 29 | - 有的 linter 关心代码 30 | - 有的 linter 关心其他 31 | - 做反馈 32 | - 有的 linter 给 Code Comment,精确到代码行 33 | - 有的 linter 给 issue comment 34 | 35 | ## 如何添加新的 Linter? 36 | 37 | - 请从 [issues](https://github.com/qiniu/reviewbot/issues) 列表中选择你要处理的 Issue. 38 | - 当然,如果没有,你可以先提个 Issue,描述清楚你要添加的 Linter 39 | - 编码 40 | - 基于 linter 关注的语言或领域,[选好代码位置](https://github.com/qiniu/reviewbot/tree/master/internal/linters) 41 | - 绝大部分的 linter 实现逻辑分三大块: 42 | - 执行 linter,一般是调用相关的可执行程序 43 | - 处理 linter 的输出,我们只会关注跟本次 PR 相关的输出 44 | - 反馈 跟本次 PR 相关的输出,精确到代码行 45 | - 部署,如果你的 linter 是外部可执行程序,那么就需要在 [Dockerfile](https://github.com/qiniu/reviewbot/blob/master/Dockerfile) 中添加如何安装这个 linter 46 | - 文档,为方便后续的使用和运维,我们应当 [在这里](https://github.com/qiniu/reviewbot/tree/master/docs/website/docs/components) 添加合适的文档 47 | -------------------------------------------------------------------------------- /docs/website/docs/components/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Linters", 3 | "position": 3, 4 | "collapsed": false, 5 | "link": { 6 | "type": "generated-index", 7 | "description": "我们来了解 Reviewbot 目前已有的功能!" 8 | } 9 | } -------------------------------------------------------------------------------- /docs/website/docs/components/c/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "C/C++", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "C/C++ 相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/c/cppcheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: cppcheck 3 | sidebar_position: 1 4 | --- 5 | 6 | **Reviewbot** 使用 [cppcheck](https://cppcheck.sourceforge.io/) 来检查C/C++代码的工程质量。 7 | 8 | 9 | -------------------------------------------------------------------------------- /docs/website/docs/components/doc/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Doc规范", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Doc规范 相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/doc/note-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: note-check 3 | sidebar_position: 1 4 | --- 5 | 6 | 参考[go doc note](https://pkg.go.dev/go/doc#Note)要求,**Reviewbot** 推荐在写Note时,带上自己的GitHub ID,类似: 7 | 8 | ```go 9 | // TODO(CarlJi): 需要处理xx情况 10 | ``` 11 | 12 | 这样好处是能清晰的知道这个标记是谁加的。 13 | 14 | :::tip 15 | 当然,通过`git history`也能看到这行是谁改的,但不够精确,容易被更改。 16 | ::: -------------------------------------------------------------------------------- /docs/website/docs/components/git/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Git协作", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Git协作 相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/git/commit-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: commit-check 3 | sidebar_position: 1 4 | --- 5 | 6 | 创建优雅的`git commit`,在大型协作项目中非常重要。 7 | 8 | **commit-check** 重点推荐以下两点实践: 9 | * 不要有重复的`commit message` 10 | * 常用`git rebase`命令,消除多余的`Merge`记录,也会让`git history timeline` 更加的优雅,少分叉 11 | 12 | :::info 13 | 这里有一篇从实际使用角度出发的 `git flow` 文档,里面也包含视频讲解,值得参阅。 14 | https://github.com/qiniu/reviewbot/blob/master/docs/engineering-practice/git-flow-instructions_zh.md 15 | ::: 16 | -------------------------------------------------------------------------------- /docs/website/docs/components/go/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Go", 3 | "position": 1, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Go 语言相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/go/gofmt.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gofmt 3 | sidebar_position: 2 4 | --- 5 | 6 | 所有的go代码都需经过[gofmt](https://pkg.go.dev/cmd/gofmt)格式化,已是go工程领域的既定事实。 7 | 8 | **Reviewbot** 会执行**gofmt**检查,确保这个规范在组织内被有效贯彻。 9 | 10 | 值得注意的是,如果**Reviewbot**检测出格式问题,她会以[suggest changes](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests/incorporating-feedback-in-your-pull-request)形式直接comment目标代码行,相对优雅些。 11 | 12 | :::info 13 | 由于 check run 模式下不支持GitHub Suggestion 功能,因此,gofmt 固定使用 GitHub PR Review 风格来反馈捕获到的问题。 14 | Issue详情参见: https://github.com/qiniu/reviewbot/issues/166 15 | ::: -------------------------------------------------------------------------------- /docs/website/docs/components/go/golangci_lint.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: golangci-lint 3 | sidebar_position: 1 4 | --- 5 | 6 | [golangci-lint](https://github.com/golangci/golangci-lint) 是 go 语言领域非常优秀的 linters 执行器,她内置支持了很多 go 领域 linter 工具。 7 | 8 | **Reviewbot** 也使用 **golangci-lint** 来规范 go 代码的编写。 9 | 10 | 不过,从简化配置以及适配各种场景的角度,`Reviewbot` 本身也引入了一些设计。 11 | 12 | ### 执行逻辑 13 | 14 | 有两种模式: 15 | 16 | - **缺省模式** 如果没有设置 Command, 或者 Command 唯一且为`golangci-lint`, 那么 Args 参数在子命令是`run`的情况下,会做如下检验(其他子命令,不会做处理) 17 | 18 | - 如果没有设置`--timeout`, 那么默认设置为 `--timeout=5m0s` 19 | - 如果没有设置`--allow-parallel-runners`, 那么默认设置为 `--allow-parallel-runners=true` 20 | - 如果没有设置`--out-format`, 那么默认设置为 `--out-format=line-number` 21 | - 如果没有设置`--print-issued-lines`, 那么默认设置为 `--print-issued-lines=false` 22 | 23 | - **自定义模式** 如果设置了 Command, 且 Command 不为`golangci-lint`, 此模式下执行器将不会做任何的验证和补充,将按照配置的内容严格执行 24 | 25 | - 此模式一般应用于比较复杂的项目,此类项目一般需要在执行命令前做一些前置工作 26 | - 对于结果解析,执行器会通过模式 **`^(._?):(\d+):(\d+)?:? (._)$`** 匹配所有的输出行,符合的情况会做相应上报. 但这种模式下有可能会有很多不预期的输出内容,有可能会干扰日常的监测运营。所以,更推荐的把 golangci-lint 的输出内容重定向到 **$ARTIFACT** 目录下,执行器会优先解析这个目录下的内容。 27 | 28 | - 参考例子: 29 | 30 | ```yaml 31 | qbox/kodo-ops: 32 | golangci-lint: 33 | enable: true 34 | comamnd: 35 | - /bin/bash 36 | - -c 37 | - -- 38 | args: 39 | - cd web && yarn build 40 | - golangci-lint run --enable-all --timeout=5m0s --allow-parallel-runners=true >> $ARTIFACTS/lint.log 2>&1 41 | ``` 42 | 43 | :::info 44 | Command 和 Args 的使用姿势跟 Kubernetes Pod 中的 Command 和 Args 的 Yaml 用法一致 45 | ::: 46 | 47 | ### golangci.yml 配置文件 48 | 49 | 可以选择继承全局的配置文件,也可以针对特定仓库选择自己的配置文件。 50 | 51 | 从使用维度考虑: 52 | 53 | - 如果仓库中包含 `.golangci.yml`配置,将优先使用该配置 54 | - 如果仓库中不包含相关配置,那么可以选择从全局或者组织下继承配置。当然,不管全局还是组织,都要保证目标配置文件要在相关执行执行的目录下存在。 55 | 56 | - 全局设置的例子 57 | 58 | ```yaml 59 | globalDefaultConfig: # global default settings, will be overridden by qbox org and repo specific settings if they exist 60 | golangcilintConfig: "config/linters-config/.golangci.yml" # golangci-lint config file to use 61 | ``` 62 | 63 | - 从组织维度 64 | ```yaml 65 | qbox: 66 | golangci-lint: 67 | enable: true 68 | configPath: "config/linters-config/.golangci.yml" // TODO(CarlJi): 路径检查 69 | ``` 70 | 71 | ### 自动选择执行目录 72 | 73 | 默认情况下,执行器会在仓库的根目录下执行,但是对于很多 monorepo 来讲,其相关的 go 代码却在特定的目录下。这时候,可能会遇到如下错误: 74 | 75 | ```bash 76 | Unexpected: level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies" 77 | ``` 78 | 79 | - 在缺省模式下执行器会根据 Github PR 所涉及到的文件目录进行 go.mod 文件的查找,设置 go.mod 文件所在目录为执行器的工作目录,并执行go mod tidy 下载相关依赖;若一个PR中涉及多个 go.mod 文件,则分别执行 go mod tidy 进行依赖下载,并且 golangci-lint 执行器也分别执行一次。 80 | - 在自定义模式下,若自定义参数不指定相应的工作目录,执行器则会在仓库根目录下执行。由于 golangci-lint 执行时不会下载相关依赖项,因此自定义模式下建议手动在linter执行目录下添加 go mod tidy 命令 ,否则可能导致golangci-lint执行失败。 81 | 82 | 83 | -------------------------------------------------------------------------------- /docs/website/docs/components/go/gomodcheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: gomodcheck 3 | sidebar_position: 3 4 | --- 5 | 6 | `gomodcheck` 是一个专门用于检查 Go 项目中 `go.mod` 文件的 linter。它的主要目的是限制跨仓库的 local replace 使用,以确保项目依赖管理的一致性和可重现性。 7 | 8 | 比如: 9 | 10 | ```go 11 | replace github.com/qiniu/go-sdk/v7 => ../another_repo/src/github.com/qiniu/go-sdk/v7 12 | ``` 13 | 14 | `../another_repo` 代表当前仓库的父目录下的 `another_repo` 目录. 这种用法非常的不推荐. 15 | 16 | ### 为什么要限制跨仓库的 local replace? 17 | 18 | 1. **可重现性**: 跨仓库的 local replace 使得构建过程依赖于本地文件系统结构,这可能导致不同环境下的构建结果不一致。 19 | 20 | 2. **依赖管理**: 它绕过了正常的依赖版本控制,可能引入未经版本控制的代码。 21 | 22 | 3. **协作困难**: 其他开发者或 CI/CD 系统可能无法访问本地替换的路径,导致构建失败。 23 | 24 | 4. **版本跟踪**: 使用 local replace 难以追踪依赖的具体版本,增加了项目维护的复杂性。 25 | 26 | ### 这种情况推荐怎么做? 27 | 28 | 尽可能使用正式发布的依赖版本, 即使是 private repo 也是一样的。 29 | 30 | :::info 31 | 32 | 可以使用 go env -w GOPRIVATE 来设置私有仓库, 方便 go mod 下载依赖. 33 | 34 | ::: 35 | -------------------------------------------------------------------------------- /docs/website/docs/components/img/docsVersionDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/img/docsVersionDropdown.png -------------------------------------------------------------------------------- /docs/website/docs/components/img/localeDropdown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/img/localeDropdown.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Java", 3 | "position": 4, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "java 语言相关的检查" 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/commentsrule.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/commentsrule.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/pmdrulefileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/pmdrulefileset.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/pmdruleurlset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/pmdruleurlset.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/stylecheckruleconfig.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/stylecheckruleconfig.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/stylecheckrulefileset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/stylecheckrulefileset.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/img/stylecheckurlset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/components/java/img/stylecheckurlset.png -------------------------------------------------------------------------------- /docs/website/docs/components/java/pmdcheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: pmd 3 | sidebar_position: 2 4 | --- 5 | 6 | [PMD](https://docs.pmd-code.org/pmd-doc-7.1.0/index.html) 是一款采用 BSD 协议发布的 Java 程序代码检查工具。该工具可以做到检查 Java 代码中是否含有未使用的变量、是否含有空的抓取块、是否含有不必要的对象等。 7 | **Reviewbot** 默认使用的是PMD提供的BestPractices规则库[BestPractices](https://github.com/pmd/pmd/tree/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices)。 8 | 9 | 默认情况下, **Reviewbot** 使用以下命令来对Java代码进行检查: 10 | 11 | ```bash 12 | pmd check -f emacs -R bestpractices.xml xx1.java xx2.java 13 | ``` 14 | 15 | :::info 16 | PMD官方提供了许多其他的代码检查规则。 17 | 详情参见: https://github.com/pmd/pmd/tree/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule 18 | ::: 19 | ### 规则文件配置 20 | PMD的检查规则配置支持两种方式,在`config.yaml`文件中,globalDefaultConfig>javapmdcheckruleConfig项配置对应的pmd规则文件路径或者网络路径。 21 | - 1.文件路径: 22 | ```yaml 23 | globalDefaultConfig: # global default settings, will be overridden by qbox org and repo specific settings if they exist 24 | javapmdcheckruleConfig: "config/linters-config/.java-bestpractices.xml" 25 | ``` 26 | - 2.网络路径: 27 | ```yaml 28 | globalDefaultConfig: # global default settings, will be overridden by qbox org and repo specific settings if they exist 29 | javapmdcheckruleConfig: "https://raw.githubusercontent.com/pmd/pmd/master/pmd-java/src/main/resources/category/java/bestpractices.xml" 30 | ``` 31 | - 如果设置为空(""),reviewbot会自动下载PMD提供的BestPractices规则库[BestPractices](https://github.com/pmd/pmd/tree/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices) 32 | 33 | ### 自定义规则 34 | 35 | 在实际的项目实施过程中,并不是系统的提供的所有规则都适合项目需要,有时候会需要对系统提供的规则进行裁剪,将不太适合项目需要的规则进行注释。 36 | - 如图,将AbstractClassWithoutAbstractMethod规制进行注释后,PMD 将不都会对代码针对该项规则的检查 37 | ![img.png](img/commentsrule.png) 38 | 39 | - 如果想进一步编写更加符合项目特征的代码检查规则,还可以自动编写PMD规则加入到Reviewbot指定的规则文件中。编写方式可以参见 [PMD自定义规则官方文档](https://docs.pmd-code.org/latest/pmd_userdocs_extending_writing_rules_intro.html) 40 | - [PMD自定义规则入门](https://docs.pmd-code.org/latest/pmd_userdocs_extending_your_first_rule.html) 41 | -------------------------------------------------------------------------------- /docs/website/docs/components/java/stylecheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: stylecheck 3 | sidebar_position: 1 4 | --- 5 | 6 | [CheckStyle](https://github.com/checkstyle/checkstyle) 是 SourceForge 下的一个项目,提供了一个帮助 JAVA 开发人员遵守某些编码规范的工具。它能够自动化代码规范检查过程,从而使得开发人员从这项重要,但是枯燥的任务中解脱出来。CheckStyle提供了大部分功能都是对于代码规范的检查。 7 | **Reviewbot** 默认使用的是sun提供的规则库[sun_style](https://checkstyle.org/sun_style.html)。 8 | 9 | 默认情况下, **Reviewbot** 使用以下命令来对Java代码进行stylecheck检查: 10 | 11 | ```bash 12 | java -jar checkstyle-10.17.0-all.jar run -c sun_checks.xml xx1.java xx2.java 13 | ``` 14 | 15 | :::info 16 | Checkstyle提供了许多的代码风格检查规则。 17 | 详情参见: https://checkstyle.org/checks.html 18 | ::: 19 | ### 规则文件配置 20 | stylecheck的检查规则配置支持两种方式。在`config.yaml`文件中,globalDefaultConfig>javastylecheckruleConfig项配置对应的stylecheck文件路径和网络路径 21 | - 1.文件路径: 22 | ```yaml 23 | globalDefaultConfig: # global default settings, will be overridden by qbox org and repo specific settings if they exist 24 | javastylecheckruleConfig: "config/linters-config/.java-sun-checks.xml" 25 | ``` 26 | - 2.网络路径: 27 | ```yaml 28 | globalDefaultConfig: # global default settings, will be overridden by qbox org and repo specific settings if they exist 29 | javastylecheckruleConfig: "https://raw.githubusercontent.com/checkstyle/checkstyle/master/src/main/resources/sun_checks.xml" 30 | ``` 31 | 32 | 如果设置为空(""),reviewbot会自动下载sun提供的规则库[sun_style](https://checkstyle.org/sun_style.html) 33 | 34 | ### 规则设置 35 | 在实际的项目实施过程中,并系统提供的所有规则并不一定都适合项目需要,需要将不太适合项目需要的规则进行注释,或者根据项目需要对规则的属性进行修改。 36 | 37 | 规则及相关属性说明见[官方文档](https://checkstyle.org/checks.html) 38 | 39 | - 举例:如图 可以根据项目需要修改property 设置代码方法长度的检查 40 | ![img.png](img/stylecheckruleconfig.png) -------------------------------------------------------------------------------- /docs/website/docs/components/lua/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Lua", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Lua 语言相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/lua/luacheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: luacheck 3 | sidebar_position: 2 4 | --- 5 | 6 | **Reviewbot** 使用 [luacheck](https://github.com/mpeterv/luacheck) 来检查Lua代码的工程质量。 -------------------------------------------------------------------------------- /docs/website/docs/components/shell/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Shell", 3 | "position": 5, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Shell 语言相关的检查", 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/components/shell/shellcheck.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: shellcheck 3 | sidebar_position: 1 4 | --- 5 | 6 | **shellcheck** 是 [koalaman](https://github.com/koalaman) 开源的shell静态检查工具,介绍较全,star较多,看起来比较受欢迎。 7 | 8 | * [官方链接 ](https://github.com/koalaman/shellcheck) 9 | 10 | **Reviewbot** 使用 **shellcheck** 来规范shell编写。 11 | 12 | :::tip 13 | shellcheck有很多checks,但其文档放在GitHub仓库的WIKI中,感觉不大明显。 14 | 15 | 要想找到关于某check的具体的文档,可以[参考这篇](https://github.com/koalaman/shellcheck/wiki/Checks) 16 | ::: 17 | 18 | 19 | -------------------------------------------------------------------------------- /docs/website/docs/configuration.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 配置 3 | sidebar_position: 4 4 | --- 5 | 6 | `Reviewbot` 尽可能追求 **无配置**,常见的行为都会固定到代码逻辑中。但针对一些特殊需要,也可以通过配置完成. 7 | 8 | 所有可以配置项,都定义在 `config/config.go` 文件中,可以参考这个文件来配置。 9 | 10 | 以下是一些常见的配置场景: 11 | 12 | ### 调整执行命令 13 | 14 | linters 一般都是用默认命令执行,但是我们也可以调整命令,比如 15 | 16 | ```yaml 17 | qbox/kodo: 18 | linters: 19 | staticcheck: 20 | workDir: "src/qiniu.com/kodo" 21 | ``` 22 | 23 | 这个配置意味着,针对`qbox/kodo`仓库代码的`staticcheck`检查,要在`src/qiniu.com/kodo`目录下执行。 24 | 25 | 我们甚至可以配置更复杂的命令,比如: 26 | 27 | ```yaml 28 | qbox/kodo: 29 | linters: 30 | golangci-lint: 31 | command: 32 | - "/bin/sh" 33 | - "-c" 34 | - "--" 35 | args: 36 | - | 37 | source env.sh 38 | cp .golangci.yml src/qiniu.com/kodo/.golangci.yml 39 | cd src/qiniu.com/kodo 40 | export GO111MODULE=auto 41 | go mod tidy 42 | golangci-lint run --timeout=10m0s --allow-parallel-runners=true --print-issued-lines=false --out-format=line-number >> $ARTIFACT/lint.log 2>&1 43 | ``` 44 | 45 | 这里的 command 和 args,与 Kubernetes Pod 的 command 和 args 类似,可以参考[Kubernetes Pod](https://kubernetes.io/docs/concepts/workloads/pods/) 46 | 47 | **$ARTIFACT** 环境变量值得注意,这个环境变量是 `Reviewbot` 内置的,用于指定输出目录,方便排除无效干扰。因为 `Reviewbot` 最终只会关心 linters 的输出,而在这个复杂场景下,shell 脚本会输出很多无关信息,所以最好需要通过这个环境变量来指定输出目录,让 `Reviewbot` 只解析这个目录下的文件。 48 | 49 | ### 关闭 Linter 50 | 51 | 比如,想在 `qbox/net-gslb` 仓库不执行`golangci-lint`检查,可以这么配置: 52 | 53 | ```yaml 54 | qbox/net-gslb: 55 | linters: 56 | golangci-lint: 57 | enable: false 58 | ``` 59 | 60 | ### 通过 Docker 执行 linter 61 | 62 | 比如,想在 `qbox/net-gslb` 仓库执行 `golangci-lint` 检查,但又不想在本地安装 `golangci-lint`,可以通过配置 Docker 镜像来完成: 63 | 64 | ```yaml 65 | qbox/net-gslb: 66 | linters: 67 | golangci-lint: 68 | dockerAsRunner: 69 | image: "golangci/golangci-lint:v1.54.2" 70 | ``` 71 | 72 | 通常,你的镜像需要包含 `golangci-lint` 命令,以及 `golangci-lint` 执行时需要的所有依赖(比如 `golangci-lint` 需要 `golang` 环境,那么你的镜像需要包含 `golang`)。 73 | -------------------------------------------------------------------------------- /docs/website/docs/getting-started/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "开始上手", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": " " 7 | } 8 | } -------------------------------------------------------------------------------- /docs/website/docs/getting-started/development.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 参与开发 3 | sidebar_position: 2 4 | --- 5 | 6 | **Reviewbot** 当前设计上主要作为 [webhook server](https://docs.github.com/en/webhooks/about-webhooks),通过接受 GitHub 事件,针对目标仓库的 PR,执行各种 linter 检查,判断代码是否符合规范。 7 | 8 | 所以,如果想在本地开发环境调试**Reviewbot**,需要准备如下: 9 | 10 | - GitHub 认证 - 有以下两种方式 11 | - [personal access tokens](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens)方式 12 | - [GitHub APP](https://docs.github.com/en/apps) 方式 13 | - 启动**Reviewbot** 14 | 15 | ```bash 16 | # access token 方式 17 | go run . -access-token= -webhook-secret= -config ./config/config.yaml -log-level 0 18 | # Github APP 方式 19 | go run . -webhook-secret= -config ./config/config.yaml -log-level 0 -app-id= -app-private-key= 20 | ``` 21 | 22 | - 测试用的 git 仓库 - 要有 admin 权限,这样可以拿到相应的 GitHub 事件 23 | - 参考 [如何给仓库配置 Webhook](https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks) 24 | - 本地模拟发送 GitHub 事件,可以借助工具 [phony](https://github.com/qiniu/reviewbot/tree/master/tools/phony) 25 | -------------------------------------------------------------------------------- /docs/website/docs/getting-started/images/comments.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/getting-started/images/comments.png -------------------------------------------------------------------------------- /docs/website/docs/getting-started/images/detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/getting-started/images/detail.png -------------------------------------------------------------------------------- /docs/website/docs/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 安装部署 3 | sidebar_position: 1 4 | --- 5 | 6 | Reviewbot 提供以下两种方式访问GitHub: 7 | 8 | * Github APP 方式 (推荐) 9 | * Access Token 方式 10 | 11 | 推荐使用`Github APP`的方式,因为[Access Token 方式不支持GitHub CheckRun 姿势](https://docs.github.com/en/rest/checks/runs?apiVersion=2022-11-28#create-a-check-run) 12 | 13 | :::tip 14 | `Github CheckRun` 姿势看起来相对优雅一些, 一家之言。 15 | ::: 16 | 17 | 创建一个`GitHub APP`也非常方便,参见: 18 | 19 | * 基于实际情况,选择是在 Org 下创建,还是在 个人账号下创建. 20 | * Org: `https://github.com/organizations/>/settings/apps` 21 | * 个人: `https://github.com/settings/apps` 22 | 23 | * 设置合适的 APP的权限 24 | * Repository permissions 25 | * Checks: Read & write 26 | * Commit statuses: Read & write 27 | * Pull requests: Read & write 28 | * 订阅需要的事件 29 | * Pull Request 30 | * Pull Request Review 31 | * Pull Request Review Comment 32 | * Pull Request Review Thread 33 | * Push 34 | * Release 35 | * Commit Comment 36 | 37 | 当创建完APP之后,我们就可以获得 `APP ID` 和 `APP Private Key`, 这些信息在部署时需要。 38 | 39 | 当然仍然可以是用`Access Token`方式,只不过反馈会以Comment形式存在。 40 | 41 | 创建`Access Token`请参考[GitHub官方文档](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens). 42 | 43 | ## 部署 44 | 45 | 推荐通过Docker方式,部署到kubernetes集群 46 | 47 | * 镜像构建,请参考 [Dockerfile](https://github.com/qiniu/reviewbot/blob/master/Dockerfile) 48 | * Kubernetes 部署: [Reviewbot.yaml](https://github.com/qiniu/reviewbot/blob/master/deploy/reviewbot.yaml) 49 | 50 | 待服务部署好之后,配置上合适的域名,然后将相关域名配置到GitHub Hook区域即可。 51 | 52 | 之后即可观察,服务是否能接受到GitHub事件,并正常执行。 53 | 54 | -------------------------------------------------------------------------------- /docs/website/docs/getting-started/quickinstall.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 快速部署 3 | sidebar_position: 3 4 | --- 5 | 6 | Reviewbot 提供以下两种方式访问GitHub: 7 | 8 | * GitHub App 方式 (推荐) 9 | * Access Token 方式 10 | 11 | Reviewbot推荐使用GitHub App的方式进行集成,这样能更加方便的无缝代码管理流程中。本文按照GitHub App的方式进行集成 12 | 13 | ### 准备 14 | 15 | 16 | 17 | 在集成部署之前,我们要先了解Reviewbot需要用到的一些配置参数和配置文件。 18 | | **名称** | **是否必须** | **用途** | **获取方式** | 19 | |:------:|:----------:|:---------:|:------:| 20 | | ssh-secret | 必须 | 用来 拉取待检查代码 |本地生成ssh_key私钥,公钥添加到对应GitHub账号 | 21 | | access-token | 必须 | 用来触发使用相关GitHub API |GitHub账号setting中获取 | 22 | | app-id | 必须 | GitHub API使用 |GitHub App中获取 | 23 | | github-app-secret | 必须 | GitHub API使用 |创建GitHub App时设置Private Key时生成, | 24 | | webhook-secret | 非必须 | 验证Webhook请求的有效性 |保持跟GitHub Webhook的设置保持一致,如果GitHub上没有设置就不用配置 | 25 | 26 | 其他: 27 | | 名称 | 是否必须 | 用户| 获取方式 | 28 | |:------:|:-------:|:------:|:------:| 29 | | config | 非必须 | Reviewbot配置文件|在没有配置的情况下,会使用系统默认配置。配置方式参看config/config.yaml | 30 | | golangci-config | 非必须 | golang语言静态检查配置|在没有配置的情况下,会使用系统默认配置。配置方式参看config/linters-config/.golangci.yml | 31 | | javapmdruleconfig | 非必须 | java pmd 检查规则|在没有配置的情况下,会使用系统默认配置。配置方式参看 [BestPractices](https://github.com/pmd/pmd/tree/master/pmd-java/src/main/java/net/sourceforge/pmd/lang/java/rule/bestpractices)| 32 | | javastylecheckruleconfig | 非必须 | java style check 规则|在没有配置的情况下,会使用系统默认配置。配置方式参看[sun_style](https://checkstyle.org/sun_style.html) | 33 | 34 | 35 | ### 安装Reviewbot服务 36 | Reviewbot的安装是支持多种方式的,支持在物理机,虚拟机,容器上安装,因为其中还会涉及到运行环境的安装,推荐使用工程中的`Dockerfile`进行容器化的安装,步骤如下。 37 | #### 构建镜像 38 | 1. 使编译Reviewbot文件,使用 `make all` 或者 `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build .` 39 | 2. 构建镜像:`docker build -t reviewbot-customimageid . ` 40 | 3. tag镜像:`docker tag reviewbot-customimageid:latest your_dockerimage_repo/reviewbot-customimageid:latest` 41 | 4. 推送镜像:`docker push your_dockerimage_repo/reviewbot-customimageid:latest` 42 | 43 | #### 部署镜像 44 | Docker 部署可以使用k8s进行部署,也可以在一台安装了docker的机器上进行部署。 45 | - k8s部署(推荐): 46 | 在k8s 上创建configmap,将对应的配置设置到configmap中。 47 | 1. 创建config 48 | ``` shell 49 | kubectl create configmap cm-reviewbot --from-file=config=/Users/mac/Documents/project/reviewbot/deploy/config -n reviewbot 50 | ``` 51 | 2. 设置 access-token 和 webhook-secret: 52 | ``` shell 53 | kubectl create secret generic github --from-literal=access-token=ghp_5vV5DueLdf0HyS9KlB4usWRJvcziK2eFFMS --from-literal=webhook-secret=910399965ee2cbb8fddad085dfda6c1cc263 -n reviewbot 54 | ``` 55 | 3. 创建 ssh_sercret: 56 | ``` shell 57 | kubectl create secret generic ssh-secret --from-file=ssh-secret=/Users/mac/.ssh/id_rsa -n reviewbot 58 | ``` 59 | 4. 创建 app-id和app-installation-id: 60 | ``` shell 61 | kubectl create secret generic github-app --from-literal=app-id=957941 --from-literal=app-installation-id=53342102 -n reviewbot 62 | ``` 63 | 5. 创建 github-app-secret,pem 文件在创建GitHub App页面 生成 64 | ``` shell 65 | kubectl create secret generic github-app-secret --from-file=github-app-secret=/Users/mac/Downloads/qiniureviewbot2.2024-07-31.private-key.pem -n reviewbot 66 | ``` 67 | 6. 通过工程中提供的`reviewbot.yaml` 在K8S上通过命令行 `kubectl apply -f reviewbot.yaml` 进行初始化的部署。 68 | 7. 如果镜像重新编译了,可以同通过下面命令重新设置镜像: 69 | ``` shell 70 | kubectl set image deployment/reviewbot reviewbot=镜像上传地址/reviewbot-新镜像:latest -n reviewbot 71 | ``` 72 | 8. 启动镜像,镜像文件从configmap中读取配置。 73 | 74 | :::tip 75 | 如果想对Reviewbot进行其他的配置设置,根据`reviewbot.yaml`的配置要求创建对应的configmap。 76 | ::: 77 | 78 | - 本地机器部署 79 | 使用docker 命令启动 编译好的docker 镜像,通过参数的方式传入必须的变量信息,ssh_key文件通过mount的方式挂载到docker。 80 | ``` shell 81 | docker run -p 8888:8888 --mount type=bind,target=/secrets/github_key,source=/Users/mac/.ssh/id_rsa reviewbot-customimageid -access-token=ghp_5vV5DueL4mx0HdddyS9KsWRJvcziK2eMS -webhook-secret=9bc cf10399965ee2cbb8fddad085dfda6c1cc263 -log-level 1 82 | ``` 83 | 84 | #### 测试 85 | 测试本地部署是否成功,可以通过本地模拟发送 GitHub 事件,可以借助工具 [phony](https://github.com/qiniu/reviewbot/tree/master/tools/phony) 86 | 发送webhook的 RecentDeliveries 中的 pull_request.synchronize 请求,发送命令如下 87 | ``` shell 88 | go run . --hmac= -payload ./pull_request.synchronize 请求.json --event=pull_request --address http://部署的机器或容器ip:8888/hook 89 | ``` 90 | 91 | #### 设置外网映射 92 | 如果是通过GitHub App的方式进行集成,需要将部署好的Reviewbot服务,映射到外网ip或者域名上面,使GitHub能访问到。 93 | 94 | ### 创建GitHub App 95 | 1. 创建GitHub App,在Settings 》 Developer settings》 创建一个GitHub App,记录GitHub App ID,在Private keys配置项中过生成Private key,同时下载保存xxx.pem文件 96 | 文件。 97 | 2. 设置权限 98 | * Repository permissions 99 | * Checks: Read & write 100 | * Commit statuses: Read & write 101 | * Pull requests: Read & write 102 | 3. 订阅事件 103 | 订阅需要的事件 104 | * Pull Request 105 | * Pull Request Review 106 | * Pull Request Review Comment 107 | * Pull Request Review Thread 108 | * Push 109 | * Release 110 | * Commit Comment 111 | 4. 设置Webook地址,将设置好的外网映射地址配置在GitHub App的Webhook地址中。 112 | 5. 安装GitHub App,在Settings>Developer Settings>Install App 安装创建的GitHub App。 113 | 114 | ### 触发检查 115 | 1. 在GitHub中 提交PR, 就能触发Reviebot运行,看到本次合并的增量代码代码检查结果和合并建议。 116 | ![comments.png](images/comments.png)![detail.png](images/detail.png) 117 | 118 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /docs/website/docs/img/arch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/docs/img/arch.png -------------------------------------------------------------------------------- /docs/website/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | slug: / 3 | title: Why Reviewbot? 4 | sidebar_position: 1 5 | --- 6 | 7 | 保障有限数量、有限语言的仓库代码质量是不难的,我们只需要利用各种检查工具给相关的仓库一一配置即可。但如果面临的是整个组织,各种语言,各种新旧仓库(300+),且有很多历史遗留问题,又该如何做呢? 8 | 9 | 我们想,最好有一个中心化的静态检查服务,能在极少配置的情况下,就能应用到所有仓库,且能让每一项新增工程实践,都能在组织内高效落地。 10 | 11 | **Reviewbot** 就是在这样的场景下诞生。 12 | 13 | 她受到了行业内很多工具的启发,但又有所不同: 14 | 15 | - 类似 [golangci-lint](https://github.com/golangci/golangci-lint), **Reviewbot** 会是个 Linters 聚合器,但她包含更多的语言和流程规范(go/java/shell/git-flow/doc-style ...),甚至自定义规范 16 | - 参考 [reviewdog](https://github.com/reviewdog/reviewdog), **Reviewbot** 主要也是以 Review Comments 形式来反馈问题,精确到代码行,可以作为质量门禁,持续的帮助组织提升代码质量,比较优雅 17 | - 推荐以 GitHub APP 或者 Webhook Server 形式部署私有运行,对私有代码友好 18 | 19 | 如果你也面临着类似的问题,欢迎尝试**Reviewbot**! 20 | -------------------------------------------------------------------------------- /docs/website/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import type * as Preset from '@docusaurus/preset-classic'; 2 | import type { Config } from '@docusaurus/types'; 3 | import { themes as prismThemes } from 'prism-react-renderer'; 4 | 5 | const config: Config = { 6 | title: 'Reviewbot', 7 | tagline: 'establish software engineering best practices and efficiently promote them within the organization', 8 | favicon: 'img/q.png', 9 | 10 | // Set the production url of your site here 11 | url: 'https://reviewbot-x.netlify.app', 12 | // Set the // pathname under which your site is served 13 | // For GitHub pages deployment, it is often '//' 14 | baseUrl: '/', 15 | 16 | // GitHub pages deployment config. 17 | // If you aren't using GitHub pages, you don't need these. 18 | organizationName: 'qiniu', // Usually your GitHub org/user name. 19 | projectName: 'reviewbot', // Usually your repo name. 20 | 21 | onBrokenLinks: 'throw', 22 | onBrokenMarkdownLinks: 'warn', 23 | 24 | // Even if you don't use internationalization, you can use this field to set 25 | // useful metadata like html lang. For example, if your site is Chinese, you 26 | // may want to replace "en" with "zh-Hans". 27 | i18n: { 28 | defaultLocale: 'zh-Hans', 29 | locales: ['zh-Hans'], 30 | }, 31 | 32 | presets: [ 33 | [ 34 | 'classic', 35 | { 36 | docs: { 37 | routeBasePath: '/', 38 | sidebarPath: './sidebars.ts', 39 | // Please change this to your repo. 40 | // Remove this to remove the "edit this page" links. 41 | editUrl: 42 | 'https://github.com/qiniu/reviewbot/tree/master/docs/website', 43 | }, 44 | blog: false, 45 | theme: { 46 | customCss: './src/css/custom.css', 47 | }, 48 | } satisfies Preset.Options, 49 | ], 50 | ], 51 | 52 | themeConfig: { 53 | // Replace with your project's social card 54 | image: 'img/q.png', 55 | navbar: { 56 | title: 'Reviewbot', 57 | logo: { 58 | alt: 'Reviewbot Logo', 59 | src: 'img/q.png', 60 | }, 61 | items: [ 62 | { 63 | type: 'docSidebar', 64 | sidebarId: 'tutorialSidebar', 65 | position: 'left', 66 | label: '文档', 67 | }, 68 | // { to: '/blog', label: 'Blog', position: 'left' }, 69 | { 70 | href: 'https://github.com/qiniu/reviewbot', 71 | label: 'GitHub', 72 | position: 'right', 73 | }, 74 | ], 75 | }, 76 | footer: { 77 | style: 'dark', 78 | links: [ 79 | ], 80 | copyright: `Copyright © ${new Date().getFullYear()} Qiniu Cloud`, 81 | }, 82 | prism: { 83 | theme: prismThemes.github, 84 | darkTheme: prismThemes.dracula, 85 | }, 86 | docs: { 87 | sidebar: { 88 | hideable: true, 89 | autoCollapseCategories: true, 90 | }, 91 | }, 92 | } satisfies Preset.ThemeConfig, 93 | }; 94 | 95 | export default config; 96 | -------------------------------------------------------------------------------- /docs/website/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "website", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.3.2", 19 | "@docusaurus/preset-classic": "3.3.2", 20 | "@mdx-js/react": "^3.0.0", 21 | "clsx": "^2.1.1", 22 | "prism-react-renderer": "^2.3.0", 23 | "react": "^18.0.0", 24 | "react-dom": "^18.0.0" 25 | }, 26 | "devDependencies": { 27 | "@docusaurus/module-type-aliases": "3.3.2", 28 | "@docusaurus/tsconfig": "3.3.2", 29 | "@docusaurus/types": "3.3.2", 30 | "typescript": "~5.2.2" 31 | }, 32 | "browserslist": { 33 | "production": [ 34 | ">0.5%", 35 | "not dead", 36 | "not op_mini all" 37 | ], 38 | "development": [ 39 | "last 3 chrome version", 40 | "last 3 firefox version", 41 | "last 5 safari version" 42 | ] 43 | }, 44 | "engines": { 45 | "node": ">=18.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /docs/website/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | tutorialSidebar: [ 16 | { 17 | type: 'autogenerated', 18 | dirName: '.', 19 | }, 20 | ], 21 | }; 22 | 23 | export default sidebars; 24 | -------------------------------------------------------------------------------- /docs/website/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the docs directory. 29 | 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 |
47 |
48 | 49 |
50 |
51 | {title} 52 |

{description}

53 |
54 |
55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 |
61 |
62 |
63 | {FeatureList.map((props, idx) => ( 64 | 65 | ))} 66 |
67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /docs/website/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /docs/website/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /docs/website/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /docs/website/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/static/.nojekyll -------------------------------------------------------------------------------- /docs/website/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/website/static/img/q.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/docs/website/static/img/q.png -------------------------------------------------------------------------------- /docs/website/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": ".", 6 | "esModuleInterop": true, 7 | } 8 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qiniu/reviewbot 2 | 3 | go 1.22.5 4 | 5 | require ( 6 | github.com/aws/aws-sdk-go-v2 v1.30.5 7 | github.com/aws/aws-sdk-go-v2/credentials v1.17.32 8 | github.com/aws/aws-sdk-go-v2/service/s3 v1.61.2 9 | github.com/aws/smithy-go v1.20.4 10 | github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 11 | github.com/docker/docker v27.2.0+incompatible 12 | github.com/golang-jwt/jwt v3.2.1+incompatible 13 | github.com/google/go-github/v57 v57.0.0 14 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 15 | github.com/hashicorp/go-version v1.7.0 16 | github.com/opencontainers/image-spec v1.1.0 17 | github.com/prometheus/client_golang v1.19.0 18 | github.com/qiniu/x v1.13.10 19 | github.com/sirupsen/logrus v1.9.3 20 | github.com/stretchr/testify v1.9.0 21 | github.com/tmc/langchaingo v0.1.12 22 | github.com/xanzy/go-gitlab v0.109.0 23 | golang.org/x/mod v0.21.0 24 | golang.org/x/oauth2 v0.20.0 25 | k8s.io/api v0.28.3 26 | k8s.io/apimachinery v0.28.3 27 | k8s.io/client-go v0.28.3 28 | sigs.k8s.io/prow v0.0.0-20230209194617-a36077c30491 29 | sigs.k8s.io/yaml v1.4.0 30 | ) 31 | 32 | require ( 33 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 34 | github.com/Microsoft/go-winio v0.6.1 // indirect 35 | github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.4 // indirect 36 | github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.17 // indirect 37 | github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.17 // indirect 38 | github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.17 // indirect 39 | github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.4 // indirect 40 | github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.19 // indirect 41 | github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.19 // indirect 42 | github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.17 // indirect 43 | github.com/beorn7/perks v1.0.1 // indirect 44 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 45 | github.com/containerd/log v0.1.0 // indirect 46 | github.com/davecgh/go-spew v1.1.1 // indirect 47 | github.com/distribution/reference v0.6.0 // indirect 48 | github.com/dlclark/regexp2 v1.10.0 // indirect 49 | github.com/docker/go-connections v0.5.0 // indirect 50 | github.com/docker/go-units v0.5.0 // indirect 51 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 52 | github.com/felixge/httpsnoop v1.0.4 // indirect 53 | github.com/go-logr/logr v1.4.2 // indirect 54 | github.com/go-logr/stdr v1.2.2 // indirect 55 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 56 | github.com/go-openapi/jsonreference v0.20.2 // indirect 57 | github.com/go-openapi/swag v0.22.4 // indirect 58 | github.com/gogo/protobuf v1.3.2 // indirect 59 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 60 | github.com/golang/protobuf v1.5.4 // indirect 61 | github.com/google/gnostic-models v0.6.8 // indirect 62 | github.com/google/go-cmp v0.6.0 // indirect 63 | github.com/google/go-github/v56 v56.0.0 // indirect 64 | github.com/google/go-querystring v1.1.0 // indirect 65 | github.com/google/gofuzz v1.2.1-0.20210504230335-f78f29fc09ea // indirect 66 | github.com/google/uuid v1.6.0 // indirect 67 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 68 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 69 | github.com/imdario/mergo v0.3.13 // indirect 70 | github.com/josharian/intern v1.0.0 // indirect 71 | github.com/json-iterator/go v1.1.12 // indirect 72 | github.com/klauspost/compress v1.17.9 // indirect 73 | github.com/mailru/easyjson v0.7.7 // indirect 74 | github.com/moby/docker-image-spec v1.3.1 // indirect 75 | github.com/moby/patternmatcher v0.6.0 // indirect 76 | github.com/moby/sys/sequential v0.6.0 // indirect 77 | github.com/moby/sys/user v0.3.0 // indirect 78 | github.com/moby/sys/userns v0.1.0 // indirect 79 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 80 | github.com/modern-go/reflect2 v1.0.2 // indirect 81 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 82 | github.com/opencontainers/go-digest v1.0.0 // indirect 83 | github.com/pkg/errors v0.9.1 // indirect 84 | github.com/pkoukk/tiktoken-go v0.1.6 // indirect 85 | github.com/pmezard/go-difflib v1.0.0 // indirect 86 | github.com/prometheus/client_model v0.6.1 // indirect 87 | github.com/prometheus/common v0.54.0 // indirect 88 | github.com/prometheus/procfs v0.12.0 // indirect 89 | github.com/spf13/pflag v1.0.5 // indirect 90 | github.com/stretchr/objx v0.5.2 // indirect 91 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 92 | go.opentelemetry.io/otel v1.29.0 // indirect 93 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.29.0 // indirect 94 | go.opentelemetry.io/otel/metric v1.29.0 // indirect 95 | go.opentelemetry.io/otel/sdk v1.29.0 // indirect 96 | go.opentelemetry.io/otel/trace v1.29.0 // indirect 97 | golang.org/x/net v0.28.0 // indirect 98 | golang.org/x/sync v0.8.0 // indirect 99 | golang.org/x/sys v0.25.0 // indirect 100 | golang.org/x/term v0.23.0 // indirect 101 | golang.org/x/text v0.17.0 // indirect 102 | golang.org/x/time v0.5.0 // indirect 103 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 104 | google.golang.org/protobuf v1.34.2 // indirect 105 | gopkg.in/inf.v0 v0.9.1 // indirect 106 | gopkg.in/yaml.v2 v2.4.0 // indirect 107 | gopkg.in/yaml.v3 v3.0.1 // indirect 108 | gotest.tools/v3 v3.5.1 // indirect 109 | k8s.io/klog/v2 v2.100.1 // indirect 110 | k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect 111 | k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect 112 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 113 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 114 | ) 115 | 116 | replace sigs.k8s.io/prow => github.com/Carlji/prow v1.0.0 117 | -------------------------------------------------------------------------------- /internal/cache/issuereference.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | type IssueCache struct { 9 | mu sync.RWMutex 10 | data map[string]string 11 | ttl time.Duration 12 | lastSetTime map[string]time.Time 13 | } 14 | 15 | func NewIssueReferencesCache(ttl time.Duration) *IssueCache { 16 | return &IssueCache{ 17 | data: make(map[string]string), 18 | ttl: ttl, 19 | lastSetTime: make(map[string]time.Time), 20 | } 21 | } 22 | 23 | func (c *IssueCache) Get(key string) (string, bool) { 24 | c.mu.RLock() 25 | defer c.mu.RUnlock() 26 | issue, isFound := c.data[key] 27 | return issue, isFound 28 | } 29 | 30 | func (c *IssueCache) Set(key string, issueContent string) { 31 | c.mu.Lock() 32 | defer c.mu.Unlock() 33 | c.data[key] = issueContent 34 | c.lastSetTime[key] = time.Now() 35 | } 36 | 37 | func (c *IssueCache) IsExpired(key string) bool { 38 | return time.Since(c.lastSetTime[key]) > c.ttl 39 | } 40 | -------------------------------------------------------------------------------- /internal/cache/token.go: -------------------------------------------------------------------------------- 1 | package cache 2 | 3 | import ( 4 | "sync" 5 | "time" 6 | ) 7 | 8 | // DefaultTokenCache uses to cache provider impersonation tokens. 9 | var DefaultTokenCache = NewTokenCache() 10 | 11 | // TokenCache implements the cache for provider impersonation tokens. 12 | type TokenCache struct { 13 | sync.RWMutex 14 | tokens map[string]tokenWithExp 15 | } 16 | 17 | type tokenWithExp struct { 18 | token string 19 | exp time.Time 20 | } 21 | 22 | // NewTokenCache creates a new token cache. 23 | func NewTokenCache() *TokenCache { 24 | return &TokenCache{ 25 | tokens: make(map[string]tokenWithExp), 26 | } 27 | } 28 | 29 | func (c *TokenCache) GetToken(key string) (string, bool) { 30 | c.RLock() 31 | t, exists := c.tokens[key] 32 | c.RUnlock() 33 | 34 | if exists && t.exp.After(time.Now()) { 35 | return t.token, true 36 | } 37 | 38 | return "", false 39 | } 40 | 41 | func (c *TokenCache) SetToken(key string, token string, exp time.Time) { 42 | c.Lock() 43 | defer c.Unlock() 44 | c.tokens[key] = tokenWithExp{ 45 | token: token, 46 | exp: exp, 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /internal/lint/agent_test.go: -------------------------------------------------------------------------------- 1 | package lint 2 | 3 | import ( 4 | "context" 5 | "reflect" 6 | "regexp" 7 | "testing" 8 | 9 | "github.com/qiniu/reviewbot/config" 10 | ) 11 | 12 | func TestApplyTypedMessageByIssueReferences(t *testing.T) { 13 | // Mock issue reference pattern 14 | pattern := regexp.MustCompile(`#(\d+)`) 15 | pattern2 := regexp.MustCompile(`#ABC`) 16 | 17 | testCases := []struct { 18 | name string 19 | reportFormat config.ReportType 20 | lintResults map[string][]LinterOutput 21 | issueRefs []config.CompiledIssueReference 22 | expectedOutput map[string][]LinterOutput 23 | }{ 24 | { 25 | name: "GithubCheckRuns format", 26 | reportFormat: config.GitHubCheckRuns, 27 | lintResults: map[string][]LinterOutput{ 28 | "file1.go": { 29 | { 30 | Message: "variable naming issue #123", 31 | Line: 10, 32 | }, 33 | }, 34 | }, 35 | issueRefs: []config.CompiledIssueReference{ 36 | { 37 | Pattern: pattern, 38 | URL: "https://github.com/qiniu/reviewbot/issues/wrongissue", 39 | IssueNumber: 123, 40 | }, 41 | }, 42 | expectedOutput: map[string][]LinterOutput{ 43 | "file1.go": { 44 | { 45 | Message: "variable naming issue #123", 46 | TypedMessage: "variable naming issue #123\nmore info: https://github.com/qiniu/reviewbot/issues/wrongissue", 47 | Line: 10, 48 | }, 49 | }, 50 | }, 51 | }, 52 | { 53 | name: "GithubPRReview format", 54 | reportFormat: config.GitHubPRReview, 55 | lintResults: map[string][]LinterOutput{ 56 | "file2.go": { 57 | { 58 | Message: "function complexity issue #456", 59 | Line: 20, 60 | }, 61 | }, 62 | }, 63 | issueRefs: []config.CompiledIssueReference{ 64 | { 65 | Pattern: pattern, 66 | URL: "https://github.com/qiniu/reviewbot/issues/wrongissue", 67 | IssueNumber: 0, 68 | }, 69 | }, 70 | expectedOutput: map[string][]LinterOutput{ 71 | "file2.go": { 72 | { 73 | Message: "function complexity issue #456", 74 | TypedMessage: "[function complexity issue #456](https://github.com/qiniu/reviewbot/issues/wrongissue)", 75 | Line: 20, 76 | }, 77 | }, 78 | }, 79 | }, 80 | { 81 | name: "GithubMixType format", 82 | reportFormat: config.GitHubMixType, 83 | lintResults: map[string][]LinterOutput{ 84 | "file3.go": { 85 | { 86 | Message: "code style issue #789", 87 | Line: 30, 88 | }, 89 | }, 90 | }, 91 | issueRefs: []config.CompiledIssueReference{ 92 | { 93 | Pattern: pattern, 94 | URL: "https://github.com/qiniu/reviewbot/issues/wrongissue", 95 | IssueNumber: 0, 96 | }, 97 | }, 98 | expectedOutput: map[string][]LinterOutput{ 99 | "file3.go": { 100 | { 101 | Message: "code style issue #789", 102 | TypedMessage: "[code style issue #789](https://github.com/qiniu/reviewbot/issues/wrongissue)", 103 | Line: 30, 104 | }, 105 | }, 106 | }, 107 | }, 108 | { 109 | name: "No matching issue reference", 110 | reportFormat: config.GitHubCheckRuns, 111 | lintResults: map[string][]LinterOutput{ 112 | "file4.go": { 113 | { 114 | Message: "regular lint message without issue reference", 115 | Line: 40, 116 | }, 117 | }, 118 | }, 119 | issueRefs: []config.CompiledIssueReference{ 120 | { 121 | Pattern: pattern, 122 | URL: "https://github.com/qiniu/reviewbot/issues/999", 123 | IssueNumber: 0, 124 | }, 125 | }, 126 | expectedOutput: map[string][]LinterOutput{ 127 | "file4.go": { 128 | { 129 | Message: "regular lint message without issue reference", 130 | Line: 40, 131 | }, 132 | }, 133 | }, 134 | }, 135 | { 136 | name: "Multiple files and issues", 137 | reportFormat: config.GitHubMixType, 138 | lintResults: map[string][]LinterOutput{ 139 | "file5.go": { 140 | { 141 | Message: "issue #111", 142 | Line: 50, 143 | }, 144 | { 145 | Message: "issue #ABC", 146 | Line: 51, 147 | }, 148 | }, 149 | "file6.go": { 150 | { 151 | Message: "no issue reference", 152 | Line: 60, 153 | }, 154 | }, 155 | }, 156 | issueRefs: []config.CompiledIssueReference{ 157 | { 158 | Pattern: pattern, 159 | URL: "https://github.com/qiniu/reviewbot/issues/wrongissue", 160 | IssueNumber: 0, 161 | }, 162 | { 163 | Pattern: pattern2, 164 | URL: "https://github.com/qiniu/reviewbot/issues/wrongissue", 165 | IssueNumber: 0, 166 | }, 167 | }, 168 | expectedOutput: map[string][]LinterOutput{ 169 | "file5.go": { 170 | { 171 | Message: "issue #111", 172 | TypedMessage: "[issue #111](https://github.com/qiniu/reviewbot/issues/wrongissue)", 173 | Line: 50, 174 | }, 175 | { 176 | Message: "issue #ABC", 177 | TypedMessage: "[issue #ABC](https://github.com/qiniu/reviewbot/issues/wrongissue)", 178 | Line: 51, 179 | }, 180 | }, 181 | "file6.go": { 182 | { 183 | Message: "no issue reference", 184 | Line: 60, 185 | }, 186 | }, 187 | }, 188 | }, 189 | } 190 | 191 | for _, tc := range testCases { 192 | t.Run(tc.name, func(t *testing.T) { 193 | // Create agent with test configuration 194 | agent := &Agent{ 195 | LinterConfig: config.Linter{ReportType: tc.reportFormat}, 196 | IssueReferences: tc.issueRefs, 197 | } 198 | 199 | // Create context 200 | ctx := context.Background() 201 | 202 | // Call the method 203 | result := agent.EnrichWithIssueReferences(ctx, tc.lintResults) 204 | 205 | // Compare results 206 | if !reflect.DeepEqual(result, tc.expectedOutput) { 207 | t.Errorf("Expected %+v, got %+v", tc.expectedOutput, result) 208 | } 209 | }) 210 | } 211 | } 212 | -------------------------------------------------------------------------------- /internal/lint/filters.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lint 18 | 19 | import ( 20 | "path/filepath" 21 | "regexp" 22 | "strings" 23 | 24 | "github.com/qiniu/x/log" 25 | "github.com/qiniu/x/xlog" 26 | ) 27 | 28 | // Filters filters the lint errors. 29 | func Filters(log *xlog.Logger, a Agent, linterResults map[string][]LinterOutput) (map[string][]LinterOutput, error) { 30 | linterResults = cleanLintResults(a.RepoDir, linterResults) 31 | results := filterByPRChanged(a.Provider, linterResults) 32 | results, err := filterByAutoGenerated(a, results) 33 | if err != nil { 34 | return nil, err 35 | } 36 | 37 | results = filterBySA5008(results) 38 | return results, nil 39 | } 40 | 41 | // LinterRelated checks if the linter is related to the PR. 42 | // Each linter has a list of languages that it supports and the file extensions are used to determine 43 | // whether the linter is related to the PR. 44 | func LinterRelated(linterName string, a Agent) bool { 45 | exts := make(map[string]bool) 46 | for _, file := range a.Provider.GetFiles(nil) { 47 | ext := filepath.Ext(file) 48 | if ext == "" { 49 | continue 50 | } 51 | exts[ext] = true 52 | } 53 | return languageRelated(linterName, exts) 54 | } 55 | 56 | // cleanLintResults cleans the file path in lint results. 57 | // It removes the workdir prefix from the file path. 58 | func cleanLintResults(workdir string, lintResults map[string][]LinterOutput) map[string][]LinterOutput { 59 | cleanedResults := make(map[string][]LinterOutput) 60 | for file, linters := range lintResults { 61 | cleanedFile := strings.TrimPrefix(file, workdir+"/") 62 | cleanedResults[cleanedFile] = linters 63 | } 64 | return cleanedResults 65 | } 66 | 67 | // filterByPRChanged filters out the lint errors that are not related to the PR. 68 | func filterByPRChanged(provider Provider, outputs map[string][]LinterOutput) map[string][]LinterOutput { 69 | result := make(map[string][]LinterOutput) 70 | for file, lintFileErrs := range outputs { 71 | for _, lintErr := range lintFileErrs { 72 | if provider.IsRelated(file, lintErr.Line, lintErr.StartLine) { 73 | result[file] = append(result[file], lintErr) 74 | } 75 | } 76 | } 77 | return result 78 | } 79 | 80 | // filterByAutoGenerated filters out the auto-generated files. 81 | func filterByAutoGenerated(a Agent, linterResults map[string][]LinterOutput) (map[string][]LinterOutput, error) { 82 | var filesToIgnore []string 83 | for file := range linterResults { 84 | absPath := filepath.Join(a.LinterConfig.WorkDir, file) 85 | if isGenerated, err := isGeneratedFile(absPath); err != nil { 86 | log.Errorf("failed to check if file is generated: %v", err) 87 | continue 88 | } else { 89 | if isGenerated { 90 | log.Infof("ignore generated file: %s", file) 91 | filesToIgnore = append(filesToIgnore, file) 92 | } 93 | } 94 | } 95 | 96 | for _, file := range filesToIgnore { 97 | delete(linterResults, file) 98 | } 99 | 100 | return linterResults, nil 101 | } 102 | 103 | // special handling for staticcheck SA5008 (unknown JSON option error) 104 | // Background: 105 | // - https://github.com/qiniu/reviewbot/issues/24 106 | var tagRex = regexp.MustCompile(`unknown JSON option "(.*)" \(SA5008\)`) 107 | 108 | func filterBySA5008(results map[string][]LinterOutput) map[string][]LinterOutput { 109 | finalResults := make(map[string][]LinterOutput) 110 | 111 | for file, linterResults := range results { 112 | var lintersCopy []LinterOutput 113 | for _, linter := range linterResults { 114 | matches := tagRex.FindStringSubmatch(linter.Message) 115 | if len(matches) == 2 && isGoZeroCustomTag(matches[1]) { 116 | log.Warnf("ignore this error: %v", linter.Message) 117 | continue 118 | } 119 | lintersCopy = append(lintersCopy, linter) 120 | } 121 | if len(lintersCopy) > 0 { 122 | finalResults[file] = lintersCopy 123 | } 124 | } 125 | 126 | return finalResults 127 | } 128 | 129 | // isGoZeroCustomTag checks if the tag is a go-zero custom tag. 130 | // refer: https://go-zero.dev/en/docs/tutorials/go-zero/configuration/overview#tag-checksum-rule 131 | const ( 132 | goZeroDefaultOption = "default" 133 | goZeroEnvOption = "env" 134 | goZeroInheritOption = "inherit" 135 | goZeroOptionalOption = "optional" 136 | goZeroOptionsOption = "options" 137 | goZeroRangeOption = "range" 138 | ) 139 | 140 | // FIXME(CarlJi): this function is a temporary solution for go-zero custom tag, see [#24](https://github.com/qiniu/reviewbot/issues/24) 141 | // expect to remove this function after staticcheck supports go-zero custom tag. 142 | func isGoZeroCustomTag(jsonOption string) bool { 143 | var found bool 144 | switch { 145 | case strings.HasPrefix(jsonOption, goZeroDefaultOption): 146 | found = true 147 | case strings.HasPrefix(jsonOption, goZeroEnvOption): 148 | found = true 149 | case strings.HasPrefix(jsonOption, goZeroInheritOption): 150 | found = true 151 | case strings.HasPrefix(jsonOption, goZeroOptionalOption): 152 | found = true 153 | case strings.HasPrefix(jsonOption, goZeroOptionsOption): 154 | found = true 155 | case strings.HasPrefix(jsonOption, goZeroRangeOption): 156 | found = true 157 | } 158 | 159 | if found { 160 | log.Warnf("this tag %v seems belongs to go-zero, ignore it temporary, see [#24](https://github.com/qiniu/reviewbot/issues/24) for more information", jsonOption) 161 | } 162 | 163 | return found 164 | } 165 | 166 | func languageRelated(linterName string, exts map[string]bool) bool { 167 | langs := Languages(linterName) 168 | for _, language := range langs { 169 | if language == "*" || exts[language] { 170 | return true 171 | } 172 | } 173 | return false 174 | } 175 | -------------------------------------------------------------------------------- /internal/lint/hunk.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lint 18 | 19 | import ( 20 | "fmt" 21 | "regexp" 22 | "strconv" 23 | ) 24 | 25 | type HunkChecker interface { 26 | InHunk(file string, line int) bool 27 | } 28 | 29 | type FileHunkChecker struct { 30 | Hunks map[string][]Hunk 31 | } 32 | 33 | type Hunk struct { 34 | StartLine int 35 | EndLine int 36 | } 37 | 38 | // NewFileHunkChecker creates a new FileHunkChecker with given hunks map. 39 | func NewFileHunkChecker(hunks map[string][]Hunk) *FileHunkChecker { 40 | return &FileHunkChecker{ 41 | Hunks: hunks, 42 | } 43 | } 44 | 45 | func (c *FileHunkChecker) InHunk(file string, line, startLine int) bool { 46 | if hunks, ok := c.Hunks[file]; ok { 47 | for _, hunk := range hunks { 48 | if startLine != 0 { 49 | if startLine >= hunk.StartLine && line <= hunk.EndLine { 50 | return true 51 | } 52 | } else if line >= hunk.StartLine && line <= hunk.EndLine { 53 | return true 54 | } 55 | } 56 | } 57 | return false 58 | } 59 | 60 | // ParsePatch parses a unified diff patch string and returns hunks. 61 | func ParsePatch(patch string) ([]Hunk, error) { 62 | hunks := make([]Hunk, 0) 63 | 64 | groups := patchRegex.FindAllStringSubmatch(patch, -1) 65 | for _, group := range groups { 66 | if len(group) != 5 { 67 | return nil, fmt.Errorf("invalid patch: %s", patch) 68 | } 69 | hunkStartLine, err := strconv.Atoi(group[3]) 70 | if err != nil { 71 | return nil, fmt.Errorf("invalid patch: %s, hunkStartLine: %s", patch, group[3]) 72 | } 73 | 74 | hunkLength, err := strconv.Atoi(group[4]) 75 | if err != nil { 76 | return nil, fmt.Errorf("invalid patch: %s, hunkLength: %s", patch, group[4]) 77 | } 78 | 79 | hunks = append(hunks, Hunk{ 80 | StartLine: hunkStartLine, 81 | EndLine: hunkStartLine + hunkLength - 1, 82 | }) 83 | } 84 | 85 | return hunks, nil 86 | } 87 | 88 | var patchRegex = regexp.MustCompile(`@@ \-(\d+),(\d+) \+(\d+),(\d+) @@`) 89 | -------------------------------------------------------------------------------- /internal/lint/hunk_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package lint 18 | 19 | import ( 20 | "fmt" 21 | "testing" 22 | ) 23 | 24 | func TestParsePatch(t *testing.T) { 25 | patch := "@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test" 26 | fmt.Println(ParsePatch(patch)) 27 | } 28 | 29 | func TestInHunk(t *testing.T) { 30 | c := FileHunkChecker{ 31 | Hunks: map[string][]Hunk{ 32 | "testfilename": { 33 | { 34 | StartLine: 100, 35 | EndLine: 105, 36 | }, 37 | { 38 | StartLine: 200, 39 | EndLine: 205, 40 | }, 41 | }, 42 | }, 43 | } 44 | tsc := []struct { 45 | conditionMsg string 46 | filename string 47 | startLine int 48 | line int 49 | expected bool 50 | }{ 51 | { 52 | "startline == 0 && line < Hunk.StartLine1 < Hunk.EndLine1 < Hunk.StartLine2 4 | cppcheck [OPTIONS] [files or paths]
5 | 6 | in this repo:
7 | Recursively check the current folder, format the error messages as file name:line number:column number: warning message and don't print progress:
8 | cppcheck --quiet --template='{file}:{line}:{column}: {message}' .
9 | 10 | For more information:
11 | https://files.cppchecksolutions.com/manual.pdf
12 | -------------------------------------------------------------------------------- /internal/linters/c/cppcheck/cppcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cppcheck 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/qiniu/reviewbot/internal/lint" 23 | "github.com/qiniu/reviewbot/internal/util" 24 | "github.com/qiniu/x/xlog" 25 | ) 26 | 27 | // refer to https://cppcheck.sourceforge.io/ 28 | var linterName = "cppcheck" 29 | 30 | func init() { 31 | lint.RegisterPullRequestHandler(linterName, cppcheckHandler) 32 | // see https://stackoverflow.com/a/3223792/5057547 33 | lint.RegisterLinterLanguages(linterName, []string{".c", ".cpp", ".h", ".hpp", ".cc", ".cxx", ".hxx", ".c++"}) 34 | } 35 | 36 | func cppcheckHandler(ctx context.Context, a lint.Agent) error { 37 | log := util.FromContext(ctx) 38 | if lint.IsEmpty(a.LinterConfig.Args...) { 39 | // The check-level parameter has been supported since version 2.11, with the default normal mode. 40 | // In exhaustive mode, Cppcheck performs additional inspection rules and more complex analysis, potentially uncovering issues that may not be detected in the default normal mode. 41 | // However, this mode comes at the cost of longer execution times, making it suitable for scenarios where higher code quality is desired and longer waiting times are acceptable. 42 | // From version 2.14, the linter will prompt: "Limiting analysis of branches. Use --check-level=exhaustive to analyze all branches." 43 | a.LinterConfig.Args = append([]string{}, "--quiet", "--check-level=exhaustive", "--template='{file}:{line}:{column}: {message}'", ".") 44 | } 45 | 46 | return lint.GeneralHandler(ctx, log, a, lint.ExecRun, parser) 47 | } 48 | 49 | func parser(log *xlog.Logger, input []byte) (map[string][]lint.LinterOutput, []string) { 50 | lineParser := func(line string) (*lint.LinterOutput, error) { 51 | if len(line) <= 2 { 52 | return nil, nil 53 | } 54 | 55 | // remove the first and last character of the line, 56 | // which are the single quotes 57 | line = line[1 : len(line)-1] 58 | return lint.GeneralLineParser(line) 59 | } 60 | return lint.Parse(log, input, lineParser) 61 | } 62 | -------------------------------------------------------------------------------- /internal/linters/c/cppcheck/cppcheck_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cppcheck 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | "github.com/qiniu/x/xlog" 25 | ) 26 | 27 | func TestParser(t *testing.T) { 28 | tc := []struct { 29 | input string 30 | expected map[string][]lint.LinterOutput 31 | unexpected []string 32 | }{ 33 | { 34 | input: "'cppcheck_test.c:6:7: Array 'a[10]' accessed at index 10, which is out of bounds.'", 35 | expected: map[string][]lint.LinterOutput{ 36 | "cppcheck_test.c": { 37 | { 38 | File: "cppcheck_test.c", 39 | Line: 6, 40 | Column: 7, 41 | Message: "Array 'a[10]' accessed at index 10, which is out of bounds.", 42 | }, 43 | }, 44 | }, 45 | unexpected: nil, 46 | }, 47 | { 48 | input: "''", 49 | expected: map[string][]lint.LinterOutput{}, 50 | unexpected: nil, 51 | }, 52 | } 53 | 54 | for _, c := range tc { 55 | output, unexpected := parser(xlog.New("cppcheck"), []byte(c.input)) 56 | if !reflect.DeepEqual(output, c.expected) { 57 | t.Errorf("expected: %v, got: %v", c.expected, output) 58 | } 59 | if !reflect.DeepEqual(unexpected, c.unexpected) { 60 | t.Errorf("expected: %v, got: %v", c.unexpected, unexpected) 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/linters/doc/note-check/README.md: -------------------------------------------------------------------------------- 1 | see [note-check](../../../../docs/website/docs/components/doc/note-check.md) 2 | -------------------------------------------------------------------------------- /internal/linters/doc/note-check/doc.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // The main purpose of this plugin is to encourage us to follow standard practices when writing notes as: 18 | // 19 | // "MARKER(uid): note body" 20 | // 21 | // which is more readable and maintainable since it is easy to search and filter notes by MARKER and uid. 22 | // more details can be found at https://pkg.go.dev/go/doc#Note 23 | package notecheck 24 | -------------------------------------------------------------------------------- /internal/linters/doc/note-check/note.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package notecheck 18 | 19 | import ( 20 | "context" 21 | "go/parser" 22 | "go/token" 23 | "path/filepath" 24 | "regexp" 25 | "strings" 26 | 27 | "github.com/qiniu/reviewbot/internal/lint" 28 | "github.com/qiniu/x/log" 29 | ) 30 | 31 | // refer to https://pkg.go.dev/go/doc#Note 32 | const linterName = "note-check" 33 | 34 | func init() { 35 | lint.RegisterPullRequestHandler(linterName, noteCheckHandler) 36 | 37 | // TODO(CarlJi): can we check other languages? 38 | lint.RegisterLinterLanguages(linterName, []string{".go"}) 39 | } 40 | 41 | // noteCheckHandler is the handler of the linter 42 | // Check the notes in the code to see if they comply with the standard rules from 43 | // https://pkg.go.dev/go/doc#Note 44 | func noteCheckHandler(ctx context.Context, a lint.Agent) error { 45 | outputs := make(map[string][]lint.LinterOutput) 46 | 47 | for _, file := range a.Provider.GetFiles(nil) { 48 | fileName := file 49 | // Only check go files 50 | if filepath.Ext(fileName) != ".go" { 51 | continue 52 | } 53 | 54 | output, err := noteCheckFile(a.LinterConfig.WorkDir, fileName) 55 | if err != nil { 56 | return err 57 | } 58 | 59 | if len(output) > 0 { 60 | for k, v := range output { 61 | if vv, ok := outputs[k]; ok { 62 | outputs[k] = append(vv, v...) 63 | } else { 64 | outputs[k] = v 65 | } 66 | } 67 | } 68 | } 69 | 70 | return lint.Report(ctx, a, outputs) 71 | } 72 | 73 | const NoteSuggestion = "A Note is recommended to use \"MARKER(uid): note body\" format." 74 | 75 | func noteCheckFile(workdir, filename string) (map[string][]lint.LinterOutput, error) { 76 | path := filepath.Join(workdir, filename) 77 | fset := token.NewFileSet() 78 | file, err := parser.ParseFile(fset, path, nil, parser.ParseComments) 79 | if err != nil { 80 | return nil, err 81 | } 82 | 83 | output := make(map[string][]lint.LinterOutput) 84 | for _, cmts := range file.Comments { 85 | for _, cmt := range cmts.List { 86 | // comments with "/*" may have multiple lines 87 | lines := strings.Split(cmt.Text, "\n") 88 | for i, line := range lines { 89 | if !hasNonstandardNote(line) { 90 | continue 91 | } 92 | 93 | log.Debugf("non-standard note: %s, pos: %v", line, fset.Position(cmt.Pos())) 94 | 95 | v, ok := output[filename] 96 | if !ok { 97 | output[filename] = []lint.LinterOutput{ 98 | { 99 | File: filename, 100 | Line: fset.Position(cmt.Pos()).Line + i, 101 | Column: fset.Position(cmt.Pos()).Column, 102 | Message: NoteSuggestion, 103 | }, 104 | } 105 | } else { 106 | v = append(v, lint.LinterOutput{ 107 | File: filename, 108 | Line: fset.Position(cmt.Pos()).Line + i, 109 | Column: fset.Position(cmt.Pos()).Column, 110 | Message: NoteSuggestion, 111 | }) 112 | output[filename] = v 113 | } 114 | } 115 | } 116 | } 117 | return output, nil 118 | } 119 | 120 | var ( 121 | standardNoteMarker = `([A-Z][A-Z]+)\(([^)]+)\):.?` // MARKER(uid), MARKER at least 2 chars, uid at least 1 char 122 | standardNoteMarkerRx = regexp.MustCompile(`^[ \t]*` + standardNoteMarker) // MARKER(uid) at text start 123 | standardNoteCommentRx = regexp.MustCompile(`^/[/*][ \t]*` + standardNoteMarker) // MARKER(uid) at comment start 124 | 125 | nonstandardNoteMarker = `([A-Z][A-Z]+):.?` // General non-standard MARKER, MARKER at least 2 chars, plus colon 126 | nonstandardNoteMarkerRx = regexp.MustCompile(`^[ \t]*` + nonstandardNoteMarker) // MARKER: at text start 127 | nonstandardNoteCommentRx = regexp.MustCompile(`^/[/*][ \t]*` + nonstandardNoteMarker) // MARKER: at comment start 128 | ) 129 | 130 | func hasNonstandardNote(comment string) bool { 131 | if comment == "" { 132 | return false 133 | } 134 | if nonstandardNoteCommentRx.MatchString(comment) && !standardNoteCommentRx.MatchString(comment) { 135 | return true 136 | } 137 | 138 | if nonstandardNoteMarkerRx.MatchString(comment) && !standardNoteMarkerRx.MatchString(comment) { 139 | return true 140 | } 141 | return false 142 | } 143 | -------------------------------------------------------------------------------- /internal/linters/doc/note-check/note_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package notecheck 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | ) 25 | 26 | func TestNoteCheckFile(t *testing.T) { 27 | tcs := []struct { 28 | name string 29 | workdir string 30 | filename string 31 | expected map[string][]lint.LinterOutput 32 | error error 33 | }{ 34 | { 35 | name: "kinds_of_notes", 36 | workdir: "testdata", 37 | filename: "note.go", 38 | expected: map[string][]lint.LinterOutput{ 39 | "note.go": { 40 | { 41 | File: "note.go", 42 | Line: 6, 43 | Column: 1, 44 | Message: NoteSuggestion, 45 | }, 46 | { 47 | File: "note.go", 48 | Line: 10, 49 | Column: 1, 50 | Message: NoteSuggestion, 51 | }, 52 | { 53 | File: "note.go", 54 | Line: 39, 55 | Column: 1, 56 | Message: NoteSuggestion, 57 | }, 58 | { 59 | File: "note.go", 60 | Line: 40, 61 | Column: 1, 62 | Message: NoteSuggestion, 63 | }, 64 | }, 65 | }, 66 | error: nil, 67 | }, 68 | } 69 | 70 | for _, tc := range tcs { 71 | t.Run(tc.name, func(t *testing.T) { 72 | actual, err := noteCheckFile(tc.workdir, tc.filename) 73 | if err != nil { 74 | t.Errorf("unexpected error: %v", err) 75 | } 76 | if !reflect.DeepEqual(actual, tc.expected) { 77 | t.Errorf("\nexpected: %v,\ngot: %v", tc.expected, actual) 78 | } 79 | }) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /internal/linters/doc/note-check/testdata/note.go: -------------------------------------------------------------------------------- 1 | // comment 0 2 | package a 3 | 4 | //BUG(uid): bug0 5 | 6 | //TODO: todo0 7 | 8 | // A note with some spaces after it, should be ignored (watch out for 9 | // emacs modes that remove trailing whitespace). 10 | //NOTE: 11 | 12 | // SECBUG(uid): sec hole 0 13 | // need to fix asap 14 | 15 | // Multiple notes may be in the same comment group and should be 16 | // recognized individually. Notes may start in the middle of a 17 | // comment group as long as they start at the beginning of an 18 | // individual comment. 19 | // 20 | // NOTE(foo): 1 of 4 - this is the first line of note 1 21 | // - note 1 continues on this 2nd line 22 | // - note 1 continues on this 3rd line 23 | // NOTE(foo): 2 of 4 24 | // NOTE(bar): 3 of 4 25 | /* NOTE(bar): 4 of 4 */ 26 | // - this is the last line of note 4 27 | // 28 | // 29 | 30 | // NOTE(bam): This note which contains a (parenthesized) subphrase 31 | // must appear in its entirety. 32 | 33 | // NOTE(xxx) The ':' after the marker and uid is optional. 34 | 35 | // NOTE(): do suggestion 36 | // NOTE() NO uid - should not show up. 37 | 38 | /* 39 | TODO: todo 40 | BUG: todo 41 | */ 42 | 43 | // ADD SUB MUL QUO REM + - * / %, pos: /var/tmp/gitrepo4087111025/ssa/ 44 | -------------------------------------------------------------------------------- /internal/linters/git-flow/commit/README.md: -------------------------------------------------------------------------------- 1 | see [commit-check](../../../../docs/website/docs/components/git/commit-check.md) 2 | -------------------------------------------------------------------------------- /internal/linters/git-flow/commit/commit_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package commit 18 | 19 | import ( 20 | "context" 21 | "strings" 22 | "testing" 23 | 24 | "github.com/qiniu/reviewbot/internal/lint" 25 | ) 26 | 27 | func TestRebaseCheckRule(t *testing.T) { 28 | tcs := []struct { 29 | title string 30 | commits []lint.Commit 31 | expected string 32 | }{ 33 | { 34 | title: "filter merge commits", 35 | commits: []lint.Commit{ 36 | { 37 | Message: "feat: add feature 1", 38 | }, 39 | { 40 | Message: "Merge a into b", 41 | }, 42 | { 43 | Message: "fix: fix bug 2", 44 | }, 45 | { 46 | Message: "Merge xxx into xxx", 47 | }, 48 | }, 49 | expected: "git merge", 50 | }, 51 | { 52 | title: "filter duplicate commits", 53 | commits: []lint.Commit{ 54 | { 55 | Message: "feat: add feature 1", 56 | }, 57 | { 58 | Message: "feat: add feature 1", 59 | }, 60 | { 61 | Message: "fix: fix bug 2", 62 | }, 63 | }, 64 | expected: "duplicated", 65 | }, 66 | { 67 | title: "filter duplicate and merge commits", 68 | commits: []lint.Commit{ 69 | { 70 | Message: "feat: add feature 1", 71 | }, 72 | { 73 | Message: "feat: add feature 1", 74 | }, 75 | { 76 | Message: "Merge xxx into xxx", 77 | }, 78 | }, 79 | expected: "feat: add feature 1", 80 | }, 81 | { 82 | title: "filter duplicate and merge commits", 83 | commits: []lint.Commit{ 84 | { 85 | Message: "feat: add feature 1", 86 | }, 87 | { 88 | Message: "feat: add feature 2", 89 | }, 90 | }, 91 | expected: "", 92 | }, 93 | } 94 | 95 | for _, tc := range tcs { 96 | t.Run(tc.title, func(t *testing.T) { 97 | comments, err := rebaseCheck(context.Background(), tc.commits) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | if tc.expected == "" && comments != "" { 103 | t.Fatalf("expected %s, got %s", tc.expected, comments) 104 | } 105 | 106 | if !strings.Contains(comments, tc.expected) { 107 | t.Fatalf("expected %s, got %s", tc.expected, comments) 108 | } 109 | }) 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /internal/linters/go/gofmt/README.md: -------------------------------------------------------------------------------- 1 | see [gofmt](../../../../docs/website/docs/components/go/gofmt.md) 2 | -------------------------------------------------------------------------------- /internal/linters/go/gofmt/gofmt_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package gofmt 18 | 19 | import ( 20 | "os" 21 | "testing" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | ) 25 | 26 | func TestGofmtOutput(t *testing.T) { 27 | content, err := os.ReadFile("./testdata/gofmt_test.txt") 28 | if err != nil { 29 | t.Errorf("open file failed ,the err is : %v", err) 30 | return 31 | } 32 | tc := []struct { 33 | input []byte 34 | expected []lint.LinterOutput 35 | }{ 36 | { 37 | content, 38 | []lint.LinterOutput{ 39 | { 40 | File: "testfile/staticcheck.go", 41 | Line: 7, 42 | Column: 1, 43 | Message: "", 44 | }, 45 | { 46 | File: "testfile/test.go", 47 | Line: 9, 48 | Column: 4, 49 | Message: "", 50 | StartLine: 6, 51 | }, 52 | }, 53 | }, 54 | } 55 | for _, c := range tc { 56 | outputMap, err := formatGofmtOutput([]byte(c.input)) 57 | for _, outputs := range outputMap { 58 | for i, output := range outputs { 59 | 60 | if err != nil { 61 | t.Errorf("unexpected error: %v", err) 62 | } 63 | 64 | if output.StartLine != 0 { 65 | if output.File != c.expected[1].File || output.StartLine != c.expected[1].StartLine || output.Line != c.expected[1].Line || output.Column != c.expected[1].Column { 66 | t.Errorf("expected: %v, got: %v", c.expected[i], output) 67 | } 68 | } else { 69 | if output.File != c.expected[0].File || output.Line != c.expected[0].Line || output.Column != c.expected[0].Column { 70 | t.Errorf("expected: %v, got: %v", c.expected[0], output) 71 | } 72 | } 73 | 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /internal/linters/go/gofmt/testdata/gofmt_test.txt: -------------------------------------------------------------------------------- 1 | diff testfile/staticcheck.go.orig testfile/staticcheck.go 2 | --- testfile/staticcheck.go.orig 3 | +++ testfile/staticcheck.go 4 | @@ -4,5 +4,5 @@ 5 | 6 | func testunnuser() { 7 | fmt.Println("unused") 8 | - // wrong format 9 | + // wrong format 10 | } 11 | diff testfile/test.go.orig testfile/test.go 12 | --- testfile/test.go.orig 13 | +++ testfile/test.go 14 | @@ -3,10 +3,10 @@ 15 | import "fmt" 16 | 17 | func test2() { 18 | - //testerr 33333 19 | - 20 | - // 222222222 21 | - //testerr 33333 22 | + //testerr 33333 23 | + 24 | + // 222222222 25 | + //testerr 33333 26 | 27 | // rrrrrrrrr 28 | } 29 | -------------------------------------------------------------------------------- /internal/linters/go/golangci_lint/README.md: -------------------------------------------------------------------------------- 1 | see [golangci_lint](../../../../docs/website/docs/components/go/golangci_lint.md) 2 | -------------------------------------------------------------------------------- /internal/linters/go/golangci_lint/testdata/.golangci.yml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/qiniu/reviewbot/3713a50d7c4c7e4a75972bae7c4f7e792d555bf9/internal/linters/go/golangci_lint/testdata/.golangci.yml -------------------------------------------------------------------------------- /internal/linters/go/gomodcheck/README.md: -------------------------------------------------------------------------------- 1 | see [gomodcheck](../../../../docs/website/docs/components/go/gomodcheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/go/gomodcheck/gomodcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package gomodcheck 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/qiniu/reviewbot/internal/lint" 26 | "github.com/qiniu/reviewbot/internal/util" 27 | "github.com/qiniu/x/xlog" 28 | "golang.org/x/mod/modfile" 29 | ) 30 | 31 | var lintName = "gomodcheck" 32 | 33 | func init() { 34 | lint.RegisterPullRequestHandler(lintName, goModCheckHandler) 35 | lint.RegisterLinterLanguages(lintName, []string{".go", ".mod"}) 36 | } 37 | 38 | func goModCheckHandler(ctx context.Context, a lint.Agent) error { 39 | log := util.FromContext(ctx) 40 | parsedOutput, err := goModCheckOutput(log, a) 41 | if err != nil { 42 | log.Errorf("gomodchecks parse output failed: %v", err) 43 | return err 44 | } 45 | return lint.Report(ctx, a, parsedOutput) 46 | } 47 | 48 | func goModCheckOutput(log *xlog.Logger, a lint.Agent) (map[string][]lint.LinterOutput, error) { 49 | output := make(map[string][]lint.LinterOutput) 50 | for _, file := range a.Provider.GetFiles(nil) { 51 | fName := file 52 | if !strings.HasSuffix(fName, "go.mod") { 53 | continue 54 | } 55 | 56 | goModPath := filepath.Join(a.RepoDir, fName) 57 | file, err := os.ReadFile(goModPath) 58 | if err != nil { 59 | log.Errorf("Error opening %s: %s", goModPath, err) 60 | return output, err 61 | } 62 | 63 | mod, err := modfile.Parse("go.mod", file, nil) 64 | if err != nil { 65 | log.Errorf("Error parsing %s: %s", goModPath, err) 66 | return output, err 67 | } 68 | for _, replace := range mod.Replace { 69 | if !strings.HasPrefix(replace.New.Path, "../") { 70 | continue 71 | } 72 | 73 | parsePath := filepath.Join(filepath.Dir(goModPath), replace.New.Path) 74 | isSub, err := isSubdirectory(a.RepoDir, parsePath) 75 | if err != nil { 76 | log.Errorf("failed to compare whether A is a subdirectory of B : %v", err) 77 | } 78 | if !isSub { 79 | output[fName] = append(output[fName], lint.LinterOutput{ 80 | File: fName, 81 | Line: replace.Syntax.Start.Line, 82 | Column: replace.Syntax.Start.LineRune, 83 | Message: "cross-repository local replacement are not allowed[reviewbot]\nfor more information see https://github.com/qiniu/reviewbot/issues/275", 84 | }) 85 | } 86 | } 87 | } 88 | 89 | return output, nil 90 | } 91 | 92 | // isSubdirectory reports whether the string b is subdirectory of a. 93 | func isSubdirectory(a, b string) (bool, error) { 94 | absA, err := filepath.Abs(a) 95 | if err != nil { 96 | return false, err 97 | } 98 | absB, err := filepath.Abs(b) 99 | if err != nil { 100 | return false, err 101 | } 102 | 103 | return strings.HasPrefix(absB, absA), nil 104 | } 105 | -------------------------------------------------------------------------------- /internal/linters/go/gomodcheck/gomodcheck_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package gomodcheck 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "reflect" 24 | "testing" 25 | 26 | "github.com/google/go-github/v57/github" 27 | "github.com/qiniu/reviewbot/internal/lint" 28 | "github.com/qiniu/x/xlog" 29 | ) 30 | 31 | func TestGoModCheck(t *testing.T) { 32 | tcs := []struct { 33 | id string 34 | content []byte 35 | input []*github.CommitFile 36 | want map[string][]lint.LinterOutput 37 | }{ 38 | { 39 | id: "case1 : cross-repository local replacement ", 40 | content: []byte("replace github.com/a/c v0.0.0 => ../../github.com/c/d"), 41 | input: []*github.CommitFile{ 42 | { 43 | Filename: github.String("c/go.mod"), 44 | }, 45 | }, 46 | want: map[string][]lint.LinterOutput{ 47 | "c/go.mod": { 48 | { 49 | File: "c/go.mod", 50 | Line: 1, 51 | Column: 1, 52 | Message: "cross-repository local replacement are not allowed[reviewbot]\nfor more information see https://github.com/qiniu/reviewbot/issues/275", 53 | }, 54 | }, 55 | }, 56 | }, 57 | { 58 | id: "case2 : valid local replacement ", 59 | content: []byte("replace github.com/a/b v0.0.0 => ../github.com/c/d"), 60 | input: []*github.CommitFile{ 61 | { 62 | Filename: github.String("c/go.mod"), 63 | }, 64 | }, 65 | want: map[string][]lint.LinterOutput{}, 66 | }, 67 | { 68 | id: "case3 : valid non-local replacement ", 69 | content: []byte("replace github.com/a/b v0.0.0 => github.com/c/d v1.1.1"), 70 | input: []*github.CommitFile{ 71 | { 72 | Filename: github.String("c/go.mod"), 73 | }, 74 | }, 75 | want: map[string][]lint.LinterOutput{}, 76 | }, 77 | { 78 | id: "case4 : multiple go.mod files", 79 | content: []byte("replace github.com/a/b v0.0.0 => ../../github.com/c/d"), 80 | input: []*github.CommitFile{ 81 | { 82 | Filename: github.String("c/go.mod"), 83 | }, 84 | { 85 | Filename: github.String("d/go.mod"), 86 | }, 87 | }, 88 | want: map[string][]lint.LinterOutput{ 89 | "c/go.mod": { 90 | { 91 | File: "c/go.mod", 92 | Line: 1, 93 | Column: 1, 94 | Message: "cross-repository local replacement are not allowed[reviewbot]\nfor more information see https://github.com/qiniu/reviewbot/issues/275", 95 | }, 96 | }, 97 | "d/go.mod": { 98 | { 99 | File: "d/go.mod", 100 | Line: 1, 101 | Column: 1, 102 | Message: "cross-repository local replacement are not allowed[reviewbot]\nfor more information see https://github.com/qiniu/reviewbot/issues/275", 103 | }, 104 | }, 105 | }, 106 | }, 107 | } 108 | 109 | for _, tc := range tcs { 110 | t.Run(tc.id, func(t *testing.T) { 111 | p, err := lint.NewGithubProvider(context.TODO(), nil, github.PullRequestEvent{}, lint.WithPullRequestChangedFiles(tc.input)) 112 | if err != nil { 113 | t.Errorf("Error creating github provider: %v", err) 114 | return 115 | } 116 | // prepare go.mod files 117 | for _, file := range p.GetFiles(nil) { 118 | filename := file 119 | dir := filepath.Dir(filename) 120 | err := os.MkdirAll(dir, 0o755) 121 | if err != nil { 122 | t.Errorf("Error creating directories: %v", err) 123 | return 124 | } 125 | defer os.RemoveAll(dir) 126 | 127 | err = os.WriteFile(filename, tc.content, 0o600) 128 | if err != nil { 129 | t.Errorf("Error writing to file: %v", err) 130 | return 131 | } 132 | } 133 | 134 | output, err := goModCheckOutput(&xlog.Logger{}, lint.Agent{ 135 | Provider: p, 136 | }) 137 | if err != nil { 138 | t.Errorf("Error execute goModCheckOutput : %v", err) 139 | } 140 | if !reflect.DeepEqual(output, tc.want) { 141 | t.Errorf("got output = %v, want = %v", output, tc.want) 142 | } 143 | }) 144 | } 145 | } 146 | 147 | func TestIsSubstring(t *testing.T) { 148 | tcs := []struct { 149 | id string 150 | A string 151 | subB string 152 | want bool 153 | }{ 154 | { 155 | id: "case1 : subB is not a subdir of A", 156 | A: "A/B/C", 157 | subB: "A/B/C/../", 158 | want: false, 159 | }, 160 | { 161 | id: "case2 : subB is not a subdir of A", 162 | A: "A/B/C", 163 | subB: "A/B/C/D/../", 164 | want: true, 165 | }, 166 | } 167 | for _, tc := range tcs { 168 | t.Run(tc.id, func(t *testing.T) { 169 | got, _ := isSubdirectory(tc.A, tc.subB) 170 | if got != tc.want { 171 | t.Errorf("isSubdirectory() = %v, want = %v", got, tc.want) 172 | } 173 | }) 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /internal/linters/go/staticcheck/README.md: -------------------------------------------------------------------------------- 1 | see [staticcheck](../../../../docs/website/docs/components/go/staticcheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/go/staticcheck/staticcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Deprecated. use golangci-lint instead 18 | package staticcheck 19 | 20 | import ( 21 | "context" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | "github.com/qiniu/reviewbot/internal/util" 25 | ) 26 | 27 | // refer to https://staticcheck.io/docs/ 28 | const linterName = "staticcheck" 29 | 30 | func init() { 31 | lint.RegisterPullRequestHandler(linterName, staticcheckHandler) 32 | lint.RegisterLinterLanguages(linterName, []string{".go"}) 33 | } 34 | 35 | func staticcheckHandler(ctx context.Context, a lint.Agent) error { 36 | log := util.FromContext(ctx) 37 | if lint.IsEmpty(a.LinterConfig.Args...) { 38 | // turn off compile errors by default 39 | a.LinterConfig.Args = append([]string{}, "-debug.no-compile-errors=true", "./...") 40 | } 41 | 42 | return lint.GeneralHandler(ctx, log, a, lint.ExecRun, lint.GeneralParse) 43 | } 44 | -------------------------------------------------------------------------------- /internal/linters/java/pmdcheck/README.md: -------------------------------------------------------------------------------- 1 | see [pmdcheck](../../../../docs/website/docs/components/java/pmdcheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/java/pmdcheck/pmdcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pmdcheck 18 | 19 | import ( 20 | "context" 21 | "io" 22 | "net/http" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/qiniu/reviewbot/internal/lint" 28 | "github.com/qiniu/reviewbot/internal/util" 29 | "github.com/qiniu/x/errors" 30 | "github.com/qiniu/x/xlog" 31 | ) 32 | 33 | // refer to https://pmd.github.io/ 34 | const ( 35 | linterName = "pmdcheck" 36 | pmdRuleURL = "https://raw.githubusercontent.com/pmd/pmd/master/pmd-java/src/main/resources/category/java/bestpractices.xml" 37 | pmdRuleDir = "/var/tmp/linters-config/" 38 | ) 39 | 40 | func init() { 41 | lint.RegisterPullRequestHandler(linterName, pmdCheckHandler) 42 | lint.RegisterLinterLanguages(linterName, []string{".java"}) 43 | } 44 | 45 | func pmdCheckHandler(ctx context.Context, a lint.Agent) error { 46 | plog := util.FromContext(ctx) 47 | var javaFiles []string 48 | rulePath := a.LinterConfig.ConfigPath 49 | for _, arg := range a.Provider.GetFiles(nil) { 50 | if strings.HasSuffix(arg, ".java") { 51 | javaFiles = append(javaFiles, arg) 52 | } 53 | } 54 | if len(javaFiles) == 0 { 55 | return nil 56 | } 57 | checkrulePath, checkerr := pmdRuleCheck(plog, rulePath, a) 58 | if checkerr != nil { 59 | plog.Errorf("pmd rule check failed: %v", checkerr) 60 | return checkerr 61 | } 62 | a = argsApply(plog, a) 63 | a.LinterConfig.Args = append(append(a.LinterConfig.Args, javaFiles...), "-R", checkrulePath) 64 | return lint.GeneralHandler(ctx, plog, a, lint.ExecRun, pmdcheckParser) 65 | } 66 | 67 | func argsApply(log *xlog.Logger, a lint.Agent) lint.Agent { 68 | config := a.LinterConfig 69 | if len(config.Command) == 1 && config.Command[0] == linterName { 70 | config.Command = []string{"pmd"} 71 | } 72 | log.Info("pmdcheck comamnd:" + strings.Join(config.Command, " ")) 73 | if lint.IsEmpty(config.Args...) { 74 | args := append([]string{}, "check") 75 | args = append(args, "-f", "emacs") 76 | config.Args = args 77 | } 78 | a.LinterConfig = config 79 | return a 80 | } 81 | 82 | func pmdcheckParser(plog *xlog.Logger, output []byte) (map[string][]lint.LinterOutput, []string) { 83 | lineParse := func(line string) (*lint.LinterOutput, error) { 84 | // pmdcheck will output lines starting with ' [WARN]' or '[ERROR]' warring/error information 85 | // which are no meaningful for the reviewbot scenario, so we discard them 86 | if strings.Contains(line, "[WARN]") || strings.Contains(line, "[ERROR]") { 87 | return nil, nil 88 | } 89 | return lint.GeneralLineParser(strings.TrimLeft(line, " ")) 90 | } 91 | return lint.Parse(plog, output, lineParse) 92 | } 93 | 94 | func getFileFromURL(plog *xlog.Logger, url string) (string, error) { 95 | newfile := filepath.Join(pmdRuleDir, filepath.Base(url)) 96 | res, err := http.Get(url) 97 | if err != nil { 98 | plog.Errorf("the file download encountered an error, Please check the file download url: %v, the error is:%v", url, err) 99 | return "", err 100 | } 101 | if err := os.MkdirAll(pmdRuleDir, os.ModePerm); err != nil { 102 | plog.Fatalf("failed to create check rule config dir: %v", err) 103 | } 104 | f, err := os.Create(newfile) 105 | if err != nil { 106 | plog.Errorf("the file saving encountered an error, Please check the directory: %v", err) 107 | return "", err 108 | } 109 | _, err = io.Copy(f, res.Body) 110 | defer res.Body.Close() 111 | if err != nil { 112 | plog.Errorf("the file saving encountered an error: %v", err) 113 | return "", err 114 | } 115 | return newfile, nil 116 | } 117 | 118 | func pmdRuleCheck(plog *xlog.Logger, pmdConf string, a lint.Agent) (string, error) { 119 | tmpnewfile := filepath.Join(pmdRuleDir, filepath.Base(pmdRuleURL)) 120 | if pmdConf == "" { 121 | absfilepath, _ := util.FileExists(tmpnewfile) 122 | if absfilepath != "" { 123 | return absfilepath, nil 124 | } 125 | downloadfilepath, err := getFileFromURL(plog, pmdRuleURL) 126 | if err != nil { 127 | plog.Errorf("the pmd rule file download faild: %v", err) 128 | return "", err 129 | } 130 | return downloadfilepath, nil 131 | } 132 | if strings.HasPrefix(pmdConf, "http://") || strings.HasPrefix(pmdConf, "https://") { 133 | downloadfilepath, err := getFileFromURL(plog, pmdRuleURL) 134 | if err != nil { 135 | plog.Errorf("the pmd rule file download faild: %v", err) 136 | return "", err 137 | } 138 | return downloadfilepath, nil 139 | } 140 | absfilepath, exist := util.FileExists(pmdConf) 141 | if exist { 142 | return absfilepath, nil 143 | } 144 | pmdconfpath := filepath.Join(a.LinterConfig.WorkDir, pmdConf) 145 | abspmdfilepath, _ := util.FileExists(pmdconfpath) 146 | if absfilepath != "" { 147 | return abspmdfilepath, nil 148 | } 149 | return "", errors.New("the pmd rule file not exist") 150 | } 151 | -------------------------------------------------------------------------------- /internal/linters/java/pmdcheck/pmdcheck_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package pmdcheck 18 | 19 | import ( 20 | "os" 21 | "reflect" 22 | "testing" 23 | 24 | "github.com/qiniu/reviewbot/config" 25 | "github.com/qiniu/reviewbot/internal/lint" 26 | "github.com/qiniu/x/errors" 27 | "github.com/qiniu/x/log" 28 | "github.com/qiniu/x/xlog" 29 | ) 30 | 31 | func TestArgs(t *testing.T) { 32 | tp := true 33 | tcs := []struct { 34 | id string 35 | input lint.Agent 36 | want lint.Agent 37 | }{ 38 | { 39 | id: "case1 - default command and args", 40 | input: lint.Agent{ 41 | LinterConfig: config.Linter{ 42 | Enable: &tp, 43 | Command: []string{"pmdcheck"}, 44 | }, 45 | }, 46 | want: lint.Agent{ 47 | LinterConfig: config.Linter{ 48 | Enable: &tp, 49 | Command: []string{"pmd"}, 50 | Args: []string{"check", "-f", "emacs"}, 51 | }, 52 | }, 53 | }, 54 | { 55 | id: "case2 - custom command", 56 | input: lint.Agent{ 57 | LinterConfig: config.Linter{ 58 | Enable: &tp, 59 | Command: []string{"/usr/pmdcheck"}, 60 | }, 61 | }, 62 | want: lint.Agent{ 63 | LinterConfig: config.Linter{ 64 | Enable: &tp, 65 | Command: []string{"/usr/pmdcheck"}, 66 | Args: []string{"check", "-f", "emacs"}, 67 | }, 68 | }, 69 | }, 70 | { 71 | id: "case3 - custom args", 72 | input: lint.Agent{ 73 | LinterConfig: config.Linter{ 74 | Enable: &tp, 75 | Command: []string{"pmdcheck"}, 76 | Args: []string{"check", "-f", "xml"}, 77 | }, 78 | }, 79 | want: lint.Agent{ 80 | LinterConfig: config.Linter{ 81 | Enable: &tp, 82 | Command: []string{"pmd"}, 83 | Args: []string{"check", "-f", "xml"}, 84 | }, 85 | }, 86 | }, 87 | } 88 | 89 | for _, tc := range tcs { 90 | t.Run(tc.id, func(t *testing.T) { 91 | got := argsApply(xlog.New("ut"), tc.input) 92 | if !reflect.DeepEqual(got.LinterConfig, tc.want.LinterConfig) { 93 | t.Errorf("args() = %v, want %v", got.LinterConfig, tc.want.LinterConfig) 94 | } 95 | }) 96 | } 97 | } 98 | 99 | func TestPmdRuleCheck(t *testing.T) { 100 | dir := "/var/tmp/linters-config/" 101 | a := lint.Agent{} 102 | a.LinterConfig.WorkDir = "" 103 | tc := []struct { 104 | input string 105 | expected string 106 | err error 107 | }{ 108 | { 109 | input: "", 110 | expected: dir + "bestpractices.xml", 111 | err: nil, 112 | }, 113 | { 114 | input: "/config/linters-config/.notjava-bestpractices.xml", 115 | expected: "", 116 | err: errors.New("the pmd rule file not exist"), 117 | }, 118 | { 119 | input: "https://raw.githubusercontent.com/pmd/pmd/master/pmd-java/src/main/resources/category/java/bestpractices.xml", 120 | expected: dir + "bestpractices.xml", 121 | err: nil, 122 | }, 123 | } 124 | for _, c := range tc { 125 | got, err := pmdRuleCheck(xlog.New("ut"), c.input, a) 126 | log.Info("E:" + c.input) 127 | if !reflect.DeepEqual(got, c.expected) { 128 | t.Errorf("pmdcheckParser(): %v, expected: %v", got, c.expected) 129 | } 130 | if !reflect.DeepEqual(err, c.err) { 131 | t.Errorf("pmdcheckParser() error: %v, unexpected: %v", err, c.err) 132 | return 133 | } 134 | } 135 | t.Cleanup(func() { 136 | _ = os.RemoveAll(dir) 137 | }) 138 | } 139 | 140 | func TestFormatPmdCheckLine(t *testing.T) { 141 | tc := []struct { 142 | input []byte 143 | expected map[string][]lint.LinterOutput 144 | unexpected []string 145 | }{ 146 | { 147 | input: []byte(`[ERROR] No such file ./test3.java 148 | [WARN] Progressbar rendering conflicts with reporting to STDOUT. No progressbar will be shown. Try running with argument -r to output the report to a file instead. 149 | [WARN] This analysis could be faster, please consider using Incremental Analysis: https://docs.pmd-code.org/pmd-doc-7.4.0/pmd_userdocs_incremental_analysis.html 150 | ./test.java:8: Avoid unused local variables such as 'test'.`), 151 | expected: map[string][]lint.LinterOutput{ 152 | "./test.java": { 153 | { 154 | File: "./test.java", 155 | Line: 8, 156 | Column: 0, 157 | Message: "Avoid unused local variables such as 'test'.", 158 | }, 159 | }, 160 | }, 161 | unexpected: nil, 162 | }, 163 | } 164 | for _, c := range tc { 165 | got, err := pmdcheckParser(xlog.New("UnitJavaPmdCheckTest"), c.input) 166 | if !reflect.DeepEqual(err, c.unexpected) { 167 | t.Errorf("stylecheckParser() error: %v, unexpected: %v", err, c.unexpected) 168 | return 169 | } 170 | if !reflect.DeepEqual(got, c.expected) { 171 | t.Errorf("stylecheckParser(): %v, expected: %v", got, c.expected) 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /internal/linters/java/stylecheck/README.md: -------------------------------------------------------------------------------- 1 | see [stylecheck](../../../../docs/website/docs/components/java/stylecheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/java/stylecheck/stylecheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package stylecheck 18 | 19 | import ( 20 | "context" 21 | "io" 22 | "net/http" 23 | "os" 24 | "path/filepath" 25 | "strings" 26 | 27 | "github.com/qiniu/reviewbot/internal/lint" 28 | "github.com/qiniu/reviewbot/internal/util" 29 | "github.com/qiniu/x/errors" 30 | "github.com/qiniu/x/xlog" 31 | ) 32 | 33 | const linterName = "stylecheck" 34 | 35 | const ( 36 | styleJarURL = "https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.17.0/checkstyle-10.17.0-all.jar" 37 | localStyleJar = "/checkstyle.jar" 38 | styleRuleURL = "https://raw.githubusercontent.com/checkstyle/checkstyle/master/src/main/resources/sun_checks.xml" 39 | styleRuleDir = "/var/tmp/linters-config/" 40 | ) 41 | 42 | func init() { 43 | lint.RegisterPullRequestHandler(linterName, stylecheckHandler) 44 | lint.RegisterLinterLanguages(linterName, []string{".java"}) 45 | } 46 | 47 | func stylecheckHandler(ctx context.Context, a lint.Agent) error { 48 | slog := util.FromContext(ctx) 49 | var javaFiles []string 50 | rulePath := a.LinterConfig.ConfigPath 51 | for _, arg := range a.Provider.GetFiles(nil) { 52 | if strings.HasSuffix(arg, ".java") { 53 | javaFiles = append(javaFiles, arg) 54 | } 55 | } 56 | if len(javaFiles) == 0 { 57 | return nil 58 | } 59 | checkrulePath, checkerr := styleRuleCheck(a.LinterConfig.WorkDir)(slog, rulePath) 60 | if checkerr != nil { 61 | slog.Errorf("style rule check failed: %v", checkerr) 62 | return checkerr 63 | } 64 | 65 | a = argsApply(slog, a) 66 | a.LinterConfig.Args = append(a.LinterConfig.Args, "-jar", localStyleJar, "-c", checkrulePath) 67 | a.LinterConfig.Args = append(a.LinterConfig.Args, javaFiles...) 68 | 69 | return lint.GeneralHandler(ctx, slog, a, lint.ExecRun, stylecheckParser(a.LinterConfig.WorkDir)) 70 | } 71 | 72 | func argsApply(log *xlog.Logger, a lint.Agent) lint.Agent { 73 | config := a.LinterConfig 74 | if len(a.LinterConfig.Command) == 1 && a.LinterConfig.Command[0] == linterName { 75 | config.Command = []string{"java"} 76 | } 77 | log.Info("stylecheck comamnd:" + strings.Join(config.Command, " ")) 78 | if lint.IsEmpty(config.Args...) { 79 | args := append([]string{}, "") 80 | config.Args = args 81 | } 82 | a.LinterConfig = config 83 | return a 84 | } 85 | 86 | func stylecheckParser(codedir string) func(slog *xlog.Logger, output []byte) (map[string][]lint.LinterOutput, []string) { 87 | return func(slog *xlog.Logger, output []byte) (map[string][]lint.LinterOutput, []string) { 88 | lineParse := func(line string) (*lint.LinterOutput, error) { 89 | // stylecheck will output lines starting with ' 开始检查(Starting audit) ' or '检查结束(Audit done) ' or 'stylecheck result(Checkstyle ends with 20 errors.)' 90 | // which are no meaningful for the reviewbot scenario, so we discard them Starting audit done. 91 | if strings.Contains(strings.ToLower(line), "checkstyle") || strings.HasPrefix(line, "Starting audit") || strings.HasPrefix(line, "Audit done") || strings.HasPrefix(line, "检查") { 92 | return nil, nil 93 | } 94 | line = strings.ReplaceAll(line, "[ERROR]", "") 95 | line = strings.ReplaceAll(line, codedir+"/", "") 96 | return lint.GeneralLineParser(strings.TrimLeft(line, " ")) 97 | } 98 | return lint.Parse(slog, output, lineParse) 99 | } 100 | } 101 | 102 | func stylecheckJar(slog *xlog.Logger) (string, error) { 103 | jarfilepath := filepath.Join(styleRuleDir, localStyleJar) 104 | _, exist := util.FileExists(jarfilepath) 105 | if !exist { 106 | res, err := getFileFromURL(slog, styleJarURL) 107 | if err != nil { 108 | return "", err 109 | } 110 | return res, nil 111 | } 112 | return jarfilepath, nil 113 | } 114 | 115 | func getFileFromURL(slog *xlog.Logger, url string) (string, error) { 116 | newfile := filepath.Join(styleRuleDir, filepath.Base(url)) 117 | res, err := http.Get(url) 118 | if err != nil { 119 | return "", err 120 | } 121 | merr := os.MkdirAll(styleRuleDir, os.ModePerm) 122 | if merr != nil { 123 | return "", merr 124 | } 125 | f, err := os.Create(newfile) 126 | if err != nil { 127 | return "", err 128 | } 129 | _, err = io.Copy(f, res.Body) 130 | defer res.Body.Close() 131 | if err != nil { 132 | slog.Errorf("the file saving encountered an error: %v", err) 133 | return "", err 134 | } 135 | return newfile, nil 136 | } 137 | 138 | func styleRuleCheck(codedir string) func(slog *xlog.Logger, styleConf string) (string, error) { 139 | return func(slog *xlog.Logger, styleConf string) (string, error) { 140 | tmpnewfile := filepath.Join(styleRuleDir, "tmp", filepath.Base(styleRuleURL)) 141 | if styleConf == "" { 142 | absfilepath, _ := util.FileExists(tmpnewfile) 143 | if absfilepath != "" { 144 | return absfilepath, nil 145 | } 146 | downloadfilepath, err := getFileFromURL(slog, styleRuleURL) 147 | if err != nil { 148 | slog.Errorf("the style rule file download faild: %v", err) 149 | return "", err 150 | } 151 | return downloadfilepath, nil 152 | } 153 | if strings.HasPrefix(styleConf, "http://") || strings.HasPrefix(styleConf, "https://") { 154 | downloadfilepath, err := getFileFromURL(slog, styleRuleURL) 155 | if err != nil { 156 | slog.Errorf("the style rule file download faild: %v", err) 157 | return "", err 158 | } 159 | return downloadfilepath, nil 160 | } 161 | absfilepath, exist := util.FileExists(styleConf) 162 | if exist { 163 | return absfilepath, nil 164 | } 165 | rulefilepathcode := filepath.Join(codedir, styleConf) 166 | absfilepathcode, existcode := util.FileExists(rulefilepathcode) 167 | if existcode { 168 | return absfilepathcode, nil 169 | } 170 | return "", errors.New("the style rule file not exist") 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /internal/linters/lua/luacheck/README.md: -------------------------------------------------------------------------------- 1 | see [luacheck](../../../../docs/website/docs/components/lua/luacheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/lua/luacheck/luacheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package luacheck 18 | 19 | import ( 20 | "context" 21 | "strings" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | "github.com/qiniu/reviewbot/internal/util" 25 | "github.com/qiniu/x/xlog" 26 | ) 27 | 28 | // refer to https://github.com/mpeterv/luacheck 29 | const linterName = "luacheck" 30 | 31 | func init() { 32 | lint.RegisterPullRequestHandler(linterName, luacheckHandler) 33 | lint.RegisterLinterLanguages(linterName, []string{".lua"}) 34 | } 35 | 36 | func luacheckHandler(ctx context.Context, a lint.Agent) error { 37 | log := util.FromContext(ctx) 38 | if lint.IsEmpty(a.LinterConfig.Args...) { 39 | // identify global variables for Redis and Nginx modules. 40 | // disable the maximum line length check, which is no need. 41 | // luacheck execute the command "--globals='ngx KEYS ARGV table redis cjson'", which does not take effect. 42 | // so the parameter should be changed to an array-based approach. 43 | // luacheck will output lines starting with unrecognized characters, so add the parameter: --no-color 44 | cmdArgs := []string{ 45 | ".", 46 | "--globals=ngx", 47 | "--globals=KEYS", 48 | "--globals=ARGV", 49 | "--globals=table", 50 | "--globals=redis", 51 | "--globals=cjson", 52 | "--no-max-line-length", 53 | "--no-color", 54 | } 55 | a.LinterConfig.Args = append([]string{}, cmdArgs...) 56 | } 57 | 58 | // recommend to use the line-number format and disable the issued lines, since these are more friendly to the reviewbot 59 | // checking on luacheck 0.26.1 Lua5.1, there is no problem even with multiple --no-color parameter, 60 | // so we can add the parameter directly 61 | a.LinterConfig.Args = append(a.LinterConfig.Args, "--no-color") 62 | return lint.GeneralHandler(ctx, log, a, lint.ExecRun, parser) 63 | } 64 | 65 | func parser(log *xlog.Logger, output []byte) (map[string][]lint.LinterOutput, []string) { 66 | lineParse := func(line string) (*lint.LinterOutput, error) { 67 | // luacheck will output lines starting with 'Total ' or 'Checking ' 68 | // which are no meaningful for the reviewbot scenario, so we discard them 69 | // such as: 70 | // 1. Total: 0 warnings / 0 errors in 0 files 71 | // 2. Checking cmd/jarviswsserver/etc/get_node_wsserver.lua 11 warnings 72 | // 3. Empty line 73 | if strings.HasPrefix(line, "Total: ") || strings.HasPrefix(line, "Checking ") || line == "" { 74 | return nil, nil 75 | } 76 | return lint.GeneralLineParser(strings.TrimLeft(line, " ")) 77 | } 78 | return lint.Parse(log, output, lineParse) 79 | } 80 | -------------------------------------------------------------------------------- /internal/linters/lua/luacheck/luacheck_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package luacheck 18 | 19 | import ( 20 | "reflect" 21 | "testing" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | "github.com/qiniu/x/xlog" 25 | ) 26 | 27 | func TestParser(t *testing.T) { 28 | tc := []struct { 29 | input []byte 30 | expected map[string][]lint.LinterOutput 31 | unexpected []string 32 | }{ 33 | { 34 | input: []byte(` 35 | video/mp4/libs/mp4lib.lua:184:11: value assigned to variable mem_data is overwritten on line 202 before use 36 | `), 37 | expected: map[string][]lint.LinterOutput{ 38 | "video/mp4/libs/mp4lib.lua": { 39 | { 40 | File: "video/mp4/libs/mp4lib.lua", 41 | Line: 184, 42 | Column: 11, 43 | Message: "value assigned to variable mem_data is overwritten on line 202 before use", 44 | }, 45 | }, 46 | }, 47 | unexpected: nil, 48 | }, 49 | { 50 | input: []byte(` 51 | utils/jsonschema.lua:723:121: line is too long (142 > 120) 52 | `), 53 | expected: map[string][]lint.LinterOutput{ 54 | "utils/jsonschema.lua": { 55 | { 56 | File: "utils/jsonschema.lua", 57 | Line: 723, 58 | Column: 121, 59 | Message: "line is too long (142 > 120)", 60 | }, 61 | }, 62 | }, 63 | unexpected: nil, 64 | }, 65 | { 66 | input: []byte(` 67 | Total: 0 warnings / 0 errors in 0 files 68 | `), 69 | expected: map[string][]lint.LinterOutput{}, 70 | unexpected: nil, 71 | }, 72 | { 73 | input: []byte(` 74 | Checking test/qtest_mgrconf.lua 75 | `), 76 | expected: map[string][]lint.LinterOutput{}, 77 | unexpected: nil, 78 | }, 79 | { 80 | input: []byte(``), 81 | expected: map[string][]lint.LinterOutput{}, 82 | unexpected: nil, 83 | }, 84 | } 85 | 86 | for _, c := range tc { 87 | got, unexpected := parser(xlog.New("UnitLuaCheckTest"), c.input) 88 | if !reflect.DeepEqual(got, c.expected) { 89 | t.Errorf("parser(): %v, expected: %v", got, c.expected) 90 | } 91 | if !reflect.DeepEqual(unexpected, c.unexpected) { 92 | t.Errorf("parser(): %v, expected: %v", unexpected, c.unexpected) 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /internal/linters/shell/shellcheck/README.md: -------------------------------------------------------------------------------- 1 | see [shellcheck](../../../../docs/website/docs/components/shell/shellcheck.md) 2 | -------------------------------------------------------------------------------- /internal/linters/shell/shellcheck/shellcheck.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package shellcheck 18 | 19 | import ( 20 | "context" 21 | "strings" 22 | 23 | "github.com/qiniu/reviewbot/internal/lint" 24 | "github.com/qiniu/reviewbot/internal/metric" 25 | "github.com/qiniu/reviewbot/internal/util" 26 | ) 27 | 28 | // refer to https://github.com/koalaman/shellcheck 29 | const linterName = "shellcheck" 30 | 31 | func init() { 32 | lint.RegisterPullRequestHandler(linterName, shellcheck) 33 | lint.RegisterLinterLanguages(linterName, []string{".sh"}) 34 | } 35 | 36 | func shellcheck(ctx context.Context, a lint.Agent) error { 37 | log := util.FromContext(ctx) 38 | var shellFiles []string 39 | for _, arg := range a.Provider.GetFiles(nil) { 40 | if strings.HasSuffix(arg, ".sh") { 41 | shellFiles = append(shellFiles, arg) 42 | } 43 | } 44 | 45 | var lintResults map[string][]lint.LinterOutput 46 | if len(shellFiles) > 0 { 47 | cmd := a.LinterConfig.Command 48 | // execute shellcheck with the following command 49 | // shellcheck -f gcc xxx.sh... 50 | if lint.IsEmpty(a.LinterConfig.Args...) { 51 | // use gcc format to make the output more readable 52 | args := append([]string{}, "-f", "gcc") 53 | args = append(args, shellFiles...) 54 | a.LinterConfig.Args = args 55 | } 56 | 57 | output, err := lint.ExecRun(ctx, a) 58 | if err != nil { 59 | log.Warnf("%s run with error: %v, mark and continue", cmd, err) 60 | } 61 | 62 | results, unexpected := lint.GeneralParse(log, output) 63 | if len(unexpected) > 0 { 64 | msg := util.LimitJoin(unexpected, 1000) 65 | log.Warnf("unexpected output: %v", msg) 66 | metric.NotifyWebhookByText(lint.ConstructUnknownMsg(linterName, a.Provider.GetCodeReviewInfo().Org+"/"+a.Provider.GetCodeReviewInfo().Repo, a.Provider.GetCodeReviewInfo().URL, log.ReqId, msg)) 67 | } 68 | 69 | lintResults = results 70 | } 71 | 72 | // even if the lintResults is empty, we still need to report the result 73 | // since we need delete the existed comments related to the linter 74 | return lint.Report(ctx, a, lintResults) 75 | } 76 | -------------------------------------------------------------------------------- /internal/llm/llm.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "time" 8 | 9 | "github.com/qiniu/reviewbot/internal/util" 10 | "github.com/tmc/langchaingo/llms" 11 | ) 12 | 13 | type Config struct { 14 | Provider string 15 | APIKey string 16 | Model string 17 | ServerURL string 18 | } 19 | 20 | var ErrUnsupportedProvider = errors.New("unsupported llm provider") 21 | var ErrModelIsNil = errors.New("model is nil") 22 | 23 | func New(ctx context.Context, config Config) (llms.Model, error) { 24 | switch config.Provider { 25 | case "openai": 26 | return initOpenAIClient(config) 27 | case "ollama": 28 | return initOllamaClient(config) 29 | default: 30 | return nil, ErrUnsupportedProvider 31 | } 32 | } 33 | 34 | func Query(ctx context.Context, model llms.Model, query string, extraContext string) (string, error) { 35 | log := util.FromContext(ctx) 36 | ragQuery := fmt.Sprintf(ragTemplateStr, query, extraContext) 37 | 38 | timeout := 5 * time.Minute 39 | ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) 40 | defer cancel() 41 | 42 | respText, err := llms.GenerateFromSinglePrompt(ctxWithTimeout, model, ragQuery) 43 | if err != nil { 44 | if errors.Is(err, context.DeadlineExceeded) { 45 | log.Warnf("LLM query operation timed out after %s", timeout) 46 | } 47 | return "", err 48 | } 49 | log.Debugf("length of LLM respText: %d", len(respText)) 50 | return respText, nil 51 | } 52 | 53 | const ragTemplateStr = ` 54 | I will ask you a question and will provide some additional context information. 55 | Assume this context information is factual and correct, as part of internal 56 | documentation. 57 | If the question relates to the context, answer it using the context. 58 | If the question does not relate to the context, answer it as normal. 59 | 60 | For example, let's say the context has nothing in it about tropical flowers; 61 | then if I ask you about tropical flowers, just answer what you know about them 62 | without referring to the context. 63 | 64 | For example, if the context does mention minerology and I ask you about that, 65 | provide information from the context along with general knowledge. 66 | 67 | Question: 68 | %s 69 | 70 | Context: 71 | %s 72 | ` 73 | 74 | func QueryForReference(ctx context.Context, model llms.Model, linterOutput string, codeLanguage string) (string, error) { 75 | log := util.FromContext(ctx) 76 | if model == nil { 77 | return "", ErrModelIsNil 78 | } 79 | ragQuery := fmt.Sprintf(referenceTemplateStr, codeLanguage, linterOutput) 80 | 81 | timeout := 5 * time.Minute 82 | ctxWithTimeout, cancel := context.WithTimeout(ctx, timeout) 83 | defer cancel() 84 | 85 | respText, err := llms.GenerateFromSinglePrompt(ctxWithTimeout, model, ragQuery) 86 | if err != nil { 87 | if errors.Is(err, context.DeadlineExceeded) { 88 | log.Errorf("LLM query operation timed out after %s", timeout) 89 | } 90 | return "", err 91 | } 92 | 93 | log.Infof("promote linter output:%s, length of response:%d", linterOutput, len(respText)) 94 | return respText, nil 95 | } 96 | 97 | const referenceTemplateStr = ` 98 | You are a lint expert who can explain in detail the meaning of lint results based on the provided . 99 | 100 | Please follow the format in to respond in Chinese: 101 | 102 | 1. **lint 解释**: 103 | - 请仔细查看内容, 其内容为某个具体的lint结果, 请用简短的语言对该lint结果进行解释。 104 | 105 | 2. **错误用法**: 106 | - 提供一个代码示例或文本描述,展示不正确的用法。若给出代码,代码语言请遵循 107 | 108 | 3. **正确用法**: 109 | - 给出一个代码示例或文本描述,展示正确的用法。若给出代码,代码语言请遵循 110 | 111 | 以上三块内容有且仅输出一次, 即一次“lint 解释”,一次“错误用法”, 一次“正确用法”。保证输出不多不少 112 | 请确保输出不超过 5000 个字符 113 | 114 | 115 | 116 | 117 | ### lint 解释 118 | 119 | ### 错误用法 120 | 121 | ### 正确用法 122 | 123 | 124 | 125 | 126 | %s 127 | 128 | 129 | 130 | %s 131 | 132 | ` 133 | -------------------------------------------------------------------------------- /internal/llm/ollama.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tmc/langchaingo/llms" 7 | "github.com/tmc/langchaingo/llms/ollama" 8 | ) 9 | 10 | var ErrServerURLRequired = errors.New("server URL is required") 11 | var ErrModelRequired = errors.New("model is required") 12 | 13 | func initOllamaClient(config Config) (llms.Model, error) { 14 | if config.ServerURL == "" { 15 | return nil, ErrServerURLRequired 16 | } 17 | if config.Model == "" { 18 | return nil, ErrModelRequired 19 | } 20 | opts := []ollama.Option{ 21 | ollama.WithServerURL(config.ServerURL), 22 | ollama.WithModel(config.Model), 23 | } 24 | m, err := ollama.New(opts...) 25 | if err != nil { 26 | return nil, err 27 | } 28 | return m, nil 29 | } 30 | -------------------------------------------------------------------------------- /internal/llm/openapi.go: -------------------------------------------------------------------------------- 1 | package llm 2 | 3 | import ( 4 | "errors" 5 | 6 | "github.com/tmc/langchaingo/llms" 7 | "github.com/tmc/langchaingo/llms/openai" 8 | ) 9 | 10 | var ErrAPIKeyRequired = errors.New("API key is required") 11 | 12 | func initOpenAIClient(config Config) (llms.Model, error) { 13 | if config.APIKey == "" { 14 | return nil, ErrAPIKeyRequired 15 | } 16 | opts := []openai.Option{ 17 | openai.WithModel(config.Model), 18 | openai.WithToken(config.APIKey), 19 | openai.WithBaseURL(config.ServerURL), 20 | } 21 | m, err := openai.New(opts...) 22 | if err != nil { 23 | return nil, err 24 | } 25 | return m, nil 26 | } 27 | -------------------------------------------------------------------------------- /internal/metric/metrics.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package metric 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "os" 24 | "strings" 25 | 26 | "github.com/prometheus/client_golang/prometheus" 27 | "github.com/prometheus/client_golang/prometheus/promauto" 28 | "github.com/qiniu/x/log" 29 | ) 30 | 31 | // use WEWORK_WEBHOOK to send alert message to wework group 32 | // refer: https://developer.work.weixin.qq.com/document/path/91770 33 | var WEWORK_WEBHOOK = os.Getenv("WEWORK_WEBHOOK") 34 | 35 | var issueCounter = promauto.NewCounterVec(prometheus.CounterOpts{ 36 | Name: "reviewbot_issue_found_total", 37 | Help: "issue found by linter", 38 | }, []string{"repo", "linter", "pull_request", "commit"}) 39 | 40 | func IncIssueCounter(repo, linter, pull_request, commit string, count float64) { 41 | issueCounter.WithLabelValues(repo, linter, pull_request, commit).Add(count) 42 | } 43 | 44 | type MessageBody struct { 45 | MsgType string `json:"msgtype"` 46 | Text MsgContent `json:"text,omitempty"` 47 | Markdown MsgContent `json:"markdown,omitempty"` 48 | } 49 | 50 | type MsgContent struct { 51 | Content string `json:"content"` 52 | } 53 | 54 | // notify sends message to wework group 55 | // refer: https://developer.work.weixin.qq.com/document/path/91770 56 | func notify(message MessageBody) error { 57 | if WEWORK_WEBHOOK == "" || (message.Text.Content == "" && message.Markdown.Content == "") { 58 | return nil 59 | } 60 | 61 | body, err := json.Marshal(message) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | resp, err := http.DefaultClient.Post(WEWORK_WEBHOOK, "application/json", strings.NewReader(string(body))) 67 | if err != nil { 68 | return err 69 | } 70 | defer resp.Body.Close() 71 | 72 | if resp.StatusCode != http.StatusOK { 73 | return fmt.Errorf("send message failed: %v", resp) 74 | } 75 | 76 | errCode := resp.Header.Get("Error-Code") 77 | if errCode != "0" { 78 | return fmt.Errorf("send message failed, errCode: %v, errMsg: %v", errCode, resp.Header.Get("Error-Msg")) 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // notifyAsync sends message to wework group asynchronously. 85 | func notifyAsync(message MessageBody) { 86 | go func() { 87 | if err := notify(message); err != nil { 88 | log.Infof("send message failed, err: %v, message: %v\n", err, message) 89 | } 90 | }() 91 | } 92 | 93 | // NotifyWebhookByText sends text message to wework group. 94 | func NotifyWebhookByText(content string) { 95 | notifyAsync(MessageBody{ 96 | MsgType: "text", 97 | Text: MsgContent{Content: content}, 98 | }) 99 | } 100 | 101 | // NotifyWebhookByMarkdown sends markdown message to wework group. 102 | func NotifyWebhookByMarkdown(content string) { 103 | notifyAsync(MessageBody{ 104 | MsgType: "markdown", 105 | Markdown: MsgContent{ 106 | Content: content, 107 | }, 108 | }) 109 | } 110 | -------------------------------------------------------------------------------- /internal/runner/runner.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package runner 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "fmt" 23 | "io" 24 | "os" 25 | "os/exec" 26 | "strings" 27 | 28 | "github.com/docker/docker/api/types" 29 | "github.com/docker/docker/api/types/container" 30 | "github.com/docker/docker/api/types/image" 31 | "github.com/docker/docker/api/types/network" 32 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 33 | "github.com/qiniu/reviewbot/config" 34 | "github.com/qiniu/reviewbot/internal/util" 35 | ) 36 | 37 | // Runner defines the interface for executing linters. 38 | // It is not concurrency-safe. Use Clone() to obtain a new instance for each linter when running concurrently. 39 | type Runner interface { 40 | // Prepare prepares the linter for running. 41 | Prepare(ctx context.Context, cfg *config.Linter) error 42 | // Run runs the linter and returns the output. 43 | Run(ctx context.Context, cfg *config.Linter) (io.ReadCloser, error) 44 | // GetFinalScript returns the final script to be executed. 45 | // It should be called after Run function. and it's used for logging and debugging. 46 | GetFinalScript() string 47 | // Clone returns a new Runner instance with the same configuration to keep concurrency safe. 48 | // It's used for creating a new runner for each linter. 49 | Clone() Runner 50 | } 51 | 52 | // LocalRunner is a runner that runs the linter locally. 53 | type LocalRunner struct { 54 | script string 55 | } 56 | 57 | func NewLocalRunner() Runner { 58 | return &LocalRunner{} 59 | } 60 | 61 | func (l *LocalRunner) Clone() Runner { 62 | return &LocalRunner{} 63 | } 64 | 65 | func (l *LocalRunner) GetFinalScript() string { 66 | return l.script 67 | } 68 | 69 | func (l *LocalRunner) Prepare(ctx context.Context, cfg *config.Linter) error { 70 | return nil 71 | } 72 | 73 | func (l *LocalRunner) Run(ctx context.Context, cfg *config.Linter) (io.ReadCloser, error) { 74 | log := util.FromContext(ctx) 75 | newCfg, err := cfg.Modifier.Modify(cfg) 76 | if err != nil { 77 | return nil, err 78 | } 79 | log.Infof("final config: %v", newCfg) 80 | 81 | // construct the script content 82 | scriptContent := "set -e\n" 83 | 84 | // handle command 85 | var shell []string 86 | if len(newCfg.Command) > 0 && (newCfg.Command[0] == "/bin/bash" || newCfg.Command[0] == "/bin/sh") { 87 | shell = newCfg.Command 88 | } else { 89 | shell = []string{"/bin/sh", "-c"} 90 | if len(newCfg.Command) > 0 { 91 | scriptContent += strings.Join(newCfg.Command, " ") + "\n" 92 | } 93 | } 94 | 95 | // handle args 96 | scriptContent += strings.Join(newCfg.Args, " ") 97 | 98 | log.Infof("Script content: \n%s", scriptContent) 99 | l.script = scriptContent 100 | 101 | //nolint:gosec 102 | c := exec.CommandContext(ctx, shell[0], append(shell[1:], scriptContent)...) 103 | c.Dir = newCfg.WorkDir 104 | 105 | // create a temp dir for the artifact 106 | artifact, err := os.MkdirTemp("", "artifact") 107 | if err != nil { 108 | return nil, err 109 | } 110 | defer os.RemoveAll(artifact) 111 | c.Env = append(os.Environ(), fmt.Sprintf("ARTIFACT=%s", artifact)) 112 | c.Env = append(c.Env, newCfg.Env...) 113 | 114 | log.Infof("run command: %v, workDir: %v", c, c.Dir) 115 | output, execErr := c.CombinedOutput() 116 | 117 | // read all files under the artifact dir 118 | var fileContent []byte 119 | artifactFiles, err := os.ReadDir(artifact) 120 | if err != nil { 121 | return nil, err 122 | } 123 | 124 | var idx int 125 | for _, file := range artifactFiles { 126 | if file.IsDir() { 127 | continue 128 | } 129 | log.Infof("artifact file: %v", file.Name()) 130 | content, err := os.ReadFile(fmt.Sprintf("%s/%s", artifact, file.Name())) 131 | if err != nil { 132 | return nil, err 133 | } 134 | if len(content) == 0 { 135 | continue 136 | } 137 | if idx > 0 { 138 | fileContent = append(fileContent, '\n') 139 | } 140 | fileContent = append(fileContent, content...) 141 | idx++ 142 | } 143 | 144 | // use the content of the files under Artifact dir as first priority 145 | if len(fileContent) > 0 { 146 | log.Debugf("artifact files used instead. legacy output:\n%v, now:\n%v", string(output), string(fileContent)) 147 | output = fileContent 148 | } 149 | 150 | // wrap the output to io.ReadCloser 151 | return io.NopCloser(bytes.NewReader(output)), execErr 152 | } 153 | 154 | // for easy mock. 155 | // copy from https://github.com/moby/moby/blob/v27.2.1/client/interface.go#L48. 156 | type DockerClientInterface interface { 157 | ImageInspectWithRaw(ctx context.Context, imageID string) (types.ImageInspect, []byte, error) 158 | ImagePull(ctx context.Context, refStr string, options image.PullOptions) (io.ReadCloser, error) 159 | ContainerCreate(ctx context.Context, config *container.Config, hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, platform *ocispec.Platform, containerName string) (container.CreateResponse, error) 160 | ContainerStart(ctx context.Context, containerID string, options container.StartOptions) error 161 | ContainerLogs(ctx context.Context, container string, options container.LogsOptions) (io.ReadCloser, error) 162 | CopyToContainer(ctx context.Context, containerID, dstPath string, content io.Reader, options container.CopyToContainerOptions) error 163 | ContainerWait(ctx context.Context, containerID string, condition container.WaitCondition) (<-chan container.WaitResponse, <-chan error) 164 | CopyFromContainer(ctx context.Context, containerID, srcPath string) (io.ReadCloser, container.PathStat, error) 165 | ContainerStatPath(ctx context.Context, containerID, path string) (container.PathStat, error) 166 | } 167 | -------------------------------------------------------------------------------- /internal/storage/file.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package storage 18 | 19 | import ( 20 | "context" 21 | "fmt" 22 | "os" 23 | "path/filepath" 24 | 25 | "github.com/qiniu/x/log" 26 | ) 27 | 28 | type LocalStorage struct { 29 | rootDir string 30 | } 31 | 32 | func NewLocalStorage(rootDir string) (Storage, error) { 33 | rootDir, err := filepath.Abs(rootDir) 34 | if err != nil { 35 | return nil, fmt.Errorf("failed to get absolute path: %w", err) 36 | } 37 | if err := os.MkdirAll(rootDir, 0o755); err != nil { 38 | return nil, fmt.Errorf("failed to make log dir: %w", err) 39 | } 40 | return &LocalStorage{rootDir: rootDir}, nil 41 | } 42 | 43 | func (l *LocalStorage) Write(ctx context.Context, path string, content []byte) error { 44 | logFile := filepath.Join(l.rootDir, path, DefaultLogName) 45 | log.Infof("writing log to %s", logFile) 46 | if err := os.MkdirAll(filepath.Dir(logFile), 0o755); err != nil { 47 | log.Errorf("failed to make log dir: %v", err) 48 | } 49 | 50 | file, err := os.OpenFile(logFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644) 51 | if err != nil { 52 | return fmt.Errorf("failed to open file: %w", err) 53 | } 54 | defer file.Close() 55 | 56 | select { 57 | case <-ctx.Done(): 58 | return fmt.Errorf("operation canceled: %w", ctx.Err()) 59 | default: 60 | if _, err := file.Write(content); err != nil { 61 | return fmt.Errorf("failed to write to file: %w", err) 62 | } 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (l *LocalStorage) Read(ctx context.Context, path string) ([]byte, error) { 69 | filePath := filepath.Join(l.rootDir, path, DefaultLogName) 70 | if _, err := os.Stat(filePath); os.IsNotExist(err) { 71 | return nil, fmt.Errorf("file does not exist: %s", filePath) 72 | } 73 | 74 | select { 75 | case <-ctx.Done(): 76 | return nil, fmt.Errorf("operation canceled: %w", ctx.Err()) 77 | default: 78 | data, err := os.ReadFile(filePath) 79 | if err != nil { 80 | return nil, fmt.Errorf("error reading file: %w", err) 81 | } 82 | return data, nil 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /internal/storage/git.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package storage 18 | 19 | import ( 20 | "context" 21 | ) 22 | 23 | type GitStorage struct{} 24 | 25 | func NewGitStorage() (Storage, error) { 26 | return &GitStorage{}, nil 27 | } 28 | 29 | func (g *GitStorage) Write(ctx context.Context, key string, content []byte) error { 30 | return nil 31 | } 32 | 33 | func (g *GitStorage) Read(ctx context.Context, key string) ([]byte, error) { 34 | return nil, nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/storage/s3.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package storage 18 | 19 | import ( 20 | "bytes" 21 | "context" 22 | "encoding/json" 23 | "errors" 24 | "fmt" 25 | "io" 26 | "os" 27 | "path/filepath" 28 | 29 | "github.com/aws/aws-sdk-go-v2/aws" 30 | "github.com/aws/aws-sdk-go-v2/credentials" 31 | "github.com/aws/aws-sdk-go-v2/service/s3" 32 | "github.com/aws/smithy-go" 33 | ) 34 | 35 | var ErrObjectNotFound = errors.New("object not found") 36 | 37 | type S3Storage struct { 38 | s3 *s3.Client 39 | bucket string 40 | } 41 | type s3Credentials struct { 42 | Region string `json:"region"` 43 | Endpoint string `json:"endpoint"` 44 | Insecure bool `json:"insecure"` 45 | S3ForcePathStyle bool `json:"s3ForcePathStyle"` 46 | AccessKey string `json:"accessKey"` 47 | SecretKey string `json:"secretKey"` 48 | Bucket string `json:"bucket"` 49 | } 50 | 51 | func NewS3Storage(credFilePath string) (Storage, error) { 52 | credential, err := os.ReadFile(credFilePath) 53 | if err != nil { 54 | return nil, fmt.Errorf("failed to open S3CredentialsFile: %w", err) 55 | } 56 | s3Creds := &s3Credentials{} 57 | if err := json.Unmarshal(credential, s3Creds); err != nil { 58 | return nil, fmt.Errorf("error getting S3 credentials from JSON: %w", err) 59 | } 60 | 61 | svc := s3.NewFromConfig(aws.Config{}, func(o *s3.Options) { 62 | o.Credentials = credentials.NewStaticCredentialsProvider(s3Creds.AccessKey, s3Creds.SecretKey, "") 63 | o.Region = s3Creds.Region 64 | o.BaseEndpoint = &s3Creds.Endpoint 65 | o.UsePathStyle = s3Creds.S3ForcePathStyle 66 | }) 67 | return &S3Storage{ 68 | s3: svc, 69 | bucket: s3Creds.Bucket, 70 | }, nil 71 | } 72 | 73 | func (s *S3Storage) Write(ctx context.Context, key string, content []byte) error { 74 | reader := bytes.NewReader(content) 75 | objectKey := filepath.Join(key, DefaultLogName) 76 | _, err := s.s3.PutObject(ctx, &s3.PutObjectInput{ 77 | Bucket: aws.String(s.bucket), 78 | Key: aws.String(objectKey), 79 | Body: reader, 80 | }) 81 | if err != nil { 82 | return fmt.Errorf("failed to upload file: %w", err) 83 | } 84 | return nil 85 | } 86 | 87 | func (s *S3Storage) Read(ctx context.Context, key string) ([]byte, error) { 88 | objectKey := filepath.Join(key, DefaultLogName) 89 | result, err := s.s3.GetObject(ctx, &s3.GetObjectInput{ 90 | Bucket: aws.String(s.bucket), 91 | Key: aws.String(objectKey), 92 | }) 93 | if err != nil { 94 | var apiErr smithy.APIError 95 | if errors.As(err, &apiErr) { 96 | if apiErr.ErrorCode() == "NoSuchKey" { 97 | return nil, ErrObjectNotFound 98 | } 99 | } 100 | return nil, fmt.Errorf("failed to download file from s3 bucket: %w", err) 101 | } 102 | defer result.Body.Close() 103 | 104 | output, err := io.ReadAll(result.Body) 105 | if err != nil { 106 | return nil, fmt.Errorf("failed to read file from s3 result body: %w", err) 107 | } 108 | return output, nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/storage/storage.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package storage 18 | 19 | import "context" 20 | 21 | type Storage interface { 22 | // Write writes the content to the specified key. 23 | Write(ctx context.Context, key string, content []byte) error 24 | 25 | // Read reads the content from the specified key. 26 | Read(ctx context.Context, key string) ([]byte, error) 27 | } 28 | 29 | const ( 30 | DefaultLogName = "log.txt" 31 | ) 32 | -------------------------------------------------------------------------------- /internal/storage/storage_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package storage_test 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "testing" 24 | 25 | "github.com/qiniu/reviewbot/internal/storage" 26 | "github.com/stretchr/testify/assert" 27 | "github.com/stretchr/testify/require" 28 | ) 29 | 30 | func TestFileStorage(t *testing.T) { 31 | tempDir, err := os.MkdirTemp("", "file_storage_test") 32 | require.NoError(t, err) 33 | defer os.RemoveAll(tempDir) 34 | fs, err := storage.NewLocalStorage(tempDir) 35 | require.NoError(t, err) 36 | 37 | t.Run("Write and Read", func(t *testing.T) { 38 | ctx := context.Background() 39 | content := []byte("测试内容") 40 | path := "test.txt" 41 | err := fs.Write(ctx, path, content) 42 | require.NoError(t, err) 43 | readContent, err := fs.Read(ctx, path) 44 | require.NoError(t, err) 45 | assert.Equal(t, content, readContent) 46 | }) 47 | 48 | t.Run("Read Non-existent File", func(t *testing.T) { 49 | ctx := context.Background() 50 | _, err := fs.Read(ctx, "non_existent.txt") 51 | require.Error(t, err) 52 | }) 53 | 54 | t.Run("Write to Non-existent Directory", func(t *testing.T) { 55 | ctx := context.Background() 56 | content := []byte("测试内容") 57 | path := filepath.Join("non", "existent", "dir", "test.txt") 58 | 59 | err := fs.Write(ctx, path, content) 60 | require.NoError(t, err) 61 | 62 | readContent, err := fs.Read(ctx, path) 63 | require.NoError(t, err) 64 | require.Equal(t, content, readContent) 65 | }) 66 | 67 | t.Run("Overwrite Existing File", func(t *testing.T) { 68 | ctx := context.Background() 69 | path := "overwrite.txt" 70 | err := fs.Write(ctx, path, []byte("原始内容")) 71 | require.NoError(t, err) 72 | 73 | newContent := []byte("新内容") 74 | err = fs.Write(ctx, path, newContent) 75 | require.NoError(t, err) 76 | 77 | readContent, err := fs.Read(ctx, path) 78 | require.NoError(t, err) 79 | require.Equal(t, newContent, readContent) 80 | }) 81 | } 82 | -------------------------------------------------------------------------------- /internal/util/util.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package util 18 | 19 | import ( 20 | "context" 21 | "os" 22 | "path/filepath" 23 | "strings" 24 | 25 | "github.com/qiniu/x/log" 26 | "github.com/qiniu/x/xlog" 27 | ) 28 | 29 | // LimitJoin joins the strings in str with a newline separator until the length of the result is greater than length. 30 | func LimitJoin(str []string, length int) string { 31 | var result string 32 | for _, s := range str { 33 | if strings.TrimSpace(s) == "" { 34 | continue 35 | } 36 | 37 | if len(result)+len(s) > length { 38 | break 39 | } 40 | 41 | result += s + "\n" 42 | } 43 | 44 | return result 45 | } 46 | 47 | func FileExists(path string) (absPath string, exist bool) { 48 | fileAbs, err := filepath.Abs(path) 49 | if err != nil { 50 | log.Warnf("failed to get absolute path of %s: %v", path, err) 51 | return "", false 52 | } 53 | 54 | _, err = os.Stat(fileAbs) 55 | if err != nil { 56 | return "", false 57 | } 58 | 59 | return fileAbs, true 60 | } 61 | 62 | type contextKey string 63 | 64 | // EventGUIDKey is the key for the event GUID in the context. 65 | const EventGUIDKey contextKey = "event_guid" 66 | 67 | // util.FromContext returns a logger from the context. 68 | func FromContext(ctx context.Context) *xlog.Logger { 69 | eventGUID, ok := ctx.Value(EventGUIDKey).(string) 70 | if !ok { 71 | return xlog.New("default") 72 | } 73 | return xlog.New(eventGUID) 74 | } 75 | 76 | // GetEventGUID returns the event GUID from the context. 77 | func GetEventGUID(ctx context.Context) string { 78 | eventGUID, ok := ctx.Value(EventGUIDKey).(string) 79 | if !ok { 80 | return "default" 81 | } 82 | return eventGUID 83 | } 84 | -------------------------------------------------------------------------------- /internal/version/version.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2024 Qiniu Cloud (qiniu.com). 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package version 18 | 19 | import ( 20 | "fmt" 21 | "runtime/debug" 22 | ) 23 | 24 | var ( 25 | defaultVersion = "UNSTABLE" 26 | version = "" 27 | ) 28 | 29 | func Version() string { 30 | if version != "" && version != defaultVersion { 31 | return version 32 | } 33 | 34 | info, ok := debug.ReadBuildInfo() 35 | if !ok { 36 | fmt.Println(ok) 37 | return defaultVersion 38 | } 39 | 40 | if info.Main.Version == "(devel)" { 41 | fmt.Println(info.Main.Version) 42 | return defaultVersion 43 | } 44 | 45 | return info.Main.Version 46 | } 47 | -------------------------------------------------------------------------------- /kustomization.yml: -------------------------------------------------------------------------------- 1 | resources: 2 | - deploy/reviewbot.yaml 3 | images: 4 | - name: aslan-spock-register.qiniu.io/qa/reviewbot:latest 5 | newName: aslan-spock-register.qiniu.io/qa/reviewbot 6 | newTag: latest 7 | 8 | -------------------------------------------------------------------------------- /tools/linterstars/100stars.txt: -------------------------------------------------------------------------------- 1 | errcheckstars: 2365 2 | gosimplestars: 6258 3 | ineffassignstars: 403 4 | unusedstars: 6258 5 | bodyclosestars: 314 6 | depguardstars: 151 7 | duplstars: 346 8 | errorlintstars: 252 9 | exhaustivestars: 300 10 | exhaustructstars: 129 11 | forbidigostars: 126 12 | gcistars: 445 13 | gochecknoglobalsstars: 105 14 | gocognitstars: 360 15 | goconststars: 295 16 | gocriticstars: 1871 17 | gocyclostars: 1388 18 | gofumptstars: 3390 19 | gosecstars: 7908 20 | misspellstars: 1354 21 | mndstars: 192 22 | nakedretstars: 127 23 | noctxstars: 185 24 | nolintlintstars: 15912 25 | preallocstars: 641 26 | revivestars: 4846 27 | sloglintstars: 111 28 | stylecheckstars: 6258 29 | testifylintstars: 109 30 | unconvertstars: 380 31 | unparamstars: 533 32 | wrapcheckstars: 310 33 | wslstars: 267 34 | exportlooprefstars: 121 -------------------------------------------------------------------------------- /tools/linterstars/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/qiniu/reviewbot/tools/ghstars 2 | 3 | go 1.23 4 | 5 | toolchain go1.23.4 6 | 7 | require github.com/PuerkitoBio/goquery v1.10.0 8 | 9 | require ( 10 | github.com/andybalholm/cascadia v1.3.2 // indirect 11 | golang.org/x/net v0.29.0 // indirect 12 | ) 13 | -------------------------------------------------------------------------------- /tools/linterstars/go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= 2 | github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= 3 | github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= 4 | github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= 5 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 6 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 7 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 8 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 9 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 10 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 11 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 12 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 13 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 14 | golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= 15 | golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= 16 | golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= 17 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 19 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 20 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 21 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 22 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 23 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 24 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 25 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 26 | golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 27 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 28 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 29 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 30 | golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= 31 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 32 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 33 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 34 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 35 | golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 36 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 37 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 38 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 39 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 40 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 41 | -------------------------------------------------------------------------------- /tools/linterstars/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net/http" 8 | "net/url" 9 | "os" 10 | "regexp" 11 | "strings" 12 | "time" 13 | 14 | "github.com/PuerkitoBio/goquery" 15 | ) 16 | 17 | type Linter struct { 18 | Name string 19 | RepoURL string 20 | Stars int 21 | } 22 | 23 | type GithubResponse struct { 24 | StargazersCount int `json:"stargazers_count"` 25 | } 26 | 27 | type GitLabResponse struct { 28 | StarCount int `json:"star_count"` 29 | } 30 | 31 | func main() { 32 | // 获取 golangci-lint 文档页面 33 | doc, err := fetchDocument("https://golangci-lint.run/usage/linters/") 34 | if err != nil { 35 | log.Fatal(err) 36 | } 37 | 38 | linters := extractLinters(doc) 39 | 40 | // 获取每个仓库的 star 数 41 | for i := range linters { 42 | stars, err := getStars(linters[i].RepoURL) 43 | if err != nil { 44 | // still note the linter 45 | stars = 0 46 | } 47 | linters[i].Stars = stars 48 | // GitHub API 限制,添加延时 49 | time.Sleep(time.Second * 2) 50 | log.Printf("linter: %v, stars: %d, repo: %s", linters[i].Name, linters[i].Stars, linters[i].RepoURL) 51 | } 52 | } 53 | 54 | func fetchDocument(url string) (*goquery.Document, error) { 55 | resp, err := http.Get(url) 56 | if err != nil { 57 | return nil, err 58 | } 59 | defer resp.Body.Close() 60 | 61 | return goquery.NewDocumentFromReader(resp.Body) 62 | } 63 | 64 | func extractLinters(doc *goquery.Document) []Linter { 65 | var linters []Linter 66 | 67 | doc.Find("table tbody tr").Each(func(i int, s *goquery.Selection) { 68 | // 获取第一个 td 中的 linter 名称 69 | nameCell := s.Find("td").First() 70 | name := strings.TrimSpace(nameCell.Find("a").First().Text()) 71 | if name == "" { 72 | return 73 | } 74 | 75 | // 在同一个 td 中查找 GitHub 链接 76 | repoURL := "" 77 | nameCell.Find("a[href*='http']").Each(func(i int, link *goquery.Selection) { 78 | href, exists := link.Attr("href") 79 | if exists { 80 | repoURL = href 81 | } 82 | }) 83 | 84 | linters = append(linters, Linter{ 85 | Name: name, 86 | RepoURL: repoURL, 87 | }) 88 | }) 89 | 90 | return linters 91 | } 92 | 93 | func getStars(repoURL string) (int, error) { 94 | if strings.Contains(repoURL, "github.com") { 95 | return getGitHubStars(repoURL) 96 | } else if strings.Contains(repoURL, "gitlab.com") { 97 | return getGitLabStars(repoURL) 98 | } 99 | return 0, fmt.Errorf("not a github or gitlab URL") 100 | } 101 | 102 | func getGitHubStars(repoURL string) (int, error) { 103 | re := regexp.MustCompile(`github\.com/([^/]+/[^/]+)`) 104 | matches := re.FindStringSubmatch(repoURL) 105 | if len(matches) < 2 { 106 | return 0, fmt.Errorf("invalid github URL format") 107 | } 108 | 109 | apiURL := fmt.Sprintf("https://api.github.com/repos/%s", matches[1]) 110 | req, err := http.NewRequest("GET", apiURL, nil) 111 | if err != nil { 112 | return 0, err 113 | } 114 | 115 | // 添加 GitHub token 支持 116 | if token := os.Getenv("GITHUB_TOKEN"); token != "" { 117 | req.Header.Add("Authorization", "Bearer "+token) 118 | } 119 | 120 | resp, err := http.DefaultClient.Do(req) 121 | if err != nil { 122 | return 0, err 123 | } 124 | defer resp.Body.Close() 125 | 126 | var result GithubResponse 127 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 128 | return 0, err 129 | } 130 | 131 | return result.StargazersCount, nil 132 | } 133 | 134 | func getGitLabStars(repoURL string) (int, error) { 135 | re := regexp.MustCompile(`gitlab\.com/([^/]+/[^/]+)`) 136 | matches := re.FindStringSubmatch(repoURL) 137 | if len(matches) < 2 { 138 | return 0, fmt.Errorf("invalid gitlab URL format") 139 | } 140 | 141 | // GitLab API 需要项目路径进行 URL 编码 142 | projectPath := url.QueryEscape(matches[1]) 143 | apiURL := fmt.Sprintf("https://gitlab.com/api/v4/projects/%s", projectPath) 144 | 145 | resp, err := http.Get(apiURL) 146 | if err != nil { 147 | return 0, err 148 | } 149 | defer resp.Body.Close() 150 | 151 | var result GitLabResponse 152 | if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { 153 | return 0, err 154 | } 155 | 156 | return result.StarCount, nil 157 | } 158 | -------------------------------------------------------------------------------- /tools/phony/README.md: -------------------------------------------------------------------------------- 1 | # phony 2 | 3 | 模拟发送 Github 事件,主要参考 [phony](https://github.com/kubernetes-sigs/prow/tree/main/cmd/phony) 4 | 5 | ## 使用例子 6 | 7 | 在当前目录下执行: 8 | 9 | ```bash 10 | go run . --hmac= -payload ./pr-open.json --event=pull_request 11 | ``` 12 | -------------------------------------------------------------------------------- /tools/phony/main.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package main 18 | 19 | import ( 20 | "bytes" 21 | "crypto/hmac" 22 | "crypto/sha1" 23 | "encoding/hex" 24 | "flag" 25 | "fmt" 26 | "io" 27 | "net/http" 28 | "os" 29 | 30 | "github.com/qiniu/x/log" 31 | ) 32 | 33 | var ( 34 | address = flag.String("address", "http://localhost:8888/hook", "Where to send the fake hook.") 35 | hmacS = flag.String("hmac", "abcde12345", "HMAC token to sign payload with.") 36 | event = flag.String("event", "ping", "Type of event to send, such as pull_request.") 37 | payload = flag.String("payload", "", "File to send as payload. If unspecified, sends \"{}\".") 38 | platform = flag.String("platform", "github", "Type of webhook to send: github or gitlab") 39 | ) 40 | 41 | func main() { 42 | flag.Parse() 43 | 44 | var body []byte 45 | if *payload == "" { 46 | body = []byte("{}") 47 | } else { 48 | d, err := os.ReadFile(*payload) 49 | if err != nil { 50 | log.Fatal("Could not read payload file.", err) 51 | } 52 | body = d 53 | } 54 | 55 | if err := SendHook(*address, *event, body, []byte(*hmacS), *platform); err != nil { 56 | log.Errorf("Error sending hook. err: %v", err) 57 | } else { 58 | log.Info("Hook sent.") 59 | } 60 | } 61 | 62 | // SendHook sends a GitHub event of type eventType to the provided address. 63 | func SendHook(address, eventType string, payload, hmac []byte, platform string) error { 64 | req, err := http.NewRequest(http.MethodPost, address, bytes.NewBuffer(payload)) 65 | if err != nil { 66 | return err 67 | } 68 | switch platform { 69 | case "github": 70 | req.Header.Set("X-GitHub-Event", eventType) 71 | req.Header.Set("X-GitHub-Delivery", "GUID") 72 | req.Header.Set("X-Hub-Signature", PayloadSignature(payload, hmac)) 73 | case "gitlab": 74 | req.Header.Set("X-Gitlab-Event", eventType) 75 | req.Header.Set("X-Gitlab-Delivery", "GUID") 76 | req.Header.Set("X-Gitlab-Token", string(hmac)) 77 | default: 78 | return fmt.Errorf("unknown platform: %s", platform) 79 | } 80 | req.Header.Set("content-type", "application/json") 81 | 82 | c := &http.Client{} 83 | resp, err := c.Do(req) 84 | if err != nil { 85 | return err 86 | } 87 | defer resp.Body.Close() 88 | rb, err := io.ReadAll(resp.Body) 89 | if err != nil { 90 | return err 91 | } 92 | if resp.StatusCode != 200 { 93 | return fmt.Errorf("response from hook has status %d and body %s", resp.StatusCode, string(bytes.TrimSpace(rb))) 94 | } 95 | return nil 96 | } 97 | 98 | // PayloadSignature returns the signature that matches the payload. 99 | func PayloadSignature(payload []byte, key []byte) string { 100 | mac := hmac.New(sha1.New, key) 101 | mac.Write(payload) 102 | sum := mac.Sum(nil) 103 | return "sha1=" + hex.EncodeToString(sum) 104 | } 105 | --------------------------------------------------------------------------------