├── .dockerignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── deploy-to-cloud.yaml │ ├── publish.yaml │ └── test.yaml ├── .gitignore ├── BUILDING ├── BUILDING_OSS ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── NOTICE ├── README.md ├── cmd ├── cli │ ├── config.go │ ├── config_get.go │ ├── deployment.go │ ├── deployment_create.go │ ├── deployment_create_test.go │ ├── deployment_get.go │ ├── deployment_list.go │ ├── deployment_update.go │ ├── deploymentstatus.go │ ├── deploymentstatus_create.go │ ├── deploymentstatus_list.go │ ├── main.go │ ├── repo.go │ ├── repo_get.go │ ├── repo_list.go │ ├── repo_update.go │ └── shared.go ├── license │ └── main.go └── server │ ├── config.go │ ├── db.go │ ├── db_oss.go │ └── main.go ├── deploy.yml ├── go.mod ├── go.sum ├── images ├── gitploy-v3.gif └── logo.png ├── internal ├── interactor │ ├── _mock.sh │ ├── config.go │ ├── config_test.go │ ├── deployment.go │ ├── deployment_test.go │ ├── deploymentstatistics.go │ ├── deploymentstatistics_test.go │ ├── event.go │ ├── interactor.go │ ├── interactor_test.go │ ├── interface.go │ ├── license.go │ ├── license_oss.go │ ├── license_test.go │ ├── license_type.go │ ├── lock.go │ ├── mock │ │ └── pkg.go │ ├── perm.go │ ├── perm_test.go │ ├── repo.go │ ├── repo_test.go │ ├── review.go │ ├── review_test.go │ ├── shared.go │ ├── user.go │ ├── user_test.go │ ├── validator.go │ └── validator_test.go ├── pkg │ ├── github │ │ ├── deployment.go │ │ ├── deploymentstatus.go │ │ ├── github.go │ │ ├── github_test.go │ │ ├── link.go │ │ ├── link_test.go │ │ ├── mapper.go │ │ ├── repos.go │ │ ├── repos_test.go │ │ ├── sync.go │ │ ├── testdata │ │ │ └── repo.get.json │ │ ├── user.go │ │ └── web.go │ └── store │ │ ├── chat_user.go │ │ ├── deployment.go │ │ ├── deployment_test.go │ │ ├── deploymentstatistics.go │ │ ├── deploymentstatistics_test.go │ │ ├── deploymentstatus.go │ │ ├── event.go │ │ ├── event_test.go │ │ ├── lock.go │ │ ├── lock_test.go │ │ ├── perm.go │ │ ├── perm_test.go │ │ ├── repo.go │ │ ├── repo_test.go │ │ ├── review.go │ │ ├── review_test.go │ │ ├── store.go │ │ ├── user.go │ │ └── user_test.go └── server │ ├── api │ ├── shared │ │ ├── interface.go │ │ ├── middleware.go │ │ ├── middleware_test.go │ │ └── mock │ │ │ └── interactor.go │ └── v1 │ │ ├── license │ │ ├── interface.go │ │ └── license.go │ │ ├── repos │ │ ├── api.go │ │ ├── branch.go │ │ ├── branch_get.go │ │ ├── branch_list.go │ │ ├── commit.go │ │ ├── commit_get.go │ │ ├── commit_list.go │ │ ├── commit_status_list.go │ │ ├── config.go │ │ ├── config_get.go │ │ ├── deployment.go │ │ ├── deployment_change.go │ │ ├── deployment_change_test.go │ │ ├── deployment_create.go │ │ ├── deployment_create_test.go │ │ ├── deployment_get.go │ │ ├── deployment_list.go │ │ ├── deployment_rollback.go │ │ ├── deployment_rollback_test.go │ │ ├── deployment_status.go │ │ ├── deployment_status_create.go │ │ ├── deployment_status_list.go │ │ ├── deployment_update.go │ │ ├── interface.go │ │ ├── lock.go │ │ ├── lock_create.go │ │ ├── lock_create_test.go │ │ ├── lock_delete.go │ │ ├── lock_delete_test.go │ │ ├── lock_list.go │ │ ├── lock_oss.go │ │ ├── lock_update.go │ │ ├── lock_update_test.go │ │ ├── middleware.go │ │ ├── middleware_test.go │ │ ├── mock │ │ │ └── interactor.go │ │ ├── perm.go │ │ ├── perm_list.go │ │ ├── repo.go │ │ ├── repo_get.go │ │ ├── repo_list.go │ │ ├── repo_update.go │ │ ├── repo_update_test.go │ │ ├── review.go │ │ ├── review_get.go │ │ ├── review_list.go │ │ ├── review_oss.go │ │ ├── review_update.go │ │ ├── review_update_test.go │ │ ├── shared.go │ │ ├── tag.go │ │ ├── tag_get.go │ │ └── tag_list.go │ │ ├── search │ │ ├── interface.go │ │ └── search.go │ │ ├── stream │ │ ├── events.go │ │ ├── events_oss.go │ │ ├── interface.go │ │ └── stream.go │ │ ├── sync │ │ ├── interface.go │ │ ├── mock │ │ │ └── interactor.go │ │ ├── syncher.go │ │ └── syncher_test.go │ │ └── users │ │ ├── delete.go │ │ ├── get.go │ │ ├── get_test.go │ │ ├── interface.go │ │ ├── list.go │ │ ├── middleware.go │ │ ├── mock │ │ └── interactor.go │ │ ├── rate_limit.go │ │ ├── shared.go │ │ ├── update.go │ │ └── update_test.go │ ├── global │ ├── helper.go │ ├── http.go │ ├── interface.go │ ├── keys.go │ └── middleware.go │ ├── hooks │ ├── hook.go │ ├── hook_test.go │ ├── interface.go │ ├── mock │ │ └── interactor.go │ └── testdata │ │ ├── github.deployment_status.json │ │ └── github.push.json │ ├── metrics │ ├── interface.go │ ├── metrics.go │ ├── metrics_oss.go │ ├── middleware.go │ └── types.go │ ├── router.go │ ├── slack │ ├── interface.go │ ├── mock │ │ └── interactor.go │ ├── notification.go │ ├── oauth.go │ ├── slack.go │ ├── slack_oss.go │ └── types.go │ └── web │ ├── index.go │ ├── interface.go │ └── link.go ├── model ├── ent │ ├── chatuser.go │ ├── chatuser │ │ ├── chatuser.go │ │ └── where.go │ ├── chatuser_create.go │ ├── chatuser_delete.go │ ├── chatuser_query.go │ ├── chatuser_update.go │ ├── client.go │ ├── config.go │ ├── context.go │ ├── custom_client.go │ ├── custom_deployment.go │ ├── custom_repo.go │ ├── custom_tx.go │ ├── deployment.go │ ├── deployment │ │ ├── deployment.go │ │ └── where.go │ ├── deployment_create.go │ ├── deployment_delete.go │ ├── deployment_query.go │ ├── deployment_update.go │ ├── deploymentstatistics.go │ ├── deploymentstatistics │ │ ├── deploymentstatistics.go │ │ └── where.go │ ├── deploymentstatistics_create.go │ ├── deploymentstatistics_delete.go │ ├── deploymentstatistics_query.go │ ├── deploymentstatistics_update.go │ ├── deploymentstatus.go │ ├── deploymentstatus │ │ ├── deploymentstatus.go │ │ └── where.go │ ├── deploymentstatus_create.go │ ├── deploymentstatus_delete.go │ ├── deploymentstatus_query.go │ ├── deploymentstatus_update.go │ ├── ent.go │ ├── entc.go │ ├── enttest │ │ └── enttest.go │ ├── event.go │ ├── event │ │ ├── event.go │ │ └── where.go │ ├── event_create.go │ ├── event_delete.go │ ├── event_query.go │ ├── event_update.go │ ├── generate.go │ ├── hook │ │ └── hook.go │ ├── lock.go │ ├── lock │ │ ├── lock.go │ │ └── where.go │ ├── lock_create.go │ ├── lock_delete.go │ ├── lock_query.go │ ├── lock_update.go │ ├── migrate │ │ ├── migrate.go │ │ └── schema.go │ ├── mutation.go │ ├── notificationrecord.go │ ├── notificationrecord │ │ ├── notificationrecord.go │ │ └── where.go │ ├── notificationrecord_create.go │ ├── notificationrecord_delete.go │ ├── notificationrecord_query.go │ ├── notificationrecord_update.go │ ├── perm.go │ ├── perm │ │ ├── perm.go │ │ └── where.go │ ├── perm_create.go │ ├── perm_delete.go │ ├── perm_query.go │ ├── perm_update.go │ ├── predicate │ │ └── predicate.go │ ├── repo.go │ ├── repo │ │ ├── repo.go │ │ └── where.go │ ├── repo_create.go │ ├── repo_delete.go │ ├── repo_query.go │ ├── repo_update.go │ ├── review.go │ ├── review │ │ ├── review.go │ │ └── where.go │ ├── review_create.go │ ├── review_delete.go │ ├── review_query.go │ ├── review_update.go │ ├── runtime.go │ ├── runtime │ │ └── runtime.go │ ├── schema │ │ ├── chatuser.go │ │ ├── deployment.go │ │ ├── deploymentstatistics.go │ │ ├── deploymentstatus.go │ │ ├── event.go │ │ ├── lock.go │ │ ├── notificationrecord.go │ │ ├── perm.go │ │ ├── repo.go │ │ ├── review.go │ │ ├── shared.go │ │ └── user.go │ ├── tx.go │ ├── user.go │ ├── user │ │ ├── user.go │ │ └── where.go │ ├── user_create.go │ ├── user_delete.go │ ├── user_query.go │ └── user_update.go └── extent │ ├── branch.go │ ├── commit.go │ ├── config.go │ ├── config_test.go │ ├── deployment.go │ ├── deploymentstatus.go │ ├── env.go │ ├── env_test.go │ ├── license.go │ ├── license_test.go │ ├── ratelimit.go │ ├── repo.go │ ├── tag.go │ ├── user.go │ └── webhook.go ├── openapi └── v1 │ ├── openapi.yaml │ ├── paths │ ├── license │ │ └── index.yaml │ ├── repos │ │ ├── branch.yaml │ │ ├── branches.yaml │ │ ├── commit.yaml │ │ ├── commit_statuses.yaml │ │ ├── commits.yaml │ │ ├── config.yaml │ │ ├── deployment.yaml │ │ ├── deployment_changes.yaml │ │ ├── deployment_remote_statuses.yaml │ │ ├── deployment_review.yaml │ │ ├── deployment_reviews.yaml │ │ ├── deployment_rollback.yaml │ │ ├── deployment_statuses.yaml │ │ ├── deployments.yaml │ │ ├── index.yaml │ │ ├── lock.yaml │ │ ├── locks.yaml │ │ ├── perms.yaml │ │ ├── repo.yaml │ │ ├── tag.yaml │ │ └── tags.yaml │ ├── search │ │ ├── deployments.yaml │ │ └── reviews.yaml │ ├── stream │ │ └── events.yaml │ ├── sync │ │ └── index.yaml │ └── users │ │ ├── index.yaml │ │ ├── me.yaml │ │ ├── ratelimit.yaml │ │ └── user.yaml │ ├── responses.yaml │ └── schemas │ ├── Branch.yaml │ ├── ChatUser.yaml │ ├── Commit.yaml │ ├── Config.yaml │ ├── Deployment.yaml │ ├── DeploymentStatus.yaml │ ├── Error.yaml │ ├── License.yaml │ ├── Lock.yaml │ ├── Perm.yaml │ ├── RateLimit.yaml │ ├── RemoteDeploymentStatus.yaml │ ├── Repository.yaml │ ├── Review.yaml │ ├── Status.yaml │ ├── Tag.yaml │ └── User.yaml ├── pkg ├── api │ ├── client.go │ ├── client_test.go │ ├── config.go │ ├── config_test.go │ ├── deployment.go │ ├── deployment_status.go │ ├── deployment_status_test.go │ ├── deployment_test.go │ ├── repo.go │ ├── repo_test.go │ ├── shared.go │ └── user.go ├── e │ ├── code.go │ ├── code_test.go │ ├── trans.go │ └── trans_test.go └── license │ └── decode.go ├── release ├── values.dev.yaml └── values.production.yaml ├── scripts └── build-cli.sh ├── tools └── tools.go └── ui ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── craco.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── manifest.json ├── robots.txt └── spinner.ico ├── src ├── App.less ├── App.tsx ├── apis │ ├── _base.ts │ ├── branch.ts │ ├── chat.ts │ ├── commit.ts │ ├── config.ts │ ├── deployment.ts │ ├── events.ts │ ├── index.ts │ ├── license.ts │ ├── lock.ts │ ├── perm.ts │ ├── repo.ts │ ├── review.ts │ ├── setting.ts │ ├── sync.ts │ ├── tag.ts │ └── user.ts ├── components │ ├── ActivateButton.tsx │ ├── CreatableSelect.tsx │ ├── DeploymentRefCode.tsx │ ├── DeploymentStatusBadge.tsx │ ├── Pagination.tsx │ ├── RecentActivities.tsx │ ├── Spin.tsx │ ├── UserAvatar.tsx │ └── partials │ │ ├── deploymentStatus.tsx │ │ └── index.tsx ├── index.css ├── index.tsx ├── libs │ └── index.ts ├── logo.svg ├── models │ ├── Branch.ts │ ├── Commit.ts │ ├── Config.ts │ ├── Deployment.ts │ ├── Event.ts │ ├── License.ts │ ├── Lock.ts │ ├── Perm.ts │ ├── Repo.ts │ ├── Request.ts │ ├── Review.ts │ ├── Tag.ts │ ├── User.ts │ ├── errors.ts │ └── index.ts ├── react-app-env.d.ts ├── redux │ ├── activities.tsx │ ├── deployment.tsx │ ├── home.ts │ ├── hooks.ts │ ├── main.ts │ ├── members.ts │ ├── repo.ts │ ├── repoDeploy.tsx │ ├── repoHome.ts │ ├── repoLock.ts │ ├── repoRollback.tsx │ ├── repoSettings.tsx │ ├── settings.ts │ └── store.ts ├── reportWebVitals.ts ├── setupTests.ts └── views │ ├── activities │ ├── ActivityHistory.tsx │ ├── SearchActivities.tsx │ └── index.tsx │ ├── deployment │ ├── CommitChanges.tsx │ ├── DeploymentDescriptor.tsx │ ├── DeploymentStatusSteps.tsx │ ├── HeaderBreadcrumb.tsx │ ├── ReviewButton.tsx │ ├── ReviewList.tsx │ └── index.tsx │ ├── home │ ├── RepoList.tsx │ └── index.tsx │ ├── main │ ├── Content.tsx │ ├── Header.tsx │ ├── LicenseWarningFooter.tsx │ └── index.tsx │ ├── members │ ├── MemberList.tsx │ └── index.tsx │ ├── repo │ └── index.tsx │ ├── repoDeploy │ ├── DeployForm.tsx │ ├── DynamicPayloadModal.tsx │ ├── StatusStateIcon.tsx │ └── index.tsx │ ├── repoHome │ ├── ActivityLogs.tsx │ └── index.tsx │ ├── repoLock │ ├── LockList.tsx │ └── index.tsx │ ├── repoRollback │ ├── RollbackForm.tsx │ └── index.tsx │ ├── repoSettings │ ├── SettingsForm.tsx │ └── index.tsx │ └── settings │ ├── SlackDescriptions.tsx │ ├── UserDescriptions.tsx │ └── index.tsx └── tsconfig.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # server 2 | .editorconfig 3 | .git 4 | README.md 5 | .env 6 | sqlite3.db 7 | 8 | # ui 9 | ui/node_modules 10 | ui/build 11 | ui/dist 12 | ui/.env.development.local 13 | ui/.env -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug report 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **Logs or Screenshots** 14 | 15 | ``` 16 | Input your error logs 17 | ``` 18 | 19 |
20 | Screenshots 21 | 22 |
23 | 24 | **Additional context** 25 | Add any other context about the problem here. 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: [] 8 | # - package-ecosystem: "gomod" # See documentation for possible values 9 | # directory: "/" # Location of package manifests 10 | # schedule: 11 | # interval: "daily" 12 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | 9 | jobs: 10 | go-test: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - 14 | uses: actions/setup-go@v2 15 | with: 16 | go-version: '1.17' 17 | - 18 | uses: actions/checkout@v2 19 | - 20 | name: golangci-lint 21 | uses: golangci/golangci-lint-action@v3.1.0 22 | with: 23 | version: v1.42 24 | args: -D errcheck --timeout 2m 25 | - 26 | run: go test -cpu 4 -coverprofile .testCoverage.txt $(go list ./... | grep -v model/ent | grep -v mock) 27 | env: 28 | GIN_MODE: release 29 | 30 | react-test: 31 | runs-on: ubuntu-latest 32 | steps: 33 | - 34 | uses: actions/checkout@v2 35 | - 36 | uses: actions/setup-node@v2 37 | with: 38 | node-version: '14.17.0' 39 | cache: 'npm' 40 | cache-dependency-path: ui/package-lock.json 41 | - 42 | run: npm install 43 | working-directory: ui 44 | - 45 | run: npm run lint 46 | working-directory: ui 47 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | .testCoverage.txt 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | 15 | # Dependency directories (remove the comment below to include it) 16 | # vendor/ 17 | .env 18 | *.db 19 | -------------------------------------------------------------------------------- /BUILDING: -------------------------------------------------------------------------------- 1 | 1. Clone the repository 2 | 2. Build the Docker image: 3 | 4 | docker build -t gitploy . 5 | -------------------------------------------------------------------------------- /BUILDING_OSS: -------------------------------------------------------------------------------- 1 | 1. Clone the repository 2 | 2. Build the Docker image: 3 | 4 | docker build -t gitploy --build-arg "OSS=true" . 5 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the server binary file. 2 | FROM golang:1.17 AS server 3 | ARG OSS=false 4 | 5 | WORKDIR /server 6 | 7 | COPY go.mod go.sum ./ 8 | RUN go mod download 9 | 10 | COPY . . 11 | RUN if [ "${OSS}" = "false" ]; then \ 12 | echo "Build the enterprise edition"; \ 13 | go build -o gitploy-server ./cmd/server; \ 14 | else \ 15 | echo "Build the community edition"; \ 16 | go build -o gitploy-server -tags "oss" ./cmd/server; \ 17 | fi 18 | 19 | # Build UI. 20 | FROM node:14.17.0 AS ui 21 | ARG OSS=false 22 | 23 | WORKDIR /ui 24 | 25 | ENV PATH /ui/node_modules/.bin:$PATH 26 | 27 | COPY ./ui/package.json ./ui/package-lock.json ./ 28 | RUN npm install --silent 29 | 30 | COPY ./ui ./ 31 | ENV REACT_APP_GITPLOY_OSS="${OSS}" 32 | RUN npm run build 33 | 34 | # Copy to the final image. 35 | FROM golang:1.17-buster AS gitploy 36 | 37 | WORKDIR /app 38 | 39 | # Create DB 40 | RUN mkdir /data 41 | 42 | COPY --from=server --chown=root:root /server/LICENSE /server/NOTICE ./ 43 | COPY --from=server --chown=root:root /server/gitploy-server /go/bin/gitploy-server 44 | 45 | # Copy UI output into the assets directory. 46 | COPY --from=ui --chown=root:root /ui/build/ /app/ 47 | 48 | ENTRYPOINT [ "/go/bin/gitploy-server" ] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2024 Gitploy.io 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Gitploy 2 | Copyright 2021 Gitploy.IO, Inc 3 | 4 | This product includes software developed by Facebook, Inc. 5 | (https://entgo.io/). 6 | -------------------------------------------------------------------------------- /cmd/cli/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var configCommand = &cli.Command{ 6 | Name: "config", 7 | Usage: "Manage config.", 8 | Subcommands: []*cli.Command{ 9 | configGetCommand, 10 | }, 11 | } 12 | -------------------------------------------------------------------------------- /cmd/cli/config_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | var configGetCommand = &cli.Command{ 8 | Name: "get", 9 | Usage: "Show the pipeline configurations.", 10 | ArgsUsage: "/", 11 | Action: func(cli *cli.Context) error { 12 | ns, n, err := splitFullName(cli.Args().First()) 13 | if err != nil { 14 | return err 15 | } 16 | 17 | c := buildClient(cli) 18 | config, err := c.Config.Get(cli.Context, ns, n) 19 | if err != nil { 20 | return err 21 | } 22 | 23 | return printJson(cli, config) 24 | }, 25 | } 26 | -------------------------------------------------------------------------------- /cmd/cli/deployment.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var deploymentCommand = &cli.Command{ 6 | Name: "deployment", 7 | Aliases: []string{"d"}, 8 | Usage: "Manage deployments.", 9 | Subcommands: []*cli.Command{ 10 | deploymentListCommand, 11 | deploymentGetCommand, 12 | deploymentCreateCommand, 13 | deploymentUpdateCommand, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmd/cli/deployment_create_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "reflect" 5 | "testing" 6 | 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | ) 9 | 10 | func Test_buildDyanmicPayload(t *testing.T) { 11 | t.Run("Return an error when syntax is invalid.", func(t *testing.T) { 12 | _, err := buildDyanmicPayload([]string{ 13 | "foo", 14 | }, &extent.Env{ 15 | DynamicPayload: &extent.DynamicPayload{ 16 | Enabled: true, 17 | }, 18 | }) 19 | 20 | if err == nil { 21 | t.Fatalf("buildDyanmicPayload dosen't return an error") 22 | } 23 | }) 24 | 25 | t.Run("Return a payload with default values.", func(t *testing.T) { 26 | var qux interface{} = "qux" 27 | 28 | payload, err := buildDyanmicPayload([]string{}, &extent.Env{ 29 | DynamicPayload: &extent.DynamicPayload{ 30 | Enabled: true, 31 | Inputs: map[string]extent.Input{ 32 | "foo": { 33 | Type: extent.InputTypeString, 34 | }, 35 | "baz": { 36 | Type: extent.InputTypeString, 37 | Default: &qux, 38 | }, 39 | }, 40 | }, 41 | }) 42 | 43 | if err != nil { 44 | t.Fatalf("buildDyanmicPayload returns an error") 45 | } 46 | 47 | if expected := map[string]interface{}{ 48 | "baz": "qux", 49 | }; !reflect.DeepEqual(payload, expected) { 50 | t.Fatalf("buildDyanmicPayload = %v, wanted %v", payload, expected) 51 | } 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /cmd/cli/deployment_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var deploymentGetCommand = &cli.Command{ 10 | Name: "get", 11 | Usage: "Show the deployment", 12 | ArgsUsage: "/ ", 13 | Action: func(cli *cli.Context) error { 14 | // Validate arguments. 15 | ns, n, err := splitFullName(cli.Args().First()) 16 | if err != nil { 17 | return err 18 | } 19 | 20 | number, err := strconv.Atoi(cli.Args().Get(1)) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | c := buildClient(cli) 26 | d, err := c.Deployment.Get(cli.Context, ns, n, number) 27 | if err != nil { 28 | return err 29 | } 30 | 31 | return printJson(cli, d) 32 | }, 33 | } 34 | -------------------------------------------------------------------------------- /cmd/cli/deployment_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent/deployment" 7 | "github.com/gitploy-io/gitploy/pkg/api" 8 | ) 9 | 10 | var deploymentListCommand = &cli.Command{ 11 | Name: "list", 12 | Aliases: []string{"ls"}, 13 | Usage: "Show the deployments under the repository.", 14 | ArgsUsage: "/", 15 | Flags: []cli.Flag{ 16 | &cli.IntFlag{ 17 | Name: "page", 18 | Value: 1, 19 | Usage: "The page of list.", 20 | }, 21 | &cli.IntFlag{ 22 | Name: "per-page", 23 | Value: 30, 24 | Usage: "The item count per page.", 25 | }, 26 | &cli.StringFlag{ 27 | Name: "env", 28 | Usage: "The name of the environment. It only shows deployments for the environment.", 29 | }, 30 | &cli.StringFlag{ 31 | Name: "status", 32 | Usage: "The deployment status: 'waiting', 'created', 'queued', 'running', 'success', or 'failure'. It only shows deployments the status is matched. ", 33 | }, 34 | }, 35 | Action: func(cli *cli.Context) error { 36 | c := buildClient(cli) 37 | 38 | ns, n, err := splitFullName(cli.Args().First()) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | ds, err := c.Deployment.List(cli.Context, ns, n, &api.DeploymentListOptions{ 44 | ListOptions: api.ListOptions{Page: cli.Int("page"), PerPage: cli.Int("per-page")}, 45 | Env: cli.String("env"), 46 | Status: deployment.Status(cli.String("status")), 47 | }) 48 | if err != nil { 49 | return err 50 | } 51 | 52 | return printJson(cli, ds) 53 | }, 54 | } 55 | -------------------------------------------------------------------------------- /cmd/cli/deployment_update.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/urfave/cli/v2" 7 | ) 8 | 9 | var deploymentUpdateCommand = &cli.Command{ 10 | Name: "update", 11 | Usage: "Trigger the deployment which has approved by reviews.", 12 | ArgsUsage: "/ ", 13 | Action: func(cli *cli.Context) error { 14 | ns, n, err := splitFullName(cli.Args().First()) 15 | if err != nil { 16 | return err 17 | } 18 | 19 | number, err := strconv.Atoi(cli.Args().Get(1)) 20 | if err != nil { 21 | return err 22 | } 23 | 24 | c := buildClient(cli) 25 | d, err := c.Deployment.Update(cli.Context, ns, n, number) 26 | if err != nil { 27 | return err 28 | } 29 | 30 | return printJson(cli, d) 31 | }, 32 | } 33 | -------------------------------------------------------------------------------- /cmd/cli/deploymentstatus.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/urfave/cli/v2" 4 | 5 | var deploymentStatusCommand = &cli.Command{ 6 | Name: "deployment-status", 7 | Aliases: []string{"ds"}, 8 | Usage: "Manage deployment statuses.", 9 | Subcommands: []*cli.Command{ 10 | deploymentStatusListCommand, 11 | deploymentStatusCreateCommand, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /cmd/cli/deploymentstatus_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/urfave/cli/v2" 7 | 8 | "github.com/gitploy-io/gitploy/pkg/api" 9 | ) 10 | 11 | var deploymentStatusListCommand = &cli.Command{ 12 | Name: "list", 13 | Aliases: []string{"ls"}, 14 | Usage: "Show the deployment status under the deployment.", 15 | ArgsUsage: "/ ", 16 | Action: func(cli *cli.Context) error { 17 | ns, n, err := splitFullName(cli.Args().First()) 18 | if err != nil { 19 | return err 20 | } 21 | 22 | number, err := strconv.Atoi(cli.Args().Get(1)) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | c := buildClient(cli) 28 | dss, err := c.DeploymentStatus.List(cli.Context, ns, n, number, &api.ListOptions{}) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | return printJson(cli, dss) 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /cmd/cli/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "log" 5 | "os" 6 | 7 | "github.com/urfave/cli/v2" 8 | ) 9 | 10 | var Version = "latest" 11 | 12 | func main() { 13 | app := &cli.App{ 14 | Name: "gitploy", 15 | Usage: "Command line utility.", 16 | Version: Version, 17 | Flags: []cli.Flag{ 18 | &cli.StringFlag{ 19 | Name: "host", 20 | Aliases: []string{"H"}, 21 | Required: true, 22 | Usage: "The host of server. It must have a trailing slash (i.e., '/').", 23 | EnvVars: []string{"GITPLOY_SERVER_HOST"}, 24 | }, 25 | &cli.StringFlag{ 26 | Name: "token", 27 | Aliases: []string{"T"}, 28 | Required: true, 29 | Usage: "The authorization token.", 30 | EnvVars: []string{"GITPLOY_TOKEN"}, 31 | }, 32 | &cli.StringFlag{ 33 | Name: "query", 34 | Usage: "A GJSON query to use in filtering the response data", 35 | }, 36 | }, 37 | Commands: []*cli.Command{ 38 | repoCommand, 39 | deploymentCommand, 40 | deploymentStatusCommand, 41 | configCommand, 42 | }, 43 | } 44 | 45 | err := app.Run(os.Args) 46 | if err != nil { 47 | log.Fatal(err) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/cli/repo.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/urfave/cli/v2" 5 | ) 6 | 7 | var repoCommand *cli.Command = &cli.Command{ 8 | Name: "repo", 9 | Usage: "Manage repositories.", 10 | Subcommands: []*cli.Command{ 11 | repoListCommand, 12 | repoGetCommand, 13 | repoUpdateCommand, 14 | }, 15 | } 16 | -------------------------------------------------------------------------------- /cmd/cli/repo_get.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/tidwall/gjson" 8 | "github.com/urfave/cli/v2" 9 | ) 10 | 11 | var repoGetCommand = &cli.Command{ 12 | Name: "get", 13 | Usage: "Show the repository.", 14 | ArgsUsage: "/", 15 | Action: func(cli *cli.Context) error { 16 | ns, n, err := splitFullName(cli.Args().First()) 17 | if err != nil { 18 | return err 19 | } 20 | 21 | c := buildClient(cli) 22 | repo, err := c.Repo.Get(cli.Context, ns, n) 23 | if err != nil { 24 | return err 25 | } 26 | 27 | output, err := json.MarshalIndent(repo, "", " ") 28 | if err != nil { 29 | return fmt.Errorf("Failed to marshal: %w", err) 30 | } 31 | 32 | if q := cli.String("query"); q != "" { 33 | fmt.Println(gjson.GetBytes(output, q)) 34 | return nil 35 | } 36 | 37 | fmt.Println(string(output)) 38 | return nil 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /cmd/cli/repo_list.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | 7 | "github.com/tidwall/gjson" 8 | "github.com/urfave/cli/v2" 9 | 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | "github.com/gitploy-io/gitploy/pkg/api" 12 | ) 13 | 14 | var repoListCommand = &cli.Command{ 15 | Name: "list", 16 | Aliases: []string{"ls"}, 17 | Usage: "Show own repositories.", 18 | Flags: []cli.Flag{ 19 | &cli.BoolFlag{ 20 | Name: "all", 21 | Usage: "Show all repositories.", 22 | }, 23 | &cli.IntFlag{ 24 | Name: "page", 25 | Value: 1, 26 | Usage: "The page of list.", 27 | }, 28 | &cli.IntFlag{ 29 | Name: "per-page", 30 | Value: 30, 31 | Usage: "The item count per page.", 32 | }, 33 | }, 34 | Action: func(cli *cli.Context) error { 35 | c := buildClient(cli) 36 | 37 | var ( 38 | repos []*ent.Repo 39 | err error 40 | ) 41 | 42 | if cli.Bool("all") { 43 | if repos, err = c.Repo.ListAll(cli.Context); err != nil { 44 | return err 45 | } 46 | } else { 47 | if repos, err = c.Repo.List(cli.Context, &api.RepoListOptions{ 48 | ListOptions: api.ListOptions{Page: cli.Int("page"), PerPage: cli.Int("per-page")}, 49 | }); err != nil { 50 | return err 51 | } 52 | } 53 | 54 | output, err := json.MarshalIndent(repos, "", " ") 55 | if err != nil { 56 | return fmt.Errorf("Failed to marshal: %w", err) 57 | } 58 | 59 | if q := cli.String("query"); q != "" { 60 | fmt.Println(gjson.GetBytes(output, q)) 61 | return nil 62 | } 63 | 64 | fmt.Println(string(output)) 65 | return nil 66 | }, 67 | } 68 | -------------------------------------------------------------------------------- /cmd/cli/shared.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "strings" 7 | 8 | "github.com/tidwall/gjson" 9 | "github.com/urfave/cli/v2" 10 | "golang.org/x/oauth2" 11 | 12 | "github.com/gitploy-io/gitploy/pkg/api" 13 | ) 14 | 15 | // buildClient returns a client to interact with a server. 16 | func buildClient(cli *cli.Context) *api.Client { 17 | ts := oauth2.StaticTokenSource( 18 | &oauth2.Token{AccessToken: cli.String("token")}, 19 | ) 20 | tc := oauth2.NewClient(cli.Context, ts) 21 | 22 | return api.NewClient(cli.String("host"), tc) 23 | } 24 | 25 | // splitFullName splits the full name into namespace, and name. 26 | func splitFullName(name string) (string, string, error) { 27 | ss := strings.Split(name, "/") 28 | if len(ss) != 2 { 29 | return "", "", fmt.Errorf("'%s' is invalid repository name", name) 30 | } 31 | 32 | return ss[0], ss[1], nil 33 | } 34 | 35 | // printJson prints the object as JSON-format. 36 | func printJson(cli *cli.Context, v interface{}) error { 37 | output, err := json.MarshalIndent(v, "", " ") 38 | if err != nil { 39 | return fmt.Errorf("Failed to print JSON format: %w", err) 40 | } 41 | 42 | if query := cli.String("query"); query != "" { 43 | fmt.Println(gjson.GetBytes(output, query)) 44 | return nil 45 | } 46 | 47 | fmt.Println(string(output)) 48 | return nil 49 | } 50 | -------------------------------------------------------------------------------- /cmd/license/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "time" 9 | 10 | "github.com/gitploy-io/gitploy/model/extent" 11 | ) 12 | 13 | const ( 14 | year = 365 * 24 * time.Hour 15 | ) 16 | 17 | var ( 18 | limit = flag.Int("limit", 0, "Member count") 19 | ) 20 | 21 | func main() { 22 | flag.Parse() 23 | 24 | if *limit == 0 { 25 | log.Fatal("Set the limit.") 26 | } 27 | 28 | d := &extent.SigningData{ 29 | MemberLimit: *limit, 30 | ExpiredAt: time.Now().Add(year), 31 | } 32 | 33 | j, err := json.Marshal(d) 34 | if err != nil { 35 | log.Fatalf("It has failed to marshal.") 36 | } 37 | 38 | fmt.Print(string(j)) 39 | } 40 | -------------------------------------------------------------------------------- /cmd/server/db.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | // +build !oss 6 | 7 | package main 8 | 9 | import ( 10 | "database/sql" 11 | "fmt" 12 | 13 | "entgo.io/ent/dialect" 14 | entsql "entgo.io/ent/dialect/sql" 15 | 16 | "github.com/gitploy-io/gitploy/model/ent" 17 | ) 18 | 19 | func OpenDB(driver string, dsn string) (*ent.Client, error) { 20 | if driver == dialect.SQLite || driver == dialect.MySQL { 21 | return ent.Open(driver, dsn) 22 | } 23 | 24 | if driver == dialect.Postgres { 25 | db, err := sql.Open("pgx", dsn) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | drv := entsql.OpenDB(dialect.Postgres, db) 31 | return ent.NewClient(ent.Driver(drv)), nil 32 | } 33 | 34 | return nil, fmt.Errorf("The driver have to be one of them: sqlite3, mysql, or postgres.") 35 | } 36 | -------------------------------------------------------------------------------- /cmd/server/db_oss.go: -------------------------------------------------------------------------------- 1 | // +build oss 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | 8 | "entgo.io/ent/dialect" 9 | "github.com/gitploy-io/gitploy/model/ent" 10 | ) 11 | 12 | func OpenDB(driver string, dsn string) (*ent.Client, error) { 13 | if driver != dialect.SQLite { 14 | return nil, fmt.Errorf("The community edition support sqlite only.") 15 | } 16 | 17 | return ent.Open(driver, dsn) 18 | } 19 | -------------------------------------------------------------------------------- /deploy.yml: -------------------------------------------------------------------------------- 1 | envs: 2 | - name: dev 3 | task: ${GITPLOY_DEPLOY_TASK:=rollback}:kubernetes 4 | auto_merge: false 5 | required_contexts: 6 | - "publish-image" 7 | deployable_ref: 'v.*\..*\..*' 8 | serialization: true 9 | dynamic_payload: 10 | enabled: true 11 | inputs: 12 | pullPolicy: 13 | required: true 14 | type: select 15 | description: Image pull policy 16 | options: 17 | - Always 18 | - IfNotPresent 19 | default: Always 20 | 21 | - name: production 22 | task: ${GITPLOY_DEPLOY_TASK:=rollback}:kubernetes 23 | auto_merge: false 24 | required_contexts: 25 | - "publish-image" 26 | production_environment: true 27 | review: 28 | enabled: false 29 | reviewers: 30 | - hanjunlee 31 | - gitploy-qa 32 | deployable_ref: 'v.*\..*\..*' 33 | serialization: true 34 | dynamic_payload: 35 | enabled: true 36 | inputs: 37 | pullPolicy: 38 | required: true 39 | type: select 40 | description: Image pull policy 41 | options: 42 | - Always 43 | - IfNotPresent 44 | default: Always 45 | -------------------------------------------------------------------------------- /images/gitploy-v3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitploy-io/gitploy/37643d4a4ef018a6b19075202a37087cef0c3f8e/images/gitploy-v3.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitploy-io/gitploy/37643d4a4ef018a6b19075202a37087cef0c3f8e/images/logo.png -------------------------------------------------------------------------------- /internal/interactor/_mock.sh: -------------------------------------------------------------------------------- 1 | mockgen \ 2 | -aux_files \ 3 | github.com/gitploy-io/gitploy/internal/interactor=user.go\ 4 | ,github.com/gitploy-io/gitploy/internal/interactor=repo.go\ 5 | ,github.com/gitploy-io/gitploy/internal/interactor=perm.go\ 6 | ,github.com/gitploy-io/gitploy/internal/interactor=config.go\ 7 | ,github.com/gitploy-io/gitploy/internal/interactor=deployment.go\ 8 | ,github.com/gitploy-io/gitploy/internal/interactor=deploymentstatistics.go\ 9 | ,github.com/gitploy-io/gitploy/internal/interactor=lock.go\ 10 | ,github.com/gitploy-io/gitploy/internal/interactor=event.go\ 11 | ,github.com/gitploy-io/gitploy/internal/interactor=review.go\ 12 | -source ./interface.go \ 13 | -package mock \ 14 | -destination ./mock/pkg.go -------------------------------------------------------------------------------- /internal/interactor/config.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | ) 9 | 10 | type ( 11 | ConfigInteractor service 12 | 13 | ConfigSCM interface { 14 | GetConfig(ctx context.Context, u *ent.User, r *ent.Repo) (*extent.Config, error) 15 | GetConfigRedirectURL(ctx context.Context, u *ent.User, r *ent.Repo) (string, error) 16 | GetNewConfigRedirectURL(ctx context.Context, u *ent.User, r *ent.Repo) (string, error) 17 | } 18 | ) 19 | 20 | // GetEvaluatedConfig returns the config after evaluating the variables. 21 | func (i *ConfigInteractor) GetEvaluatedConfig(ctx context.Context, u *ent.User, r *ent.Repo, v *extent.EvalValues) (*extent.Config, error) { 22 | config, err := i.scm.GetConfig(ctx, u, r) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | if err = config.Eval(v); err != nil { 28 | return nil, err 29 | } 30 | 31 | return config, nil 32 | } 33 | -------------------------------------------------------------------------------- /internal/interactor/config_test.go: -------------------------------------------------------------------------------- 1 | package interactor_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | 9 | i "github.com/gitploy-io/gitploy/internal/interactor" 10 | "github.com/gitploy-io/gitploy/internal/interactor/mock" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | "github.com/gitploy-io/gitploy/model/extent" 13 | ) 14 | 15 | func TestConfigInteractor_GetEvaluatedConfig(t *testing.T) { 16 | t.Run("Return the evaluated config", func(t *testing.T) { 17 | t.Log("Start mocking: ") 18 | ctrl := gomock.NewController(t) 19 | scm := mock.NewMockSCM(ctrl) 20 | 21 | t.Log("\tGet the config.") 22 | scm.EXPECT(). 23 | GetConfig(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{})). 24 | Return(&extent.Config{}, nil) 25 | 26 | it := i.NewInteractor(&i.InteractorConfig{ 27 | Store: mock.NewMockStore(ctrl), 28 | SCM: scm, 29 | }) 30 | 31 | _, err := it.GetEvaluatedConfig(context.Background(), &ent.User{}, &ent.Repo{}, &extent.EvalValues{}) 32 | if err != nil { 33 | t.Fatalf("GetEvaluatedConfig returns an error: %v", err) 34 | } 35 | }) 36 | } 37 | -------------------------------------------------------------------------------- /internal/interactor/license.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package interactor 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/gitploy-io/gitploy/model/extent" 13 | "github.com/gitploy-io/gitploy/pkg/e" 14 | "github.com/gitploy-io/gitploy/pkg/license" 15 | ) 16 | 17 | func (i *LicenseInteractor) GetLicense(ctx context.Context) (*extent.License, error) { 18 | var ( 19 | memberCnt int 20 | deploymentCnt int 21 | d *extent.SigningData 22 | err error 23 | ) 24 | 25 | if memberCnt, err = i.store.CountUsers(ctx); err != nil { 26 | return nil, err 27 | } 28 | 29 | if deploymentCnt, err = i.store.CountDeployments(ctx); err != nil { 30 | return nil, err 31 | } 32 | 33 | if i.LicenseKey == "" { 34 | lic := extent.NewTrialLicense(memberCnt, deploymentCnt) 35 | return lic, nil 36 | } 37 | 38 | if d, err = license.Decode(i.LicenseKey); err != nil { 39 | return nil, e.NewError( 40 | e.ErrorCodeLicenseDecode, 41 | err, 42 | ) 43 | } 44 | 45 | lic := extent.NewStandardLicense(memberCnt, d) 46 | return lic, nil 47 | } 48 | -------------------------------------------------------------------------------- /internal/interactor/license_oss.go: -------------------------------------------------------------------------------- 1 | //go:build oss 2 | 3 | package interactor 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/gitploy-io/gitploy/model/extent" 9 | ) 10 | 11 | func (i *LicenseInteractor) GetLicense(ctx context.Context) (*extent.License, error) { 12 | return extent.NewOSSLicense(), nil 13 | } 14 | -------------------------------------------------------------------------------- /internal/interactor/license_test.go: -------------------------------------------------------------------------------- 1 | package interactor_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | i "github.com/gitploy-io/gitploy/internal/interactor" 8 | "github.com/gitploy-io/gitploy/internal/interactor/mock" 9 | "github.com/gitploy-io/gitploy/model/extent" 10 | "github.com/golang/mock/gomock" 11 | ) 12 | 13 | func TestInteractor_GetLicense(t *testing.T) { 14 | t.Run("Return the trial license when the signing data is nil.", func(t *testing.T) { 15 | ctrl := gomock.NewController(t) 16 | store := mock.NewMockStore(ctrl) 17 | 18 | t.Log("MOCK - return the count of users.") 19 | store. 20 | EXPECT(). 21 | CountUsers(gomock.AssignableToTypeOf(context.Background())). 22 | Return(extent.TrialMemberLimit, nil) 23 | 24 | store. 25 | EXPECT(). 26 | CountDeployments(gomock.AssignableToTypeOf(context.Background())). 27 | Return(extent.TrialDeploymentLimit, nil) 28 | 29 | it := i.NewInteractor(&i.InteractorConfig{ 30 | Store: store, 31 | }) 32 | 33 | lic, err := it.GetLicense(context.Background()) 34 | if err != nil { 35 | t.Fatalf("GetLicense returns an error: %s", err) 36 | } 37 | 38 | if !lic.IsTrial() { 39 | t.Fatalf("GetLicense = %v, wanted %v", lic.Kind, extent.LicenseKindTrial) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/interactor/license_type.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | type ( 4 | LicenseInteractor struct { 5 | *service 6 | 7 | LicenseKey string 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /internal/interactor/lock.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type ( 12 | // LockInteractor provides application logic for interacting with users. 13 | LockInteractor service 14 | 15 | // LockStore defines operations for working with locks. 16 | LockStore interface { 17 | ListExpiredLocksLessThanTime(ctx context.Context, t time.Time) ([]*ent.Lock, error) 18 | ListLocksOfRepo(ctx context.Context, r *ent.Repo) ([]*ent.Lock, error) 19 | FindLockOfRepoByEnv(ctx context.Context, r *ent.Repo, env string) (*ent.Lock, error) 20 | HasLockOfRepoForEnv(ctx context.Context, r *ent.Repo, env string) (bool, error) 21 | FindLockByID(ctx context.Context, id int) (*ent.Lock, error) 22 | CreateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) 23 | UpdateLock(ctx context.Context, l *ent.Lock) (*ent.Lock, error) 24 | DeleteLock(ctx context.Context, l *ent.Lock) error 25 | } 26 | ) 27 | 28 | func (i *LockInteractor) runAutoUnlock(stop <-chan struct{}) { 29 | ctx := context.Background() 30 | 31 | ticker := time.NewTicker(time.Minute) 32 | L: 33 | for { 34 | select { 35 | case _, ok := <-stop: 36 | if !ok { 37 | ticker.Stop() 38 | break L 39 | } 40 | case t := <-ticker.C: 41 | ls, err := i.store.ListExpiredLocksLessThanTime(ctx, t.UTC()) 42 | if err != nil { 43 | i.log.Error("It has failed to read expired locks.", zap.Error(err)) 44 | continue 45 | } 46 | 47 | for _, l := range ls { 48 | i.store.DeleteLock(ctx, l) 49 | i.log.Debug("Delete the expired lock.", zap.Int("id", l.ID), zap.Time("time", t)) 50 | } 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/interactor/repo_test.go: -------------------------------------------------------------------------------- 1 | package interactor_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/golang/mock/gomock" 8 | 9 | i "github.com/gitploy-io/gitploy/internal/interactor" 10 | "github.com/gitploy-io/gitploy/internal/interactor/mock" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | func TestInteractor_DeactivateRepo(t *testing.T) { 16 | t.Run("Deactivate successfully even if the webhook is not found.", func(t *testing.T) { 17 | ctrl := gomock.NewController(t) 18 | store := mock.NewMockStore(ctrl) 19 | scm := mock.NewMockSCM(ctrl) 20 | 21 | t.Log("Mocking DeleteWebhook to return an EntityNotFound error.") 22 | scm. 23 | EXPECT(). 24 | DeleteWebhook(gomock.Any(), gomock.AssignableToTypeOf(&ent.User{}), gomock.AssignableToTypeOf(&ent.Repo{}), gomock.Any()). 25 | Return(e.NewError(e.ErrorCodeEntityNotFound, nil)) 26 | 27 | store. 28 | EXPECT(). 29 | Deactivate(gomock.Any(), gomock.AssignableToTypeOf(&ent.Repo{})). 30 | Return(&ent.Repo{}, nil) 31 | 32 | it := i.NewInteractor(&i.InteractorConfig{ 33 | Store: store, 34 | SCM: scm, 35 | }) 36 | 37 | _, err := it.DeactivateRepo(context.Background(), &ent.User{}, &ent.Repo{}) 38 | if err != nil { 39 | t.Fatalf("DeactivateRepo returns an error: %v", err) 40 | } 41 | }) 42 | } 43 | -------------------------------------------------------------------------------- /internal/interactor/review.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | import ( 4 | "context" 5 | 6 | "go.uber.org/zap" 7 | 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | "github.com/gitploy-io/gitploy/model/ent/event" 10 | ) 11 | 12 | type ( 13 | ReviewInteractor service 14 | 15 | // ReviewStore defines operations for working with reviews. 16 | ReviewStore interface { 17 | SearchReviews(ctx context.Context, u *ent.User) ([]*ent.Review, error) 18 | ListReviews(ctx context.Context, d *ent.Deployment) ([]*ent.Review, error) 19 | FindReviewOfUser(ctx context.Context, u *ent.User, d *ent.Deployment) (*ent.Review, error) 20 | FindReviewByID(ctx context.Context, id int) (*ent.Review, error) 21 | // CreateReview creates a review of which status is pending. 22 | CreateReview(ctx context.Context, rv *ent.Review) (*ent.Review, error) 23 | // UpdateReview update the status and comment of the review. 24 | UpdateReview(ctx context.Context, rv *ent.Review) (*ent.Review, error) 25 | } 26 | ) 27 | 28 | // RespondReview update the status of review. 29 | func (i *ReviewInteractor) RespondReview(ctx context.Context, rv *ent.Review) (*ent.Review, error) { 30 | rv, err := i.store.UpdateReview(ctx, rv) 31 | if err != nil { 32 | return nil, err 33 | } 34 | 35 | if _, err := i.store.CreateEvent(ctx, &ent.Event{ 36 | Kind: event.KindReview, 37 | Type: event.TypeCreated, 38 | ReviewID: rv.ID, 39 | }); err != nil { 40 | i.log.Error("Failed to create a review event.", zap.Error(err)) 41 | } 42 | 43 | return rv, nil 44 | } 45 | -------------------------------------------------------------------------------- /internal/interactor/review_test.go: -------------------------------------------------------------------------------- 1 | package interactor_test 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | gm "github.com/golang/mock/gomock" 8 | 9 | i "github.com/gitploy-io/gitploy/internal/interactor" 10 | "github.com/gitploy-io/gitploy/internal/interactor/mock" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | ) 13 | 14 | func TestInteractor_RespondReview(t *testing.T) { 15 | t.Run("Return the review.", func(t *testing.T) { 16 | t.Log("Start mocking:") 17 | ctrl := gm.NewController(t) 18 | store := mock.NewMockStore(ctrl) 19 | 20 | t.Log("\tUpdates the review and dispatches a event.") 21 | store.EXPECT(). 22 | UpdateReview(gm.Any(), gm.AssignableToTypeOf(&ent.Review{})). 23 | Return(&ent.Review{}, nil) 24 | 25 | store.EXPECT(). 26 | CreateEvent(gm.Any(), gm.AssignableToTypeOf(&ent.Event{})). 27 | Return(&ent.Event{}, nil) 28 | 29 | it := i.NewInteractor(&i.InteractorConfig{ 30 | Store: store, 31 | }) 32 | _, err := it.RespondReview(context.Background(), &ent.Review{}) 33 | if err != nil { 34 | t.Fatalf("RespondReview returns an error: %v", err) 35 | } 36 | }) 37 | 38 | } 39 | -------------------------------------------------------------------------------- /internal/interactor/shared.go: -------------------------------------------------------------------------------- 1 | package interactor 2 | 3 | type ( 4 | // ListOptions specifies the optional parameters that 5 | // support cursor pagination. 6 | ListOptions struct { 7 | Page int 8 | PerPage int 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /internal/pkg/github/deploymentstatus.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | "github.com/gitploy-io/gitploy/pkg/e" 9 | "github.com/google/go-github/v42/github" 10 | ) 11 | 12 | func (g *Github) CreateRemoteDeploymentStatus(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, ds *extent.RemoteDeploymentStatus) (*extent.RemoteDeploymentStatus, error) { 13 | gds, _, err := g.Client(ctx, u.Token). 14 | Repositories. 15 | CreateDeploymentStatus(ctx, r.Namespace, r.Name, d.UID, &github.DeploymentStatusRequest{ 16 | State: github.String(ds.Status), 17 | Description: github.String(ds.Description), 18 | LogURL: github.String(ds.LogURL), 19 | }) 20 | if err != nil { 21 | return nil, e.NewError(e.ErrorCodeEntityUnprocessable, err) 22 | } 23 | 24 | return mapGithubDeploymentStatusToRemoteDeploymentStatus(gds), nil 25 | } 26 | 27 | func mapGithubDeploymentStatusToRemoteDeploymentStatus(gds *github.DeploymentStatus) *extent.RemoteDeploymentStatus { 28 | return &extent.RemoteDeploymentStatus{ 29 | ID: *gds.ID, 30 | Status: *gds.State, 31 | Description: *gds.Description, 32 | LogURL: *gds.LogURL, 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /internal/pkg/github/github.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/google/go-github/v42/github" 7 | graphql "github.com/shurcooL/githubv4" 8 | "golang.org/x/oauth2" 9 | ) 10 | 11 | type ( 12 | Github struct { 13 | baseURL string 14 | } 15 | 16 | GithubConfig struct { 17 | BaseURL string 18 | } 19 | ) 20 | 21 | func NewGithub(c *GithubConfig) *Github { 22 | return &Github{ 23 | baseURL: c.BaseURL, 24 | } 25 | } 26 | 27 | func (g *Github) Client(c context.Context, token string) *github.Client { 28 | tc := oauth2.NewClient(c, oauth2.StaticTokenSource( 29 | &oauth2.Token{AccessToken: token}, 30 | )) 31 | 32 | var client *github.Client 33 | if g.baseURL != "" { 34 | client, _ = github.NewEnterpriseClient(g.baseURL, g.baseURL, tc) 35 | } else { 36 | client = github.NewClient(tc) 37 | } 38 | 39 | return client 40 | } 41 | 42 | func (g *Github) GraphQLClient(c context.Context, token string) *graphql.Client { 43 | tc := oauth2.NewClient(c, oauth2.StaticTokenSource( 44 | &oauth2.Token{AccessToken: token}, 45 | )) 46 | 47 | var client *graphql.Client 48 | if g.baseURL != "" { 49 | client = graphql.NewEnterpriseClient(g.baseURL, tc) 50 | } else { 51 | client = graphql.NewClient(tc) 52 | } 53 | 54 | return client 55 | } 56 | -------------------------------------------------------------------------------- /internal/pkg/github/github_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func Test_NewGithub(t *testing.T) { 8 | t.Run("Create a new Github with the base URL.", func(t *testing.T) { 9 | url := "https://github.gitploy.io/" 10 | 11 | g := NewGithub(&GithubConfig{ 12 | BaseURL: url, 13 | }) 14 | 15 | if g.baseURL != url { 16 | t.Fatalf("NewGithub.baseURL = %v, wanted %v", g.baseURL, url) 17 | } 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /internal/pkg/github/repos_test.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | "github.com/gitploy-io/gitploy/pkg/e" 9 | "gopkg.in/h2non/gock.v1" 10 | ) 11 | 12 | func TestGithub_DeleteWebhook(t *testing.T) { 13 | t.Run("Return the ErrorCodeEntityNotFound error when the webhook is not found.", func(t *testing.T) { 14 | t.Log("Mocking the delete webhook API") 15 | gock.New("https://api.github.com"). 16 | Delete("/repos/gitploy-io/gitploy/hooks/1"). 17 | Reply(404) 18 | 19 | g := NewGithub(&GithubConfig{}) 20 | 21 | const hookID = 1 22 | 23 | err := g.DeleteWebhook( 24 | context.Background(), 25 | &ent.User{}, 26 | &ent.Repo{ 27 | Namespace: "gitploy-io", 28 | Name: "gitploy", 29 | }, 30 | hookID, 31 | ) 32 | 33 | if !e.HasErrorCode(err, e.ErrorCodeEntityNotFound) { 34 | t.Fatalf("DeleteWebhook doesn't returns an ErrorCodeEntityNotFound error: %v", err) 35 | } 36 | }) 37 | 38 | t.Run("Delete the webhook.", func(t *testing.T) { 39 | t.Log("Mocking the delete webhook API") 40 | gock.New("https://api.github.com"). 41 | Delete("/repos/gitploy-io/gitploy/hooks/1"). 42 | Reply(200) 43 | 44 | g := NewGithub(&GithubConfig{}) 45 | 46 | const hookID = 1 47 | 48 | err := g.DeleteWebhook( 49 | context.Background(), 50 | &ent.User{}, 51 | &ent.Repo{ 52 | Namespace: "gitploy-io", 53 | Name: "gitploy", 54 | }, 55 | hookID, 56 | ) 57 | 58 | if err != nil { 59 | t.Fatalf("DeleteWebhook returns an error: %v", err) 60 | } 61 | }) 62 | } 63 | -------------------------------------------------------------------------------- /internal/pkg/github/sync.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | "github.com/gitploy-io/gitploy/pkg/e" 9 | "github.com/google/go-github/v42/github" 10 | ) 11 | 12 | func (g *Github) ListRemoteRepos(ctx context.Context, u *ent.User) ([]*extent.RemoteRepo, error) { 13 | grs, err := g.listRemoteRepos(ctx, u) 14 | if err != nil { 15 | return nil, e.NewError(e.ErrorCodeInternalError, err) 16 | } 17 | 18 | remotes := make([]*extent.RemoteRepo, 0) 19 | for _, r := range grs { 20 | remotes = append(remotes, mapGithubRepoToRemotePerm(r)) 21 | } 22 | 23 | return remotes, nil 24 | } 25 | 26 | func (g *Github) listRemoteRepos(ctx context.Context, u *ent.User) ([]*github.Repository, error) { 27 | opt := &github.RepositoryListOptions{ 28 | ListOptions: github.ListOptions{PerPage: 100}, 29 | } 30 | 31 | all := make([]*github.Repository, 0) 32 | for { 33 | remotes, res, err := g.Client(ctx, u.Token). 34 | Repositories. 35 | List(ctx, "", opt) 36 | if err != nil { 37 | return nil, err 38 | } 39 | 40 | all = append(all, remotes...) 41 | if res.NextPage == 0 { 42 | break 43 | } 44 | 45 | opt.Page = res.NextPage 46 | } 47 | 48 | return all, nil 49 | } 50 | -------------------------------------------------------------------------------- /internal/pkg/github/user.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | "github.com/google/go-github/v42/github" 9 | ) 10 | 11 | func (g *Github) GetRateLimit(ctx context.Context, u *ent.User) (*extent.RateLimit, error) { 12 | rl, _, err := g.Client(ctx, u.Token). 13 | RateLimits(ctx) 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return mapGithubRateLimitToRateLimit(rl), nil 19 | } 20 | 21 | func mapGithubRateLimitToRateLimit(gr *github.RateLimits) *extent.RateLimit { 22 | return &extent.RateLimit{ 23 | Limit: gr.Core.Limit, 24 | Remaining: gr.Core.Remaining, 25 | Reset: gr.Core.Reset.Time, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /internal/pkg/github/web.go: -------------------------------------------------------------------------------- 1 | package github 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/extent" 7 | "github.com/google/go-github/v42/github" 8 | ) 9 | 10 | func (g *Github) GetRemoteUserByToken(ctx context.Context, token string) (*extent.RemoteUser, error) { 11 | c := g.Client(ctx, token) 12 | 13 | u, _, err := c.Users.Get(ctx, "") 14 | if err != nil { 15 | return nil, err 16 | } 17 | 18 | return mapGithubUserToUser(u), err 19 | } 20 | 21 | func (g *Github) ListRemoteOrgsByToken(ctx context.Context, token string) ([]string, error) { 22 | // TODO: List all orgs. 23 | orgs, _, err := g.Client(ctx, token). 24 | Organizations. 25 | List(ctx, "", &github.ListOptions{}) 26 | if err != nil { 27 | return nil, err 28 | } 29 | 30 | ret := []string{} 31 | for _, o := range orgs { 32 | ret = append(ret, *o.Login) 33 | } 34 | 35 | return ret, nil 36 | } 37 | -------------------------------------------------------------------------------- /internal/pkg/store/chat_user.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/gitploy-io/gitploy/model/ent/chatuser" 8 | ) 9 | 10 | func (s *Store) FindChatUserByID(ctx context.Context, id string) (*ent.ChatUser, error) { 11 | return s.c.ChatUser. 12 | Query(). 13 | Where( 14 | chatuser.IDEQ(id), 15 | ). 16 | WithUser(). 17 | First(ctx) 18 | } 19 | 20 | func (s *Store) CreateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) { 21 | return s.c.ChatUser. 22 | Create(). 23 | SetID(cu.ID). 24 | SetToken(cu.Token). 25 | SetBotToken(cu.BotToken). 26 | SetRefresh(cu.Refresh). 27 | SetExpiry(cu.Expiry). 28 | SetUserID(cu.UserID). 29 | Save(ctx) 30 | } 31 | 32 | func (s *Store) UpdateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) { 33 | return s.c.ChatUser. 34 | UpdateOneID(cu.ID). 35 | SetToken(cu.Token). 36 | SetBotToken(cu.BotToken). 37 | SetRefresh(cu.Refresh). 38 | SetExpiry(cu.Expiry). 39 | Save(ctx) 40 | } 41 | 42 | func (s *Store) DeleteChatUser(ctx context.Context, cu *ent.ChatUser) error { 43 | return s.c.ChatUser. 44 | DeleteOneID(cu.ID). 45 | Exec(ctx) 46 | } 47 | -------------------------------------------------------------------------------- /internal/pkg/store/deploymentstatistics_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | "time" 7 | 8 | "github.com/gitploy-io/gitploy/model/ent/enttest" 9 | "github.com/gitploy-io/gitploy/model/ent/migrate" 10 | ) 11 | 12 | func TestStore_ListDeploymentStatisticsGreaterThanTime(t *testing.T) { 13 | ctx := context.Background() 14 | 15 | client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", 16 | enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), 17 | ) 18 | defer client.Close() 19 | 20 | tm := time.Now() 21 | 22 | client.DeploymentStatistics. 23 | Create(). 24 | SetRepoID(1). 25 | SetEnv("dev"). 26 | SetUpdatedAt(tm.Add(-time.Hour)). 27 | SaveX(ctx) 28 | 29 | client.DeploymentStatistics. 30 | Create(). 31 | SetRepoID(1). 32 | SetEnv("prod"). 33 | SetUpdatedAt(tm.Add(time.Hour)). 34 | SaveX(ctx) 35 | 36 | s := NewStore(client) 37 | 38 | dcs, err := s.ListDeploymentStatisticsGreaterThanTime(ctx, tm) 39 | if err != nil { 40 | t.Fatalf("ListDeploymentStatisticssGreaterThanTime returns an error: %s", err) 41 | } 42 | 43 | expected := 1 44 | if len(dcs) != expected { 45 | t.Fatalf("ListDeploymentStatisticssGreaterThanTime = %v, wanted %v", len(dcs), expected) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /internal/pkg/store/event_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | "github.com/gitploy-io/gitploy/model/ent/enttest" 9 | "github.com/gitploy-io/gitploy/model/ent/event" 10 | "github.com/gitploy-io/gitploy/model/ent/migrate" 11 | ) 12 | 13 | func TestStore_CreateEvent(t *testing.T) { 14 | 15 | t.Run("Create a new deployment_status event", func(t *testing.T) { 16 | ctx := context.Background() 17 | 18 | client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", 19 | enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), 20 | ) 21 | defer client.Close() 22 | 23 | s := NewStore(client) 24 | 25 | _, err := s.CreateEvent(ctx, &ent.Event{ 26 | Kind: event.KindDeploymentStatus, 27 | Type: event.TypeCreated, 28 | DeploymentStatusID: 1, 29 | }) 30 | if err != nil { 31 | t.Fatalf("CreateEvent returns an error: %s", err) 32 | } 33 | }) 34 | } 35 | -------------------------------------------------------------------------------- /internal/pkg/store/review_test.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/gitploy-io/gitploy/model/ent/enttest" 8 | "github.com/gitploy-io/gitploy/model/ent/migrate" 9 | "github.com/gitploy-io/gitploy/model/ent/review" 10 | "github.com/gitploy-io/gitploy/pkg/e" 11 | ) 12 | 13 | func TestStore_UpdateReview(t *testing.T) { 14 | t.Run("Return an unprocessible entity error when the vaildation is failed.", func(t *testing.T) { 15 | ctx := context.Background() 16 | 17 | client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", 18 | enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), 19 | ) 20 | defer client.Close() 21 | 22 | r := client.Review. 23 | Create(). 24 | SetDeploymentID(1). 25 | SetUserID(1). 26 | SaveX(ctx) 27 | 28 | s := NewStore(client) 29 | 30 | r.Status = review.Status("UNPROCESSIBLE") 31 | _, err := s.UpdateReview(ctx, r) 32 | if !e.HasErrorCode(err, e.ErrorCodeEntityUnprocessable) { 33 | t.Fatalf("UpdateReview error code = %v, wanted unprocessable_entity", err) 34 | } 35 | }) 36 | 37 | t.Run("Update the review.", func(t *testing.T) { 38 | ctx := context.Background() 39 | 40 | client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1", 41 | enttest.WithMigrateOptions(migrate.WithForeignKeys(false)), 42 | ) 43 | defer client.Close() 44 | 45 | r := client.Review. 46 | Create(). 47 | SetDeploymentID(1). 48 | SetUserID(1). 49 | SaveX(ctx) 50 | 51 | s := NewStore(client) 52 | 53 | r.Status = review.StatusApproved 54 | _, err := s.UpdateReview(ctx, r) 55 | if err != nil { 56 | t.Fatalf("UpdateReview returns an error: %v", err) 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /internal/pkg/store/store.go: -------------------------------------------------------------------------------- 1 | package store 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | "github.com/pkg/errors" 8 | ) 9 | 10 | type ( 11 | Store struct { 12 | c *ent.Client 13 | } 14 | ) 15 | 16 | func NewStore(c *ent.Client) *Store { 17 | return &Store{ 18 | c: c, 19 | } 20 | } 21 | 22 | // WithTx runs callbacks in a transaction. 23 | func (s *Store) WithTx(ctx context.Context, fn func(tx *ent.Tx) error) error { 24 | tx, err := s.c.Tx(ctx) 25 | if err != nil { 26 | return err 27 | } 28 | defer func() { 29 | if v := recover(); v != nil { 30 | tx.Rollback() 31 | panic(v) 32 | } 33 | }() 34 | if err := fn(tx); err != nil { 35 | if rerr := tx.Rollback(); rerr != nil { 36 | err = errors.Wrapf(err, "rolling back transaction: %v", rerr) 37 | } 38 | return err 39 | } 40 | if err := tx.Commit(); err != nil { 41 | return errors.Wrapf(err, "committing transaction: %v", err) 42 | } 43 | return nil 44 | } 45 | 46 | func offset(page, perPage int) int { 47 | return (page - 1) * perPage 48 | } 49 | -------------------------------------------------------------------------------- /internal/server/api/shared/interface.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source ./interface.go -destination ./mock/interactor.go -package mock 2 | 3 | package shared 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/gitploy-io/gitploy/model/extent" 9 | ) 10 | 11 | type ( 12 | Interactor interface { 13 | GetLicense(ctx context.Context) (*extent.License, error) 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /internal/server/api/v1/license/interface.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/extent" 7 | ) 8 | 9 | type ( 10 | Interactor interface { 11 | GetLicense(ctx context.Context) (*extent.License, error) 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /internal/server/api/v1/license/license.go: -------------------------------------------------------------------------------- 1 | package license 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | gb "github.com/gitploy-io/gitploy/internal/server/global" 8 | "go.uber.org/zap" 9 | ) 10 | 11 | type ( 12 | Licenser struct { 13 | i Interactor 14 | log *zap.Logger 15 | } 16 | ) 17 | 18 | func NewLicenser(intr Interactor) *Licenser { 19 | return &Licenser{ 20 | i: intr, 21 | log: zap.L().Named("license"), 22 | } 23 | } 24 | 25 | func (l *Licenser) GetLicense(c *gin.Context) { 26 | ctx := c.Request.Context() 27 | 28 | lic, err := l.i.GetLicense(ctx) 29 | if err != nil { 30 | l.log.Check(gb.GetZapLogLevel(err), "It has failed to get the license.").Write(zap.Error(err)) 31 | gb.ResponseWithError(c, err) 32 | return 33 | } 34 | 35 | gb.Response(c, http.StatusOK, lic) 36 | } 37 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/api.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import "go.uber.org/zap" 4 | 5 | type ( 6 | API struct { 7 | common *service 8 | 9 | // APIs used for talking to different parts of the entities. 10 | Repo *RepoAPI 11 | Commits *CommitAPI 12 | Branch *BranchAPI 13 | Tag *TagAPI 14 | Deployment *DeploymentAPI 15 | Config *ConfigAPI 16 | Review *ReviewAPI 17 | DeploymentStatus *DeploymentStatusAPI 18 | Lock *LockAPI 19 | Perm *PermAPI 20 | } 21 | 22 | APIConfig struct { 23 | Interactor 24 | } 25 | 26 | service struct { 27 | i Interactor 28 | log *zap.Logger 29 | } 30 | ) 31 | 32 | func NewAPI(c APIConfig) *API { 33 | api := &API{} 34 | 35 | api.common = &service{ 36 | i: c.Interactor, 37 | log: zap.L().Named("repos"), 38 | } 39 | api.Repo = (*RepoAPI)(api.common) 40 | api.Commits = (*CommitAPI)(api.common) 41 | api.Branch = (*BranchAPI)(api.common) 42 | api.Tag = (*TagAPI)(api.common) 43 | api.Deployment = (*DeploymentAPI)(api.common) 44 | api.Config = (*ConfigAPI)(api.common) 45 | api.Review = (*ReviewAPI)(api.common) 46 | api.DeploymentStatus = (*DeploymentStatusAPI)(api.common) 47 | api.Lock = (*LockAPI)(api.common) 48 | api.Perm = (*PermAPI)(api.common) 49 | 50 | return api 51 | } 52 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/branch.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | BranchAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/branch_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | gb "github.com/gitploy-io/gitploy/internal/server/global" 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | ) 12 | 13 | func (s *BranchAPI) Get(c *gin.Context) { 14 | ctx := c.Request.Context() 15 | 16 | var ( 17 | branch = c.Param("branch") 18 | ) 19 | 20 | uv, _ := c.Get(gb.KeyUser) 21 | u := uv.(*ent.User) 22 | 23 | rv, _ := c.Get(KeyRepo) 24 | repo := rv.(*ent.Repo) 25 | 26 | b, err := s.i.GetBranch(ctx, u, repo, branch) 27 | if err != nil { 28 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the branch.").Write(zap.Error(err)) 29 | gb.ResponseWithError(c, err) 30 | return 31 | } 32 | 33 | gb.Response(c, http.StatusOK, b) 34 | } 35 | 36 | func (s *BranchAPI) GetDefault(c *gin.Context) { 37 | ctx := c.Request.Context() 38 | 39 | uv, _ := c.Get(gb.KeyUser) 40 | u := uv.(*ent.User) 41 | 42 | rv, _ := c.Get(KeyRepo) 43 | repo := rv.(*ent.Repo) 44 | 45 | b, err := s.i.GetDefaultBranch(ctx, u, repo) 46 | if err != nil { 47 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the branch.").Write(zap.Error(err)) 48 | gb.ResponseWithError(c, err) 49 | return 50 | } 51 | 52 | gb.Response(c, http.StatusOK, b) 53 | } 54 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/branch_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | i "github.com/gitploy-io/gitploy/internal/interactor" 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | "github.com/gitploy-io/gitploy/pkg/e" 14 | ) 15 | 16 | func (s *BranchAPI) List(c *gin.Context) { 17 | ctx := c.Request.Context() 18 | 19 | var ( 20 | page int 21 | perPage int 22 | err error 23 | ) 24 | 25 | // Validate quries 26 | if page, err = strconv.Atoi(c.DefaultQuery("page", defaultQueryPage)); err != nil { 27 | s.log.Warn("Invalid parameter: page is not integer.", zap.Error(err)) 28 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 29 | return 30 | } 31 | 32 | if perPage, err = strconv.Atoi(c.DefaultQuery("per_page", defaultQueryPerPage)); err != nil { 33 | s.log.Warn("Invalid parameter: per_page is not integer.", zap.Error(err)) 34 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 35 | return 36 | } 37 | 38 | uv, _ := c.Get(gb.KeyUser) 39 | u := uv.(*ent.User) 40 | 41 | rv, _ := c.Get(KeyRepo) 42 | repo := rv.(*ent.Repo) 43 | 44 | branches, err := s.i.ListBranches(ctx, u, repo, &i.ListOptions{ 45 | Page: page, 46 | PerPage: perPage, 47 | }) 48 | if err != nil { 49 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list branches.").Write(zap.Error(err)) 50 | gb.ResponseWithError(c, err) 51 | return 52 | } 53 | 54 | gb.Response(c, http.StatusOK, branches) 55 | } 56 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/commit.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | CommitAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/commit_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | gb "github.com/gitploy-io/gitploy/internal/server/global" 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func (s *CommitAPI) Get(c *gin.Context) { 13 | ctx := c.Request.Context() 14 | 15 | var ( 16 | sha = c.Param("sha") 17 | ) 18 | 19 | uv, _ := c.Get(gb.KeyUser) 20 | u := uv.(*ent.User) 21 | 22 | rv, _ := c.Get(KeyRepo) 23 | repo := rv.(*ent.Repo) 24 | 25 | commit, err := s.i.GetCommit(ctx, u, repo, sha) 26 | if err != nil { 27 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the commit.").Write(zap.Error(err)) 28 | gb.ResponseWithError(c, err) 29 | return 30 | } 31 | 32 | gb.Response(c, http.StatusOK, commit) 33 | } 34 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/commit_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | i "github.com/gitploy-io/gitploy/internal/interactor" 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | "github.com/gitploy-io/gitploy/pkg/e" 14 | ) 15 | 16 | func (s *CommitAPI) List(c *gin.Context) { 17 | ctx := c.Request.Context() 18 | 19 | var ( 20 | branch = c.Query("branch") 21 | page int 22 | perPage int 23 | err error 24 | ) 25 | 26 | // Validate quries 27 | if page, err = strconv.Atoi(c.DefaultQuery("page", defaultQueryPage)); err != nil { 28 | s.log.Warn("Invalid parameter: page is not integer.", zap.Error(err)) 29 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 30 | return 31 | } 32 | 33 | if perPage, err = strconv.Atoi(c.DefaultQuery("per_page", defaultQueryPerPage)); err != nil { 34 | s.log.Warn("Invalid parameter: per_page is not integer.", zap.Error(err)) 35 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 36 | return 37 | } 38 | 39 | uv, _ := c.Get(gb.KeyUser) 40 | u := uv.(*ent.User) 41 | 42 | rv, _ := c.Get(KeyRepo) 43 | repo := rv.(*ent.Repo) 44 | 45 | commits, err := s.i.ListCommits(ctx, u, repo, branch, &i.ListOptions{ 46 | Page: page, 47 | PerPage: perPage, 48 | }) 49 | if err != nil { 50 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list commits.").Write(zap.Error(err)) 51 | gb.ResponseWithError(c, err) 52 | return 53 | } 54 | 55 | gb.Response(c, http.StatusOK, commits) 56 | } 57 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/commit_status_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | gb "github.com/gitploy-io/gitploy/internal/server/global" 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | "github.com/gitploy-io/gitploy/model/extent" 12 | ) 13 | 14 | func (s *CommitAPI) ListStatuses(c *gin.Context) { 15 | ctx := c.Request.Context() 16 | 17 | var ( 18 | sha = c.Param("sha") 19 | ) 20 | 21 | uv, _ := c.Get(gb.KeyUser) 22 | u := uv.(*ent.User) 23 | 24 | rv, _ := c.Get(KeyRepo) 25 | repo := rv.(*ent.Repo) 26 | 27 | ss, err := s.i.ListCommitStatuses(ctx, u, repo, sha) 28 | if err != nil { 29 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list commit statuses.").Write(zap.Error(err)) 30 | gb.ResponseWithError(c, err) 31 | return 32 | } 33 | 34 | gb.Response(c, http.StatusOK, map[string]interface{}{ 35 | "state": mergeState(ss), 36 | "statuses": ss, 37 | }) 38 | } 39 | 40 | func mergeState(ss []*extent.Status) string { 41 | // The state is failure if one of them is failure. 42 | for _, s := range ss { 43 | if s.State == extent.StatusStateFailure || s.State == extent.StatusStateCancelled { 44 | return string(extent.StatusStateFailure) 45 | } 46 | } 47 | 48 | for _, s := range ss { 49 | if s.State == extent.StatusStatePending { 50 | return string(extent.StatusStatePending) 51 | } 52 | } 53 | 54 | return string(extent.StatusStateSuccess) 55 | } 56 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/config.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | ConfigAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/config_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | gb "github.com/gitploy-io/gitploy/internal/server/global" 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | ) 12 | 13 | func (s *ConfigAPI) Get(c *gin.Context) { 14 | ctx := c.Request.Context() 15 | 16 | vu, _ := c.Get(gb.KeyUser) 17 | u := vu.(*ent.User) 18 | 19 | vr, _ := c.Get(KeyRepo) 20 | re := vr.(*ent.Repo) 21 | 22 | config, err := s.i.GetConfig(ctx, u, re) 23 | if err != nil { 24 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the configuration.").Write(zap.Error(err)) 25 | gb.ResponseWithError(c, err) 26 | return 27 | } 28 | 29 | gb.Response(c, http.StatusOK, config) 30 | } 31 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/deployment.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | DeploymentAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/deployment_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | gb "github.com/gitploy-io/gitploy/internal/server/global" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | func (s *DeploymentAPI) Get(c *gin.Context) { 16 | var ( 17 | number int 18 | err error 19 | ) 20 | 21 | if number, err = strconv.Atoi(c.Param("number")); err != nil { 22 | s.log.Warn("Invalid parameter: number must be integer.", zap.Error(err)) 23 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 24 | return 25 | } 26 | 27 | vr, _ := c.Get(KeyRepo) 28 | re := vr.(*ent.Repo) 29 | 30 | ctx := c.Request.Context() 31 | 32 | d, err := s.i.FindDeploymentOfRepoByNumber(ctx, re, number) 33 | if err != nil { 34 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the deployments.").Write(zap.Error(err)) 35 | gb.ResponseWithError(c, err) 36 | return 37 | } 38 | 39 | gb.Response(c, http.StatusOK, d) 40 | } 41 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/deployment_status.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | DeploymentStatusAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/deployment_status_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | gb "github.com/gitploy-io/gitploy/internal/server/global" 9 | "github.com/gitploy-io/gitploy/model/ent" 10 | "github.com/gitploy-io/gitploy/pkg/e" 11 | "go.uber.org/zap" 12 | ) 13 | 14 | func (s *DeploymentStatusAPI) List(c *gin.Context) { 15 | ctx := c.Request.Context() 16 | 17 | var ( 18 | number int 19 | err error 20 | ) 21 | 22 | if number, err = strconv.Atoi(c.Param("number")); err != nil { 23 | s.log.Warn("Invalid parameter: number is not integer.", zap.String("number", c.Param("number"))) 24 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 25 | return 26 | } 27 | 28 | vr, _ := c.Get(KeyRepo) 29 | re := vr.(*ent.Repo) 30 | 31 | d, err := s.i.FindDeploymentOfRepoByNumber(ctx, re, number) 32 | if err != nil { 33 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the deployments.").Write(zap.Error(err)) 34 | gb.ResponseWithError(c, err) 35 | return 36 | } 37 | 38 | dss, err := s.i.ListDeploymentStatuses(ctx, d) 39 | if err != nil { 40 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list the deployment statuses.").Write(zap.Error(err)) 41 | gb.ResponseWithError(c, err) 42 | return 43 | } 44 | 45 | s.log.Debug("Success to list deployment statuses.") 46 | gb.Response(c, http.StatusOK, dss) 47 | } 48 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/lock.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | LockAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/lock_delete.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package repos 8 | 9 | import ( 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/gin-gonic/gin" 14 | "go.uber.org/zap" 15 | 16 | gb "github.com/gitploy-io/gitploy/internal/server/global" 17 | "github.com/gitploy-io/gitploy/pkg/e" 18 | ) 19 | 20 | func (s *LockAPI) Delete(c *gin.Context) { 21 | ctx := c.Request.Context() 22 | 23 | var ( 24 | id int 25 | err error 26 | ) 27 | 28 | if id, err = strconv.Atoi(c.Param("lockID")); err != nil { 29 | gb.ResponseWithError( 30 | c, 31 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "The ID must be number.", nil), 32 | ) 33 | return 34 | } 35 | 36 | l, err := s.i.FindLockByID(ctx, id) 37 | if err != nil { 38 | s.log.Check(gb.GetZapLogLevel(err), "Failed to find the lock.").Write(zap.Error(err)) 39 | gb.ResponseWithError(c, err) 40 | return 41 | } 42 | 43 | if err := s.i.DeleteLock(ctx, l); err != nil { 44 | s.log.Check(gb.GetZapLogLevel(err), "Failed to delete the lock.").Write(zap.Error(err)) 45 | gb.ResponseWithError(c, err) 46 | return 47 | } 48 | 49 | s.log.Debug("Unlock the env.", zap.String("env", l.Env)) 50 | gb.Response(c, http.StatusOK, nil) 51 | } 52 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/lock_delete_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package repos 8 | 9 | import ( 10 | "fmt" 11 | "net/http" 12 | "net/http/httptest" 13 | "testing" 14 | 15 | "github.com/gin-gonic/gin" 16 | "github.com/gitploy-io/gitploy/internal/server/api/v1/repos/mock" 17 | "github.com/gitploy-io/gitploy/model/ent" 18 | "github.com/golang/mock/gomock" 19 | "go.uber.org/zap" 20 | ) 21 | 22 | func TestLockAPI_Delete(t *testing.T) { 23 | t.Run("Unlock the env", func(t *testing.T) { 24 | input := struct { 25 | id int 26 | }{ 27 | id: 1, 28 | } 29 | 30 | ctrl := gomock.NewController(t) 31 | m := mock.NewMockInteractor(ctrl) 32 | 33 | t.Log("Find the lock") 34 | m. 35 | EXPECT(). 36 | FindLockByID(gomock.Any(), input.id). 37 | Return(&ent.Lock{ID: input.id}, nil) 38 | 39 | t.Log("Delete the lock") 40 | m. 41 | EXPECT(). 42 | DeleteLock(gomock.Any(), gomock.Eq(&ent.Lock{ID: input.id})). 43 | Return(nil) 44 | 45 | s := LockAPI{i: m, log: zap.L()} 46 | gin.SetMode(gin.ReleaseMode) 47 | router := gin.New() 48 | router.DELETE("repos/:id/locks/:lockID", s.Delete) 49 | 50 | req, _ := http.NewRequest("DELETE", fmt.Sprintf("/repos/1/locks/%d", input.id), nil) 51 | w := httptest.NewRecorder() 52 | 53 | router.ServeHTTP(w, req) 54 | if w.Code != http.StatusOK { 55 | t.Fatalf("Code = %v, wanted %v. Body=%v", w.Code, http.StatusCreated, w.Body) 56 | } 57 | }) 58 | } 59 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/lock_list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package repos 8 | 9 | import ( 10 | "net/http" 11 | 12 | "github.com/gin-gonic/gin" 13 | "go.uber.org/zap" 14 | 15 | gb "github.com/gitploy-io/gitploy/internal/server/global" 16 | "github.com/gitploy-io/gitploy/model/ent" 17 | ) 18 | 19 | func (s *LockAPI) List(c *gin.Context) { 20 | ctx := c.Request.Context() 21 | 22 | vr, _ := c.Get(KeyRepo) 23 | re := vr.(*ent.Repo) 24 | 25 | locks, err := s.i.ListLocksOfRepo(ctx, re) 26 | if err != nil { 27 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list locks.").Write(zap.Error(err)) 28 | gb.ResponseWithError(c, err) 29 | return 30 | } 31 | 32 | gb.Response(c, http.StatusOK, locks) 33 | } 34 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/lock_oss.go: -------------------------------------------------------------------------------- 1 | //go:build oss 2 | 3 | package repos 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | gb "github.com/gitploy-io/gitploy/internal/server/global" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | func (s *LockAPI) List(c *gin.Context) { 16 | gb.Response(c, http.StatusOK, make([]*ent.Lock, 0)) 17 | } 18 | 19 | func (s *LockAPI) Create(c *gin.Context) { 20 | gb.ResponseWithError( 21 | c, 22 | e.NewError(e.ErrorCodeLicenseRequired, nil), 23 | ) 24 | } 25 | 26 | func (s *LockAPI) Update(c *gin.Context) { 27 | gb.ResponseWithError( 28 | c, 29 | e.NewError(e.ErrorCodeLicenseRequired, nil), 30 | ) 31 | } 32 | 33 | func (s *LockAPI) Delete(c *gin.Context) { 34 | gb.ResponseWithError( 35 | c, 36 | e.NewError(e.ErrorCodeLicenseRequired, nil), 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/perm.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | PermAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/perm_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | i "github.com/gitploy-io/gitploy/internal/interactor" 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | "github.com/gitploy-io/gitploy/pkg/e" 14 | ) 15 | 16 | func (s *PermAPI) List(c *gin.Context) { 17 | ctx := c.Request.Context() 18 | 19 | var ( 20 | q = c.DefaultQuery("q", "") 21 | page int 22 | perPage int 23 | err error 24 | ) 25 | 26 | // Validate quries 27 | if page, err = strconv.Atoi(c.DefaultQuery("page", defaultQueryPage)); err != nil { 28 | s.log.Warn("Invalid parameter: page is not integer.", zap.Error(err)) 29 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 30 | return 31 | } 32 | 33 | if perPage, err = strconv.Atoi(c.DefaultQuery("per_page", defaultQueryPerPage)); err != nil { 34 | s.log.Warn("Invalid parameter: per_page is not integer.", zap.Error(err)) 35 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 36 | return 37 | } 38 | 39 | v, _ := c.Get(KeyRepo) 40 | re := v.(*ent.Repo) 41 | 42 | if perPage > 100 { 43 | perPage = 100 44 | } 45 | 46 | perms, err := s.i.ListPermsOfRepo(ctx, re, &i.ListPermsOfRepoOptions{ 47 | ListOptions: i.ListOptions{Page: page, PerPage: perPage}, 48 | Query: q, 49 | }) 50 | if err != nil { 51 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list permissions.").Write(zap.Error(err)) 52 | gb.ResponseWithError(c, err) 53 | return 54 | } 55 | 56 | gb.Response(c, http.StatusOK, perms) 57 | } 58 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/repo.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | RepoAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/repo_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | 8 | gb "github.com/gitploy-io/gitploy/internal/server/global" 9 | "github.com/gitploy-io/gitploy/model/ent" 10 | ) 11 | 12 | func (s *RepoAPI) Get(c *gin.Context) { 13 | rv, _ := c.Get(KeyRepo) 14 | repo := rv.(*ent.Repo) 15 | 16 | gb.Response(c, http.StatusOK, repo) 17 | } 18 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/review.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | ReviewAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/review_get.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package repos 8 | 9 | import ( 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/gin-gonic/gin" 14 | "go.uber.org/zap" 15 | 16 | gb "github.com/gitploy-io/gitploy/internal/server/global" 17 | "github.com/gitploy-io/gitploy/model/ent" 18 | "github.com/gitploy-io/gitploy/pkg/e" 19 | ) 20 | 21 | func (s *ReviewAPI) GetMine(c *gin.Context) { 22 | ctx := c.Request.Context() 23 | 24 | var ( 25 | number int 26 | err error 27 | ) 28 | 29 | if number, err = strconv.Atoi(c.Param("number")); err != nil { 30 | s.log.Warn("Invalid parameter: number must be integer.", zap.Error(err)) 31 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 32 | return 33 | } 34 | 35 | vu, _ := c.Get(gb.KeyUser) 36 | u := vu.(*ent.User) 37 | 38 | vr, _ := c.Get(KeyRepo) 39 | re := vr.(*ent.Repo) 40 | 41 | d, err := s.i.FindDeploymentOfRepoByNumber(ctx, re, number) 42 | if err != nil { 43 | s.log.Check(gb.GetZapLogLevel(err), "Failed to find the deployment.").Write(zap.Error(err)) 44 | gb.ResponseWithError(c, err) 45 | return 46 | } 47 | 48 | rv, err := s.i.FindReviewOfUser(ctx, u, d) 49 | if err != nil { 50 | s.log.Check(gb.GetZapLogLevel(err), "Failed to find the user's review.").Write(zap.Error(err)) 51 | gb.ResponseWithError(c, err) 52 | return 53 | } 54 | 55 | gb.Response(c, http.StatusOK, rv) 56 | } 57 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/review_list.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package repos 8 | 9 | import ( 10 | "net/http" 11 | "strconv" 12 | 13 | "github.com/gin-gonic/gin" 14 | gb "github.com/gitploy-io/gitploy/internal/server/global" 15 | "github.com/gitploy-io/gitploy/model/ent" 16 | "github.com/gitploy-io/gitploy/pkg/e" 17 | "go.uber.org/zap" 18 | ) 19 | 20 | func (s *ReviewAPI) List(c *gin.Context) { 21 | ctx := c.Request.Context() 22 | 23 | var ( 24 | number int 25 | err error 26 | ) 27 | 28 | if number, err = strconv.Atoi(c.Param("number")); err != nil { 29 | s.log.Warn("Invalid parameter: number must be integer.", zap.Error(err)) 30 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 31 | return 32 | } 33 | 34 | vr, _ := c.Get(KeyRepo) 35 | re := vr.(*ent.Repo) 36 | 37 | d, err := s.i.FindDeploymentOfRepoByNumber(ctx, re, number) 38 | if err != nil { 39 | s.log.Check(gb.GetZapLogLevel(err), "Failed to find the deployment.").Write(zap.Error(err)) 40 | gb.ResponseWithError(c, err) 41 | return 42 | } 43 | 44 | rvs, err := s.i.ListReviews(ctx, d) 45 | if err != nil { 46 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list reviews.").Write(zap.Error(err)) 47 | gb.ResponseWithError(c, err) 48 | return 49 | } 50 | 51 | gb.Response(c, http.StatusOK, rvs) 52 | } 53 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/review_oss.go: -------------------------------------------------------------------------------- 1 | //go:build oss 2 | 3 | package repos 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | 10 | gb "github.com/gitploy-io/gitploy/internal/server/global" 11 | "github.com/gitploy-io/gitploy/model/ent" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | func (s *ReviewAPI) List(c *gin.Context) { 16 | gb.Response(c, http.StatusOK, make([]*ent.Review, 0)) 17 | } 18 | 19 | func (s *ReviewAPI) GetMine(c *gin.Context) { 20 | gb.Response(c, http.StatusNotFound, nil) 21 | } 22 | 23 | func (s *ReviewAPI) UpdateMine(c *gin.Context) { 24 | gb.ResponseWithError( 25 | c, 26 | e.NewError(e.ErrorCodeLicenseRequired, nil), 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/shared.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | const ( 4 | defaultQueryPage = "1" 5 | defaultQueryPerPage = "30" 6 | ) 7 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/tag.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | type ( 4 | TagAPI service 5 | ) 6 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/tag_get.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | gb "github.com/gitploy-io/gitploy/internal/server/global" 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | "go.uber.org/zap" 10 | ) 11 | 12 | func (s *TagAPI) Get(c *gin.Context) { 13 | ctx := c.Request.Context() 14 | 15 | var ( 16 | tag = c.Param("tag") 17 | ) 18 | 19 | uv, _ := c.Get(gb.KeyUser) 20 | u := uv.(*ent.User) 21 | 22 | rv, _ := c.Get(KeyRepo) 23 | repo := rv.(*ent.Repo) 24 | 25 | t, err := s.i.GetTag(ctx, u, repo, tag) 26 | if err != nil { 27 | s.log.Check(gb.GetZapLogLevel(err), "Failed to get the tag.").Write(zap.Error(err)) 28 | gb.ResponseWithError(c, err) 29 | return 30 | } 31 | 32 | gb.Response(c, http.StatusOK, t) 33 | } 34 | -------------------------------------------------------------------------------- /internal/server/api/v1/repos/tag_list.go: -------------------------------------------------------------------------------- 1 | package repos 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | i "github.com/gitploy-io/gitploy/internal/interactor" 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | "github.com/gitploy-io/gitploy/pkg/e" 14 | ) 15 | 16 | func (s *TagAPI) List(c *gin.Context) { 17 | ctx := c.Request.Context() 18 | 19 | var ( 20 | page int 21 | perPage int 22 | err error 23 | ) 24 | 25 | // Validate quries 26 | if page, err = strconv.Atoi(c.DefaultQuery("page", defaultQueryPage)); err != nil { 27 | s.log.Warn("Invalid parameter: page is not integer.", zap.Error(err)) 28 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 29 | return 30 | } 31 | 32 | if perPage, err = strconv.Atoi(c.DefaultQuery("per_page", defaultQueryPerPage)); err != nil { 33 | s.log.Warn("Invalid parameter: per_page is not integer.", zap.Error(err)) 34 | gb.ResponseWithError(c, e.NewError(e.ErrorCodeParameterInvalid, err)) 35 | return 36 | } 37 | 38 | uv, _ := c.Get(gb.KeyUser) 39 | u := uv.(*ent.User) 40 | 41 | rv, _ := c.Get(KeyRepo) 42 | repo := rv.(*ent.Repo) 43 | 44 | tags, err := s.i.ListTags(ctx, u, repo, &i.ListOptions{ 45 | Page: page, 46 | PerPage: perPage, 47 | }) 48 | if err != nil { 49 | s.log.Check(gb.GetZapLogLevel(err), "Failed to list tags.").Write(zap.Error(err)) 50 | gb.ResponseWithError(c, err) 51 | return 52 | } 53 | 54 | gb.Response(c, http.StatusOK, tags) 55 | } 56 | -------------------------------------------------------------------------------- /internal/server/api/v1/search/interface.go: -------------------------------------------------------------------------------- 1 | package search 2 | 3 | import ( 4 | "context" 5 | 6 | i "github.com/gitploy-io/gitploy/internal/interactor" 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | ) 9 | 10 | type ( 11 | Interactor interface { 12 | SearchDeploymentsOfUser(ctx context.Context, u *ent.User, opt *i.SearchDeploymentsOfUserOptions) ([]*ent.Deployment, error) 13 | SearchReviews(ctx context.Context, u *ent.User) ([]*ent.Review, error) 14 | } 15 | ) 16 | -------------------------------------------------------------------------------- /internal/server/api/v1/stream/events_oss.go: -------------------------------------------------------------------------------- 1 | // +build oss 2 | 3 | package stream 4 | 5 | import ( 6 | "time" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func (s *Stream) GetEvents(c *gin.Context) { 12 | w := c.Writer 13 | 14 | L: 15 | for { 16 | select { 17 | case <-w.CloseNotify(): 18 | break L 19 | case <-time.After(time.Minute): 20 | break L 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/server/api/v1/stream/interface.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | ) 8 | 9 | type ( 10 | Interactor interface { 11 | SubscribeEvent(fn func(e *ent.Event)) error 12 | UnsubscribeEvent(fn func(e *ent.Event)) error 13 | FindDeploymentByID(ctx context.Context, id int) (*ent.Deployment, error) 14 | FindDeploymentStatusByID(ctx context.Context, id int) (*ent.DeploymentStatus, error) 15 | FindReviewByID(ctx context.Context, id int) (*ent.Review, error) 16 | FindPermOfRepo(ctx context.Context, r *ent.Repo, u *ent.User) (*ent.Perm, error) 17 | } 18 | ) 19 | -------------------------------------------------------------------------------- /internal/server/api/v1/stream/stream.go: -------------------------------------------------------------------------------- 1 | package stream 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | type ( 8 | Stream struct { 9 | i Interactor 10 | log *zap.Logger 11 | } 12 | ) 13 | 14 | func NewStream(i Interactor) *Stream { 15 | return &Stream{ 16 | i: i, 17 | log: zap.L().Named("stream"), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/server/api/v1/sync/interface.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source ./interface.go -destination ./mock/interactor.go -package mock 2 | 3 | package sync 4 | 5 | import ( 6 | "context" 7 | "time" 8 | 9 | "github.com/gitploy-io/gitploy/model/ent" 10 | "github.com/gitploy-io/gitploy/model/extent" 11 | ) 12 | 13 | type ( 14 | Interactor interface { 15 | ListRemoteRepos(ctx context.Context, u *ent.User) ([]*extent.RemoteRepo, error) 16 | IsEntryOrg(ctx context.Context, namespace string) bool 17 | SyncRemoteRepo(ctx context.Context, u *ent.User, re *extent.RemoteRepo, t time.Time) error 18 | DeletePermsOfUserLessThanSyncedAt(ctx context.Context, u *ent.User, t time.Time) (int, error) 19 | } 20 | ) 21 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/delete.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | gb "github.com/gitploy-io/gitploy/internal/server/global" 11 | "github.com/gitploy-io/gitploy/pkg/e" 12 | ) 13 | 14 | func (u *UserAPI) Delete(c *gin.Context) { 15 | ctx := c.Request.Context() 16 | 17 | var ( 18 | id int64 19 | err error 20 | ) 21 | 22 | if id, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil { 23 | u.log.Warn("The id must be number.", zap.Error(err)) 24 | gb.ResponseWithError( 25 | c, 26 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "The id must be number.", err), 27 | ) 28 | return 29 | } 30 | 31 | du, err := u.i.FindUserByID(ctx, id) 32 | if err != nil { 33 | u.log.Check(gb.GetZapLogLevel(err), "Failed to find the user.").Write(zap.Error(err)) 34 | gb.ResponseWithError(c, err) 35 | return 36 | } 37 | 38 | if err := u.i.DeleteUser(ctx, du); err != nil { 39 | u.log.Check(gb.GetZapLogLevel(err), "Failed to delete the user.").Write(zap.Error(err)) 40 | gb.ResponseWithError(c, err) 41 | return 42 | } 43 | 44 | c.Status(http.StatusOK) 45 | } 46 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/get.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | gb "github.com/gitploy-io/gitploy/internal/server/global" 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | ) 12 | 13 | type ( 14 | // extendedUserData includes the 'hash' field. 15 | extendedUserData struct { 16 | *ent.User 17 | 18 | Hash string `json:"hash"` 19 | } 20 | ) 21 | 22 | func (u *UserAPI) GetMe(c *gin.Context) { 23 | ctx := c.Request.Context() 24 | 25 | v, _ := c.Get(gb.KeyUser) 26 | uv, _ := v.(*ent.User) 27 | 28 | uv, err := u.i.FindUserByID(ctx, uv.ID) 29 | if err != nil { 30 | u.log.Check(gb.GetZapLogLevel(err), "Failed to find the user.").Write(zap.Error(err)) 31 | gb.ResponseWithError(c, err) 32 | return 33 | } 34 | 35 | gb.Response(c, http.StatusOK, extendedUserData{ 36 | User: uv, 37 | Hash: uv.Hash, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/get_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "encoding/json" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/gitploy-io/gitploy/internal/server/api/v1/users/mock" 11 | "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | gm "github.com/golang/mock/gomock" 14 | ) 15 | 16 | func init() { 17 | gin.SetMode(gin.ReleaseMode) 18 | } 19 | 20 | func TestUsers_GetMe(t *testing.T) { 21 | t.Run("Return user's information with the 'hash' field.", func(t *testing.T) { 22 | const hash = "HASH_VALUE" 23 | 24 | t.Log("Start mocking:") 25 | ctrl := gm.NewController(t) 26 | i := mock.NewMockInteractor(ctrl) 27 | 28 | t.Log("\tFind the user.") 29 | i.EXPECT(). 30 | FindUserByID(gm.Any(), gm.AssignableToTypeOf(int64(1))). 31 | Return(&ent.User{Hash: hash}, nil) 32 | 33 | api := NewUserAPI(i) 34 | r := gin.New() 35 | r.GET("/user", 36 | func(c *gin.Context) { 37 | c.Set(global.KeyUser, &ent.User{}) 38 | }, 39 | api.GetMe) 40 | 41 | req, _ := http.NewRequest("GET", "/user", nil) 42 | w := httptest.NewRecorder() 43 | r.ServeHTTP(w, req) 44 | 45 | t.Log("Evaluate the return value.") 46 | if w.Code != http.StatusOK { 47 | t.Fatalf("Code = %v, wanted %v", w.Code, http.StatusOK) 48 | } 49 | 50 | d := extendedUserData{} 51 | if err := json.Unmarshal(w.Body.Bytes(), &d); err != nil { 52 | t.Fatalf("Failed to unmarshal: %v", err) 53 | } 54 | 55 | if d.Hash != hash { 56 | t.Fatalf("Hash = %v, wanted %v", d.Hash, hash) 57 | } 58 | }) 59 | } 60 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/interface.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source ./interface.go -destination ./mock/interactor.go -package mock 2 | 3 | package users 4 | 5 | import ( 6 | "context" 7 | 8 | i "github.com/gitploy-io/gitploy/internal/interactor" 9 | "github.com/gitploy-io/gitploy/model/ent" 10 | "github.com/gitploy-io/gitploy/model/extent" 11 | ) 12 | 13 | type ( 14 | Interactor interface { 15 | SearchUsers(ctx context.Context, opts *i.SearchUsersOptions) ([]*ent.User, error) 16 | FindUserByID(ctx context.Context, id int64) (*ent.User, error) 17 | GetRateLimit(ctx context.Context, u *ent.User) (*extent.RateLimit, error) 18 | UpdateUser(ctx context.Context, u *ent.User) (*ent.User, error) 19 | DeleteUser(ctx context.Context, u *ent.User) error 20 | } 21 | ) 22 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/list.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "go.uber.org/zap" 9 | 10 | i "github.com/gitploy-io/gitploy/internal/interactor" 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | func (u *UserAPI) List(c *gin.Context) { 16 | ctx := c.Request.Context() 17 | 18 | var ( 19 | q = c.DefaultQuery("q", "") 20 | p int 21 | pp int 22 | err error 23 | ) 24 | 25 | if p, err = strconv.Atoi(c.DefaultQuery("page", "1")); err != nil { 26 | gb.ResponseWithError( 27 | c, 28 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "The page must be number.", err), 29 | ) 30 | } 31 | 32 | if pp, err = strconv.Atoi(c.DefaultQuery("per_page", "30")); err != nil { 33 | gb.ResponseWithError( 34 | c, 35 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "The per_page must be number.", err), 36 | ) 37 | } 38 | 39 | us, err := u.i.SearchUsers(ctx, &i.SearchUsersOptions{ 40 | Query: q, 41 | ListOptions: i.ListOptions{Page: p, PerPage: pp}, 42 | }) 43 | if err != nil { 44 | u.log.Check(gb.GetZapLogLevel(err), "Failed to list users.").Write(zap.Error(err)) 45 | gb.ResponseWithError(c, err) 46 | return 47 | } 48 | 49 | gb.Response(c, http.StatusOK, us) 50 | } 51 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/middleware.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | gb "github.com/gitploy-io/gitploy/internal/server/global" 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | ) 10 | 11 | type ( 12 | UserMiddleware struct{} 13 | ) 14 | 15 | func NewUserMiddleware() *UserMiddleware { 16 | return &UserMiddleware{} 17 | } 18 | 19 | func (m *UserMiddleware) AdminOnly() gin.HandlerFunc { 20 | return func(c *gin.Context) { 21 | v, _ := c.Get(gb.KeyUser) 22 | u, _ := v.(*ent.User) 23 | 24 | if !u.Admin { 25 | c.AbortWithStatusJSON(http.StatusForbidden, map[string]string{ 26 | "message": "Only admin can access.", 27 | }) 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/rate_limit.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/gin-gonic/gin" 7 | "go.uber.org/zap" 8 | 9 | gb "github.com/gitploy-io/gitploy/internal/server/global" 10 | "github.com/gitploy-io/gitploy/model/ent" 11 | "github.com/gitploy-io/gitploy/model/extent" 12 | ) 13 | 14 | func (u *UserAPI) GetRateLimit(c *gin.Context) { 15 | ctx := c.Request.Context() 16 | 17 | v, _ := c.Get(gb.KeyUser) 18 | uv, _ := v.(*ent.User) 19 | 20 | var ( 21 | rl *extent.RateLimit 22 | err error 23 | ) 24 | 25 | if rl, err = u.i.GetRateLimit(ctx, uv); err != nil { 26 | u.log.Check(gb.GetZapLogLevel(err), "Failed to get the rate-limit.").Write(zap.Error(err)) 27 | gb.ResponseWithError(c, err) 28 | return 29 | } 30 | 31 | gb.Response(c, http.StatusOK, rl) 32 | } 33 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/shared.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | ) 6 | 7 | type ( 8 | UserAPI struct { 9 | i Interactor 10 | log *zap.Logger 11 | } 12 | ) 13 | 14 | func NewUserAPI(i Interactor) *UserAPI { 15 | return &UserAPI{ 16 | i: i, 17 | log: zap.L().Named("users"), 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/update.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "net/http" 5 | "strconv" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/gin-gonic/gin/binding" 9 | "go.uber.org/zap" 10 | 11 | gb "github.com/gitploy-io/gitploy/internal/server/global" 12 | "github.com/gitploy-io/gitploy/pkg/e" 13 | ) 14 | 15 | type ( 16 | userPatchPayload struct { 17 | Admin *bool `json:"admin"` 18 | } 19 | ) 20 | 21 | func (u *UserAPI) Update(c *gin.Context) { 22 | ctx := c.Request.Context() 23 | 24 | var ( 25 | id int64 26 | err error 27 | ) 28 | 29 | if id, err = strconv.ParseInt(c.Param("id"), 10, 64); err != nil { 30 | u.log.Warn("The id must be number.", zap.Error(err)) 31 | gb.ResponseWithError( 32 | c, 33 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "The id must be number.", err), 34 | ) 35 | return 36 | } 37 | 38 | p := &userPatchPayload{} 39 | if err := c.ShouldBindBodyWith(p, binding.JSON); err != nil { 40 | u.log.Warn("It has failed to binding the payload.", zap.Error(err)) 41 | gb.ResponseWithError( 42 | c, 43 | e.NewErrorWithMessage(e.ErrorCodeParameterInvalid, "It has failed to binding the payload.", err), 44 | ) 45 | return 46 | } 47 | 48 | du, err := u.i.FindUserByID(ctx, id) 49 | if err != nil { 50 | u.log.Check(gb.GetZapLogLevel(err), "Failed to find the user.").Write(zap.Error(err)) 51 | gb.ResponseWithError(c, err) 52 | return 53 | } 54 | 55 | if p.Admin != nil { 56 | du.Admin = *p.Admin 57 | if du, err = u.i.UpdateUser(ctx, du); err != nil { 58 | u.log.Check(gb.GetZapLogLevel(err), "Failed to update the user.").Write(zap.Error(err)) 59 | gb.ResponseWithError(c, err) 60 | return 61 | } 62 | } 63 | 64 | gb.Response(c, http.StatusOK, du) 65 | } 66 | -------------------------------------------------------------------------------- /internal/server/api/v1/users/update_test.go: -------------------------------------------------------------------------------- 1 | package users 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "net/http/httptest" 10 | "testing" 11 | 12 | "github.com/AlekSi/pointer" 13 | "github.com/gin-gonic/gin" 14 | "github.com/gitploy-io/gitploy/internal/server/api/v1/users/mock" 15 | "github.com/gitploy-io/gitploy/model/ent" 16 | "github.com/golang/mock/gomock" 17 | ) 18 | 19 | func TestUserAPI_UpdateUser(t *testing.T) { 20 | input := struct { 21 | ID int64 22 | Payload *userPatchPayload 23 | }{ 24 | ID: 1, 25 | Payload: &userPatchPayload{ 26 | Admin: pointer.ToBool(true), 27 | }, 28 | } 29 | 30 | ctrl := gomock.NewController(t) 31 | m := mock.NewMockInteractor(ctrl) 32 | 33 | ctx := gomock.Any() 34 | 35 | t.Log("FindUserByID returns non-admin user.") 36 | m. 37 | EXPECT(). 38 | FindUserByID(ctx, input.ID). 39 | Return(&ent.User{ 40 | ID: input.ID, 41 | Admin: false, 42 | }, nil) 43 | 44 | t.Log("UpdateUser updates the user admin.") 45 | m. 46 | EXPECT(). 47 | UpdateUser(ctx, gomock.Eq(&ent.User{ 48 | ID: input.ID, 49 | Admin: true, 50 | })). 51 | DoAndReturn(func(ctx context.Context, u *ent.User) (*ent.User, error) { 52 | return u, nil 53 | }) 54 | 55 | gin.SetMode(gin.ReleaseMode) 56 | r := gin.New() 57 | r.PATCH("/users/:id", NewUserAPI(m).Update) 58 | 59 | p, _ := json.Marshal(input.Payload) 60 | req, _ := http.NewRequest("PATCH", fmt.Sprintf("/users/%d", input.ID), bytes.NewBuffer(p)) 61 | 62 | w := httptest.NewRecorder() 63 | r.ServeHTTP(w, req) 64 | 65 | if w.Code != http.StatusOK { 66 | t.Fatalf("Code = %v, wanted %v", w.Code, http.StatusOK) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /internal/server/global/helper.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "go.uber.org/zap/zapcore" 5 | 6 | "github.com/gitploy-io/gitploy/pkg/e" 7 | ) 8 | 9 | // GetZapLogLevel return the warning level if the error is managed in the system. 10 | func GetZapLogLevel(err error) zapcore.Level { 11 | if !e.IsError(err) { 12 | return zapcore.ErrorLevel 13 | } 14 | 15 | return zapcore.WarnLevel 16 | } 17 | -------------------------------------------------------------------------------- /internal/server/global/interface.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | ) 8 | 9 | type ( 10 | Interactor interface { 11 | FindUserByHash(ctx context.Context, hash string) (*ent.User, error) 12 | } 13 | ) 14 | -------------------------------------------------------------------------------- /internal/server/global/keys.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | const ( 4 | CookieSession = "__sess__" 5 | ) 6 | 7 | // Gin context values. 8 | const ( 9 | KeyUser = "gitploy.user" 10 | ) 11 | -------------------------------------------------------------------------------- /internal/server/global/middleware.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/gin-gonic/gin" 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | ) 9 | 10 | type ( 11 | Middleware struct { 12 | i Interactor 13 | } 14 | ) 15 | 16 | func NewMiddleware(i Interactor) *Middleware { 17 | return &Middleware{ 18 | i: i, 19 | } 20 | } 21 | 22 | func (s *Middleware) SetUser() gin.HandlerFunc { 23 | return func(c *gin.Context) { 24 | ctx := c.Request.Context() 25 | 26 | u, err := s.i.FindUserByHash(ctx, FindHash(c)) 27 | if ent.IsNotFound(err) { 28 | return 29 | } 30 | 31 | c.Set(KeyUser, u) 32 | } 33 | } 34 | 35 | func FindHash(c *gin.Context) string { 36 | s, _ := c.Cookie(CookieSession) 37 | if s != "" { 38 | return s 39 | } 40 | 41 | header := c.GetHeader("Authorization") 42 | s = strings.TrimPrefix(header, "Bearer ") 43 | if s != "" { 44 | return s 45 | } 46 | 47 | return "" 48 | } 49 | -------------------------------------------------------------------------------- /internal/server/hooks/interface.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source ./interface.go -destination ./mock/interactor.go -package mock 2 | 3 | package hooks 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | "github.com/gitploy-io/gitploy/model/extent" 10 | ) 11 | 12 | type ( 13 | Interactor interface { 14 | FindRepoByID(ctx context.Context, id int64) (*ent.Repo, error) 15 | FindDeploymentByUID(ctx context.Context, uid int64) (*ent.Deployment, error) 16 | Deploy(ctx context.Context, u *ent.User, r *ent.Repo, d *ent.Deployment, env *extent.Env) (*ent.Deployment, error) 17 | UpdateDeployment(ctx context.Context, d *ent.Deployment) (*ent.Deployment, error) 18 | CreateDeploymentStatus(ctx context.Context, ds *ent.DeploymentStatus) (*ent.DeploymentStatus, error) 19 | ProduceDeploymentStatisticsOfRepo(ctx context.Context, r *ent.Repo, d *ent.Deployment) (*ent.DeploymentStatistics, error) 20 | GetEvaluatedConfig(ctx context.Context, u *ent.User, r *ent.Repo, v *extent.EvalValues) (*extent.Config, error) 21 | } 22 | ) 23 | -------------------------------------------------------------------------------- /internal/server/metrics/interface.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "context" 5 | "time" 6 | 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | "github.com/gitploy-io/gitploy/model/extent" 9 | ) 10 | 11 | type ( 12 | Interactor interface { 13 | CountActiveRepos(ctx context.Context) (int, error) 14 | CountRepos(ctx context.Context) (int, error) 15 | ListAllDeploymentStatistics(ctx context.Context) ([]*ent.DeploymentStatistics, error) 16 | ListDeploymentStatisticsGreaterThanTime(ctx context.Context, updated time.Time) ([]*ent.DeploymentStatistics, error) 17 | GetLicense(ctx context.Context) (*extent.License, error) 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /internal/server/metrics/metrics_oss.go: -------------------------------------------------------------------------------- 1 | // +build oss 2 | 3 | package metrics 4 | 5 | import ( 6 | "net/http" 7 | 8 | "github.com/gin-gonic/gin" 9 | ) 10 | 11 | func NewMetric(c *MetricConfig) *Metric { 12 | return &Metric{} 13 | } 14 | 15 | func (m *Metric) CollectMetrics(c *gin.Context) { 16 | c.Status(http.StatusOK) 17 | } 18 | -------------------------------------------------------------------------------- /internal/server/metrics/middleware.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "github.com/gin-gonic/gin" 8 | "github.com/prometheus/client_golang/prometheus" 9 | ) 10 | 11 | const ( 12 | namespace = "gitploy" 13 | ) 14 | 15 | var ( 16 | RequestCount = prometheus.NewCounterVec(prometheus.CounterOpts{ 17 | Namespace: namespace, 18 | Subsystem: "", 19 | Name: "requests_total", 20 | Help: "How many HTTP requests processed, partitioned by status code and HTTP method.", 21 | }, []string{"code", "method", "path"}) 22 | 23 | RequestDuration = prometheus.NewHistogramVec(prometheus.HistogramOpts{ 24 | Namespace: namespace, 25 | Subsystem: "", 26 | Name: "request_duration_seconds", 27 | Help: "The HTTP request latencies in seconds.", 28 | }, []string{"code", "method", "path"}) 29 | ) 30 | 31 | func init() { 32 | prometheus.MustRegister(RequestCount) 33 | prometheus.MustRegister(RequestDuration) 34 | } 35 | 36 | // CollectRequestMetrics is the middleware to collect metrics for the request. 37 | func CollectRequestMetrics() gin.HandlerFunc { 38 | return func(c *gin.Context) { 39 | start := time.Now() 40 | 41 | c.Next() 42 | 43 | status := strconv.Itoa(c.Writer.Status()) 44 | 45 | { 46 | RequestCount.WithLabelValues(status, c.Request.Method, c.Request.URL.Path).Inc() 47 | } 48 | { 49 | elapsed := float64(time.Since(start)) / float64(time.Second) 50 | RequestDuration.WithLabelValues(status, c.Request.Method, c.Request.URL.Path).Observe(elapsed) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /internal/server/metrics/types.go: -------------------------------------------------------------------------------- 1 | package metrics 2 | 3 | type ( 4 | Metric struct { 5 | prometheusAuthSecret string 6 | } 7 | 8 | MetricConfig struct { 9 | Interactor 10 | PrometheusAuthSecret string 11 | } 12 | ) 13 | -------------------------------------------------------------------------------- /internal/server/slack/interface.go: -------------------------------------------------------------------------------- 1 | //go:generate mockgen -source ./interface.go -destination ./mock/interactor.go -package mock 2 | 3 | package slack 4 | 5 | import ( 6 | "context" 7 | 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | ) 10 | 11 | type ( 12 | Interactor interface { 13 | FindUserByID(ctx context.Context, id int64) (*ent.User, error) 14 | 15 | FindChatUserByID(ctx context.Context, id string) (*ent.ChatUser, error) 16 | CreateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) 17 | UpdateChatUser(ctx context.Context, cu *ent.ChatUser) (*ent.ChatUser, error) 18 | DeleteChatUser(ctx context.Context, cu *ent.ChatUser) error 19 | 20 | FindDeploymentByID(ctx context.Context, id int) (*ent.Deployment, error) 21 | FindDeploymentStatusByID(ctx context.Context, id int) (*ent.DeploymentStatus, error) 22 | 23 | FindReviewByID(ctx context.Context, id int) (*ent.Review, error) 24 | 25 | SubscribeEvent(fn func(e *ent.Event)) error 26 | UnsubscribeEvent(fn func(e *ent.Event)) error 27 | } 28 | ) 29 | -------------------------------------------------------------------------------- /internal/server/slack/slack.go: -------------------------------------------------------------------------------- 1 | // Copyright 2021 Gitploy.IO Inc. All rights reserved. 2 | // Use of this source code is governed by the Gitploy Non-Commercial License 3 | // that can be found in the LICENSE file. 4 | 5 | //go:build !oss 6 | 7 | package slack 8 | 9 | import ( 10 | "context" 11 | 12 | "github.com/gitploy-io/gitploy/model/ent" 13 | "go.uber.org/zap" 14 | ) 15 | 16 | func NewSlack(c *SlackConfig) *Slack { 17 | s := &Slack{ 18 | host: c.ServerHost, 19 | proto: c.ServerProto, 20 | c: c.Config, 21 | i: c.Interactor, 22 | log: zap.L().Named("slack"), 23 | } 24 | 25 | s.i.SubscribeEvent(func(e *ent.Event) { 26 | s.Notify(context.Background(), e) 27 | }) 28 | 29 | return s 30 | } 31 | -------------------------------------------------------------------------------- /internal/server/slack/slack_oss.go: -------------------------------------------------------------------------------- 1 | //go:build oss 2 | 3 | package slack 4 | 5 | func NewSlack(c *SlackConfig) *Slack { 6 | return &Slack{} 7 | } 8 | -------------------------------------------------------------------------------- /internal/server/slack/types.go: -------------------------------------------------------------------------------- 1 | package slack 2 | 3 | import ( 4 | "go.uber.org/zap" 5 | "golang.org/x/oauth2" 6 | ) 7 | 8 | type ( 9 | Slack struct { 10 | host string 11 | proto string 12 | 13 | c *oauth2.Config 14 | i Interactor 15 | 16 | log *zap.Logger 17 | } 18 | 19 | SlackConfig struct { 20 | ServerHost string 21 | ServerProto string 22 | *oauth2.Config 23 | Interactor 24 | } 25 | ) 26 | -------------------------------------------------------------------------------- /internal/server/web/interface.go: -------------------------------------------------------------------------------- 1 | package web 2 | 3 | import ( 4 | "context" 5 | 6 | i "github.com/gitploy-io/gitploy/internal/interactor" 7 | "github.com/gitploy-io/gitploy/model/ent" 8 | "github.com/gitploy-io/gitploy/model/extent" 9 | ) 10 | 11 | type ( 12 | Interactor interface { 13 | FindUserByID(ctx context.Context, id int64) (*ent.User, error) 14 | IsAdminUser(ctx context.Context, login string) bool 15 | IsEntryMember(ctx context.Context, login string) bool 16 | IsOrgMember(ctx context.Context, orgs []string) bool 17 | CreateUser(ctx context.Context, u *ent.User) (*ent.User, error) 18 | UpdateUser(ctx context.Context, u *ent.User) (*ent.User, error) 19 | // Fetch the user information from SCM. 20 | // It has the id, login, avatar and so on. 21 | GetRemoteUserByToken(ctx context.Context, token string) (*extent.RemoteUser, error) 22 | ListRemoteOrgsByToken(ctx context.Context, token string) ([]string, error) 23 | FindUserByHash(ctx context.Context, hash string) (*ent.User, error) 24 | FindRepoOfUserByNamespaceName(ctx context.Context, u *ent.User, opt *i.FindRepoOfUserByNamespaceNameOptions) (*ent.Repo, error) 25 | GetConfigRedirectURL(ctx context.Context, u *ent.User, r *ent.Repo) (string, error) 26 | GetNewConfigRedirectURL(ctx context.Context, u *ent.User, r *ent.Repo) (string, error) 27 | GetLicense(ctx context.Context) (*extent.License, error) 28 | } 29 | ) 30 | -------------------------------------------------------------------------------- /model/ent/context.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package ent 4 | 5 | import ( 6 | "context" 7 | ) 8 | 9 | type clientCtxKey struct{} 10 | 11 | // FromContext returns a Client stored inside a context, or nil if there isn't one. 12 | func FromContext(ctx context.Context) *Client { 13 | c, _ := ctx.Value(clientCtxKey{}).(*Client) 14 | return c 15 | } 16 | 17 | // NewContext returns a new context with the given Client attached. 18 | func NewContext(parent context.Context, c *Client) context.Context { 19 | return context.WithValue(parent, clientCtxKey{}, c) 20 | } 21 | 22 | type txCtxKey struct{} 23 | 24 | // TxFromContext returns a Tx stored inside a context, or nil if there isn't one. 25 | func TxFromContext(ctx context.Context) *Tx { 26 | tx, _ := ctx.Value(txCtxKey{}).(*Tx) 27 | return tx 28 | } 29 | 30 | // NewTxContext returns a new context with the given Tx attached. 31 | func NewTxContext(parent context.Context, tx *Tx) context.Context { 32 | return context.WithValue(parent, txCtxKey{}, tx) 33 | } 34 | -------------------------------------------------------------------------------- /model/ent/custom_client.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | func (c *Client) GetDriverDialect() string { 4 | return c.config.driver.Dialect() 5 | } 6 | -------------------------------------------------------------------------------- /model/ent/custom_deployment.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | import ( 4 | "github.com/gitploy-io/gitploy/model/ent/deployment" 5 | ) 6 | 7 | func (d *Deployment) GetShortRef() string { 8 | const maxlen = 7 9 | 10 | if d.Type == deployment.TypeCommit && 11 | len(d.Ref) > maxlen { 12 | return d.Ref[:7] 13 | } 14 | 15 | return d.Ref 16 | } 17 | -------------------------------------------------------------------------------- /model/ent/custom_repo.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | import "fmt" 4 | 5 | func (r *Repo) GetFullName() string { 6 | return fmt.Sprintf("%s/%s", r.Namespace, r.Name) 7 | } 8 | -------------------------------------------------------------------------------- /model/ent/custom_tx.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | func (tx *Tx) GetDriverDialect() string { 4 | return tx.config.driver.Dialect() 5 | } 6 | -------------------------------------------------------------------------------- /model/ent/entc.go: -------------------------------------------------------------------------------- 1 | //go:build ignore 2 | 3 | package main 4 | 5 | import ( 6 | "fmt" 7 | "log" 8 | 9 | "entgo.io/ent/entc" 10 | "entgo.io/ent/entc/gen" 11 | ) 12 | 13 | // Code generation hook 14 | // https://entgo.io/docs/code-gen/#code-generation-hooks 15 | // https://github.com/ent/ent/tree/master/examples/entcpkg 16 | func main() { 17 | err := entc.Generate("./schema", &gen.Config{ 18 | Features: []gen.Feature{ 19 | gen.FeatureLock, 20 | }, 21 | Hooks: []gen.Hook{ 22 | TagFields("json"), 23 | }, 24 | }) 25 | if err != nil { 26 | log.Fatalf("running ent codegen: %v", err) 27 | } 28 | } 29 | 30 | // TagFields tags all fields defined in the schema with the given struct-tag. 31 | // To remove omitempty for json tag. 32 | func TagFields(name string) gen.Hook { 33 | return func(next gen.Generator) gen.Generator { 34 | return gen.GenerateFunc(func(g *gen.Graph) error { 35 | for _, node := range g.Nodes { 36 | for _, field := range node.Fields { 37 | if field.Optional || field.Nillable { 38 | field.StructTag = fmt.Sprintf("%s:\"%s,omitemtpy\"", name, field.Name) 39 | } else { 40 | field.StructTag = fmt.Sprintf("%s:%q", name, field.Name) 41 | } 42 | } 43 | } 44 | return next.Generate(g) 45 | }) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /model/ent/generate.go: -------------------------------------------------------------------------------- 1 | package ent 2 | 3 | //go:generate go run entc.go 4 | -------------------------------------------------------------------------------- /model/ent/notificationrecord/notificationrecord.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package notificationrecord 4 | 5 | const ( 6 | // Label holds the string label denoting the notificationrecord type in the database. 7 | Label = "notification_record" 8 | // FieldID holds the string denoting the id field in the database. 9 | FieldID = "id" 10 | // FieldEventID holds the string denoting the event_id field in the database. 11 | FieldEventID = "event_id" 12 | // EdgeEvent holds the string denoting the event edge name in mutations. 13 | EdgeEvent = "event" 14 | // Table holds the table name of the notificationrecord in the database. 15 | Table = "notification_records" 16 | // EventTable is the table that holds the event relation/edge. 17 | EventTable = "notification_records" 18 | // EventInverseTable is the table name for the Event entity. 19 | // It exists in this package in order to avoid circular dependency with the "event" package. 20 | EventInverseTable = "events" 21 | // EventColumn is the table column denoting the event relation/edge. 22 | EventColumn = "event_id" 23 | ) 24 | 25 | // Columns holds all SQL columns for notificationrecord fields. 26 | var Columns = []string{ 27 | FieldID, 28 | FieldEventID, 29 | } 30 | 31 | // ValidColumn reports if the column name is valid (part of the table columns). 32 | func ValidColumn(column string) bool { 33 | for i := range Columns { 34 | if column == Columns[i] { 35 | return true 36 | } 37 | } 38 | return false 39 | } 40 | -------------------------------------------------------------------------------- /model/ent/predicate/predicate.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package predicate 4 | 5 | import ( 6 | "entgo.io/ent/dialect/sql" 7 | ) 8 | 9 | // ChatUser is the predicate function for chatuser builders. 10 | type ChatUser func(*sql.Selector) 11 | 12 | // Deployment is the predicate function for deployment builders. 13 | type Deployment func(*sql.Selector) 14 | 15 | // DeploymentStatistics is the predicate function for deploymentstatistics builders. 16 | type DeploymentStatistics func(*sql.Selector) 17 | 18 | // DeploymentStatus is the predicate function for deploymentstatus builders. 19 | type DeploymentStatus func(*sql.Selector) 20 | 21 | // Event is the predicate function for event builders. 22 | type Event func(*sql.Selector) 23 | 24 | // Lock is the predicate function for lock builders. 25 | type Lock func(*sql.Selector) 26 | 27 | // NotificationRecord is the predicate function for notificationrecord builders. 28 | type NotificationRecord func(*sql.Selector) 29 | 30 | // Perm is the predicate function for perm builders. 31 | type Perm func(*sql.Selector) 32 | 33 | // Repo is the predicate function for repo builders. 34 | type Repo func(*sql.Selector) 35 | 36 | // Review is the predicate function for review builders. 37 | type Review func(*sql.Selector) 38 | 39 | // User is the predicate function for user builders. 40 | type User func(*sql.Selector) 41 | -------------------------------------------------------------------------------- /model/ent/runtime/runtime.go: -------------------------------------------------------------------------------- 1 | // Code generated by entc, DO NOT EDIT. 2 | 3 | package runtime 4 | 5 | // The schema-stitching logic is generated in github.com/gitploy-io/gitploy/model/ent/runtime.go 6 | 7 | const ( 8 | Version = "v0.10.1" // Version of ent codegen. 9 | Sum = "h1:dM5h4Zk6yHGIgw4dCqVzGw3nWgpGYJiV4/kyHEF6PFo=" // Sum of ent codegen. 10 | ) 11 | -------------------------------------------------------------------------------- /model/ent/schema/chatuser.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | // ChatUser holds the schema definition for the ChatUser entity. 10 | type ChatUser struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the ChatUser. 15 | func (ChatUser) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.String("id"), 18 | field.String("token"). 19 | Sensitive(), 20 | field.String("refresh"). 21 | Sensitive(), 22 | field.Time("expiry"), 23 | field.String("bot_token"). 24 | Sensitive(), 25 | field.Time("created_at"). 26 | Default(nowUTC), 27 | field.Time("updated_at"). 28 | Default(nowUTC). 29 | UpdateDefault(nowUTC), 30 | field.Int64("user_id"), 31 | } 32 | } 33 | 34 | // Edges of the ChatUser. 35 | func (ChatUser) Edges() []ent.Edge { 36 | return []ent.Edge{ 37 | edge.From("user", User.Type). 38 | Ref("chat_user"). 39 | Field("user_id"). 40 | Unique(). 41 | Required(), 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /model/ent/schema/deploymentstatistics.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | ) 9 | 10 | // DeploymentStatistics holds the schema definition for the DeploymentStatistics entity. 11 | type DeploymentStatistics struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the DeploymentStatistics. 16 | func (DeploymentStatistics) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("env"), 19 | field.Int("count"). 20 | Default(0), 21 | field.Int("rollback_count"). 22 | Default(0), 23 | field.Int("additions"). 24 | Default(0), 25 | field.Int("deletions"). 26 | Default(0), 27 | field.Int("changes"). 28 | Default(0), 29 | field.Int("lead_time_seconds"). 30 | Default(0), 31 | field.Int("commit_count"). 32 | Default(0), 33 | field.Time("created_at"). 34 | Default(nowUTC), 35 | field.Time("updated_at"). 36 | Default(nowUTC). 37 | UpdateDefault(nowUTC), 38 | field.Int64("repo_id"), 39 | } 40 | } 41 | 42 | // Edges of the DeploymentStatistics. 43 | func (DeploymentStatistics) Edges() []ent.Edge { 44 | return []ent.Edge{ 45 | edge.From("repo", Repo.Type). 46 | Ref("deployment_statistics"). 47 | Field("repo_id"). 48 | Unique(). 49 | Required(), 50 | } 51 | } 52 | 53 | func (DeploymentStatistics) Indexes() []ent.Index { 54 | return []ent.Index{ 55 | index.Fields("repo_id", "env"). 56 | Unique(), 57 | // The collector searches updated records only. 58 | index.Fields("updated_at"), 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /model/ent/schema/deploymentstatus.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect/entsql" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | // DeploymentStatus holds the schema definition for the DeploymentStatus entity. 11 | type DeploymentStatus struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the DeploymentStatus. 16 | func (DeploymentStatus) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("status"), 19 | field.String("description"). 20 | Optional(), 21 | field.String("log_url"). 22 | Optional(), 23 | field.Time("created_at"). 24 | Default(nowUTC), 25 | field.Time("updated_at"). 26 | Default(nowUTC). 27 | UpdateDefault(nowUTC), 28 | 29 | // edges 30 | field.Int("deployment_id"), 31 | // Denormalize the 'repo_id' field so that 32 | // we can figure out the repository easily. 33 | field.Int64("repo_id"). 34 | Optional(), 35 | } 36 | } 37 | 38 | // Edges of the DeploymentStatus. 39 | func (DeploymentStatus) Edges() []ent.Edge { 40 | return []ent.Edge{ 41 | edge.From("deployment", Deployment.Type). 42 | Ref("deployment_statuses"). 43 | Field("deployment_id"). 44 | Unique(). 45 | Required(), 46 | edge.From("repo", Repo.Type). 47 | Ref("deployment_statuses"). 48 | Field("repo_id"). 49 | Unique(), 50 | edge.To("event", Event.Type). 51 | Annotations(entsql.Annotation{ 52 | OnDelete: entsql.Cascade, 53 | }), 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /model/ent/schema/event.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect/entsql" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/index" 9 | ) 10 | 11 | // Event holds the schema definition for the Event entity. 12 | type Event struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the Event. 17 | func (Event) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Enum("kind"). 20 | Values( 21 | "deployment_status", 22 | "review", 23 | ), 24 | field.Enum("type"). 25 | Values( 26 | "created", 27 | "updated", 28 | "deleted", 29 | ), 30 | field.Time("created_at"). 31 | Default(nowUTC), 32 | field.Int("deployment_status_id"). 33 | Optional(), 34 | field.Int("review_id"). 35 | Optional(), 36 | // This field is filled when the type is 'deleted'. 37 | field.Int("deleted_id"). 38 | Optional(), 39 | } 40 | } 41 | 42 | // Edges of the Event. 43 | func (Event) Edges() []ent.Edge { 44 | return []ent.Edge{ 45 | edge.From("deployment_status", DeploymentStatus.Type). 46 | Ref("event"). 47 | Field("deployment_status_id"). 48 | Unique(), 49 | edge.From("review", Review.Type). 50 | Ref("event"). 51 | Field("review_id"). 52 | Unique(), 53 | edge.To("notification_record", NotificationRecord.Type). 54 | Annotations(entsql.Annotation{ 55 | OnDelete: entsql.SetNull, 56 | }). 57 | Unique(), 58 | } 59 | } 60 | 61 | func (Event) Indexes() []ent.Index { 62 | return []ent.Index{ 63 | index.Fields("created_at"), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /model/ent/schema/lock.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | "entgo.io/ent/schema/index" 8 | ) 9 | 10 | // Lock holds the schema definition for the Lock entity. 11 | type Lock struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Lock. 16 | func (Lock) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.String("env"), 19 | field.Time("expired_at"). 20 | Optional(). 21 | Nillable(), 22 | field.Time("created_at"). 23 | Default(nowUTC), 24 | // Edges 25 | field.Int64("user_id"). 26 | Optional(), 27 | field.Int64("repo_id"), 28 | } 29 | } 30 | 31 | // Edges of the Lock. 32 | func (Lock) Edges() []ent.Edge { 33 | return []ent.Edge{ 34 | edge.From("user", User.Type). 35 | Ref("locks"). 36 | Field("user_id"). 37 | Unique(), 38 | edge.From("repo", Repo.Type). 39 | Ref("locks"). 40 | Field("repo_id"). 41 | Unique(). 42 | Required(), 43 | } 44 | } 45 | 46 | func (Lock) Indexes() []ent.Index { 47 | return []ent.Index{ 48 | index.Fields("repo_id", "env"). 49 | Unique(), 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /model/ent/schema/notificationrecord.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/schema/edge" 6 | "entgo.io/ent/schema/field" 7 | ) 8 | 9 | // NotificationRecord holds the schema definition for the NotificationRecord entity. 10 | type NotificationRecord struct { 11 | ent.Schema 12 | } 13 | 14 | // Fields of the NotificationRecord. 15 | func (NotificationRecord) Fields() []ent.Field { 16 | return []ent.Field{ 17 | field.Int("event_id"). 18 | Optional(), 19 | } 20 | } 21 | 22 | // Edges of the NotificationRecord. 23 | func (NotificationRecord) Edges() []ent.Edge { 24 | return []ent.Edge{ 25 | edge.From("event", Event.Type). 26 | Ref("notification_record"). 27 | Field("event_id"). 28 | Unique(), 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /model/ent/schema/perm.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | "entgo.io/ent/schema/index" 9 | ) 10 | 11 | // Perm holds the schema definition for the Perm entity. 12 | type Perm struct { 13 | ent.Schema 14 | } 15 | 16 | // Fields of the Perm. 17 | func (Perm) Fields() []ent.Field { 18 | return []ent.Field{ 19 | field.Enum("repo_perm"). 20 | Values( 21 | "read", 22 | "write", 23 | "admin", 24 | ). 25 | Default("read"), 26 | field.Time("synced_at"). 27 | Optional(). 28 | SchemaType(map[string]string{ 29 | dialect.MySQL: "timestamp(6)", 30 | }), 31 | field.Time("created_at"). 32 | Default(nowUTC), 33 | field.Time("updated_at"). 34 | Default(nowUTC). 35 | UpdateDefault(nowUTC), 36 | // Edges 37 | field.Int64("user_id"), 38 | field.Int64("repo_id"), 39 | } 40 | } 41 | 42 | // Edges of the Perm. 43 | func (Perm) Edges() []ent.Edge { 44 | return []ent.Edge{ 45 | edge.From("user", User.Type). 46 | Ref("perms"). 47 | Field("user_id"). 48 | Unique(). 49 | Required(), 50 | edge.From("repo", Repo.Type). 51 | Ref("perms"). 52 | Field("repo_id"). 53 | Unique(). 54 | Required(), 55 | } 56 | } 57 | 58 | func (Perm) Indexes() []ent.Index { 59 | return []ent.Index{ 60 | // Find the perm for the repository. 61 | index.Fields("repo_id", "user_id"), 62 | // Delete staled perms after synchronization 63 | index.Fields("user_id", "synced_at"), 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /model/ent/schema/review.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "entgo.io/ent" 5 | "entgo.io/ent/dialect/entsql" 6 | "entgo.io/ent/schema/edge" 7 | "entgo.io/ent/schema/field" 8 | ) 9 | 10 | // Review holds the schema definition for the Review entity. 11 | type Review struct { 12 | ent.Schema 13 | } 14 | 15 | // Fields of the Review. 16 | func (Review) Fields() []ent.Field { 17 | return []ent.Field{ 18 | field.Enum("status"). 19 | Values( 20 | "pending", 21 | "rejected", 22 | "approved", 23 | ). 24 | Default("pending"), 25 | field.Text("comment"). 26 | Optional(), 27 | field.Time("created_at"). 28 | Default(nowUTC), 29 | field.Time("updated_at"). 30 | Default(nowUTC). 31 | UpdateDefault(nowUTC), 32 | // Edges 33 | field.Int64("user_id"). 34 | Optional(), 35 | field.Int("deployment_id"), 36 | } 37 | } 38 | 39 | // Edges of the Review. 40 | func (Review) Edges() []ent.Edge { 41 | return []ent.Edge{ 42 | edge.From("user", User.Type). 43 | Ref("reviews"). 44 | Field("user_id"). 45 | Unique(), 46 | edge.From("deployment", Deployment.Type). 47 | Ref("reviews"). 48 | Field("deployment_id"). 49 | Unique(). 50 | Required(), 51 | edge.To("event", Event.Type). 52 | Annotations(entsql.Annotation{ 53 | OnDelete: entsql.Cascade, 54 | }), 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /model/ent/schema/shared.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | func generateHash() string { 9 | rand.Seed(time.Now().UnixNano()) 10 | 11 | var letterRunes = []rune("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 12 | b := make([]rune, 32) 13 | for i := range b { 14 | b[i] = letterRunes[rand.Intn(len(letterRunes))] 15 | } 16 | return string(b) 17 | } 18 | 19 | func nowUTC() time.Time { 20 | return time.Now().UTC() 21 | } 22 | 23 | func zeroTime() time.Time { 24 | return time.Time{} 25 | } 26 | -------------------------------------------------------------------------------- /model/extent/branch.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | Branch struct { 5 | Name string `json:"name"` 6 | CommitSHA string `json:"commit_sha"` 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /model/extent/commit.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | import "time" 4 | 5 | type ( 6 | Commit struct { 7 | SHA string `json:"sha"` 8 | Message string `json:"message"` 9 | IsPullRequest bool `json:"is_pull_request"` 10 | HTMLURL string `json:"html_url"` 11 | Author *Author `json:"author,omitempty"` 12 | } 13 | 14 | Author struct { 15 | Login string `json:"login"` 16 | AvatarURL string `json:"avatar_url"` 17 | Date time.Time `json:"date"` 18 | } 19 | 20 | StatusState string 21 | 22 | Status struct { 23 | Context string `json:"context"` 24 | AvatarURL string `json:"avatar_url"` 25 | TargetURL string `json:"target_url"` 26 | State StatusState `json:"state"` 27 | } 28 | 29 | CommitFile struct { 30 | FileName string `json:"filename"` 31 | Additions int `json:"addtitions"` 32 | Deletions int `json:"deletions"` 33 | Changes int `json:"changes"` 34 | } 35 | ) 36 | 37 | const ( 38 | StatusStateSuccess StatusState = "success" 39 | StatusStateFailure StatusState = "failure" 40 | StatusStatePending StatusState = "pending" 41 | StatusStateCancelled StatusState = "cancelled" 42 | StatusStateSkipped StatusState = "skipped" 43 | ) 44 | -------------------------------------------------------------------------------- /model/extent/deployment.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | RemoteDeployment struct { 5 | UID int64 `json:"uid"` 6 | SHA string `json:"sha"` 7 | HTLMURL string `json:"html_url"` 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /model/extent/deploymentstatus.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | RemoteDeploymentStatus struct { 5 | ID int64 `json:"id"` 6 | Status string `json:"status"` 7 | Description string `json:"description"` 8 | LogURL string `json:"log_url"` 9 | } 10 | ) 11 | -------------------------------------------------------------------------------- /model/extent/ratelimit.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | import "time" 4 | 5 | type ( 6 | RateLimit struct { 7 | Limit int `json:"limit"` 8 | Remaining int `json:"remaining"` 9 | Reset time.Time `json:"reset"` 10 | } 11 | ) 12 | -------------------------------------------------------------------------------- /model/extent/repo.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | const ( 4 | RemoteRepoPermRead RemoteRepoPerm = "read" 5 | RemoteRepoPermWrite RemoteRepoPerm = "write" 6 | RemoteRepoPermAdmin RemoteRepoPerm = "admin" 7 | ) 8 | 9 | type ( 10 | RemoteRepoPerm string 11 | 12 | RemoteRepo struct { 13 | ID int64 `json:"id"` 14 | Namespace string `json:"namespace"` 15 | Name string `json:"name"` 16 | Description string `json:"description"` 17 | Perm RemoteRepoPerm `json:"repo_perm"` 18 | } 19 | ) 20 | -------------------------------------------------------------------------------- /model/extent/tag.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | Tag struct { 5 | Name string `json:"name,omitempty"` 6 | CommitSHA string `json:"commit_sha,omitempty"` 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /model/extent/user.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | RemoteUser struct { 5 | ID int64 6 | Login string 7 | AvatarURL string 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /model/extent/webhook.go: -------------------------------------------------------------------------------- 1 | package extent 2 | 3 | type ( 4 | WebhookConfig struct { 5 | URL string 6 | Secret string 7 | SSL bool 8 | } 9 | ) 10 | -------------------------------------------------------------------------------- /openapi/v1/paths/license/index.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - License 4 | summary: Return license 5 | responses: 6 | '200': 7 | description: Licnese 8 | content: 9 | application/json: 10 | schema: 11 | $ref: '../../schemas/License.yaml' 12 | '500': 13 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/branch.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: Get the branch of repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: branch 18 | required: true 19 | schema: 20 | type: string 21 | description: The branch name 22 | responses: 23 | '200': 24 | description: Branch 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '../../schemas/Branch.yaml' 29 | '401': 30 | $ref: '../../responses.yaml#/401Unauthorized' 31 | '402': 32 | $ref: '../../responses.yaml#/402PaymentRequired' 33 | '403': 34 | $ref: '../../responses.yaml#/403Forbidden' 35 | '404': 36 | $ref: '../../responses.yaml#/404NotFound' 37 | '500': 38 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/branches.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List branches of the repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: query 17 | name: page 18 | schema: 19 | type: integer 20 | default: 1 21 | description: The page number 22 | - in: query 23 | name: per_page 24 | schema: 25 | type: integer 26 | default: 30 27 | description: The item count per page 28 | responses: 29 | '200': 30 | description: Branches 31 | content: 32 | application/json: 33 | schema: 34 | type: array 35 | items: 36 | $ref: '../../schemas/Branch.yaml' 37 | '401': 38 | $ref: '../../responses.yaml#/401Unauthorized' 39 | '402': 40 | $ref: '../../responses.yaml#/402PaymentRequired' 41 | '403': 42 | $ref: '../../responses.yaml#/403Forbidden' 43 | '500': 44 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/commit.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: Get the commit of repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: sha 18 | required: true 19 | schema: 20 | type: string 21 | description: The commit sha 22 | responses: 23 | '200': 24 | description: Commit 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '../../schemas/Commit.yaml' 29 | '401': 30 | $ref: '../../responses.yaml#/401Unauthorized' 31 | '402': 32 | $ref: '../../responses.yaml#/402PaymentRequired' 33 | '403': 34 | $ref: '../../responses.yaml#/403Forbidden' 35 | '404': 36 | $ref: '../../responses.yaml#/404NotFound' 37 | '500': 38 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/commit_statuses.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List statuses of the commit. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: sha 18 | required: true 19 | schema: 20 | type: string 21 | description: The commit sha 22 | responses: 23 | '200': 24 | description: Statuses for ref 25 | content: 26 | application/json: 27 | schema: 28 | type: object 29 | properties: 30 | state: 31 | type: string 32 | statuses: 33 | type: array 34 | items: 35 | $ref: '../../schemas/Status.yaml' 36 | '401': 37 | $ref: '../../responses.yaml#/401Unauthorized' 38 | '402': 39 | $ref: '../../responses.yaml#/402PaymentRequired' 40 | '403': 41 | $ref: '../../responses.yaml#/403Forbidden' 42 | '404': 43 | $ref: '../../responses.yaml#/404NotFound' 44 | '500': 45 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/commits.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List commits of repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: query 17 | name: branch 18 | schema: 19 | type: string 20 | default: 21 | description: > 22 | The branch to start listing commits from. 23 | Default - the repository’s default branch. 24 | - in: query 25 | name: page 26 | schema: 27 | type: integer 28 | default: 1 29 | description: The page number 30 | - in: query 31 | name: per_page 32 | schema: 33 | type: integer 34 | default: 30 35 | description: The page number 36 | responses: 37 | '200': 38 | description: Commits 39 | content: 40 | application/json: 41 | schema: 42 | type: array 43 | items: 44 | $ref: '../../schemas/Commit.yaml' 45 | '401': 46 | $ref: '../../responses.yaml#/401Unauthorized' 47 | '402': 48 | $ref: '../../responses.yaml#/402PaymentRequired' 49 | '403': 50 | $ref: '../../responses.yaml#/403Forbidden' 51 | '500': 52 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/config.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: Get the config file 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | responses: 17 | '200': 18 | description: Config 19 | content: 20 | application/json: 21 | schema: 22 | $ref: '../../schemas/Config.yaml' 23 | '401': 24 | $ref: '../../responses.yaml#/401Unauthorized' 25 | '402': 26 | $ref: '../../responses.yaml#/402PaymentRequired' 27 | '403': 28 | $ref: '../../responses.yaml#/403Forbidden' 29 | '404': 30 | $ref: '../../responses.yaml#/404NotFound' 31 | '422': 32 | $ref: '../../responses.yaml#/422UnprocessableEntity' 33 | '500': 34 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/deployment_changes.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List commits from the environment state. 5 | description: > 6 | List commits from the environment state, internally, 7 | it compares with the previous succeed deployment. 8 | parameters: 9 | - in: path 10 | name: namespace 11 | required: true 12 | schema: 13 | type: string 14 | - in: path 15 | name: name 16 | required: true 17 | schema: 18 | type: string 19 | - in: path 20 | name: number 21 | required: true 22 | schema: 23 | type: integer 24 | description: The deployment number. 25 | - in: query 26 | name: page 27 | schema: 28 | type: integer 29 | default: 1 30 | - in: query 31 | name: per_page 32 | schema: 33 | type: integer 34 | default: 30 35 | responses: 36 | '200': 37 | description: Commits 38 | content: 39 | application/json: 40 | schema: 41 | type: array 42 | items: 43 | $ref: '../../schemas/Commit.yaml' 44 | '401': 45 | $ref: '../../responses.yaml#/401Unauthorized' 46 | '402': 47 | $ref: '../../responses.yaml#/402PaymentRequired' 48 | '403': 49 | $ref: '../../responses.yaml#/403Forbidden' 50 | '404': 51 | $ref: '../../responses.yaml#/404NotFound' 52 | '500': 53 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/deployment_remote_statuses.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | tags: 3 | - Repo 4 | summary: Create a new remote deployment status. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: number 18 | required: true 19 | schema: 20 | type: integer 21 | description: The deployment number. 22 | requestBody: 23 | content: 24 | application/json: 25 | schema: 26 | type: object 27 | properties: 28 | status: 29 | type: string 30 | description: 31 | type: string 32 | log_url: 33 | type: string 34 | required: 35 | - status 36 | responses: 37 | '200': 38 | description: Return the deployment status. 39 | content: 40 | application/json: 41 | schema: 42 | $ref: '../../schemas/RemoteDeploymentStatus.yaml' 43 | '401': 44 | $ref: '../../responses.yaml#/401Unauthorized' 45 | '402': 46 | $ref: '../../responses.yaml#/402PaymentRequired' 47 | '403': 48 | $ref: '../../responses.yaml#/403Forbidden' 49 | '422': 50 | $ref: '../../responses.yaml#/422UnprocessableEntity' 51 | '500': 52 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/deployment_rollback.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | tags: 3 | - Repo 4 | summary: Rollback by the deployment. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: number 18 | required: true 19 | schema: 20 | type: integer 21 | description: The deployment number. 22 | responses: 23 | '201': 24 | description: Rollbacked Deployment 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '../../schemas/Deployment.yaml' 29 | '401': 30 | $ref: '../../responses.yaml#/401Unauthorized' 31 | '402': 32 | $ref: '../../responses.yaml#/402PaymentRequired' 33 | '403': 34 | $ref: '../../responses.yaml#/403Forbidden' 35 | '404': 36 | $ref: '../../responses.yaml#/404NotFound' 37 | '409': 38 | $ref: '../../responses.yaml#/409Conflict' 39 | '422': 40 | description: The deployment number or the configuration is invalid. 41 | $ref: '../../responses.yaml#/422UnprocessableEntity' 42 | '500': 43 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/deployment_statuses.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List deployment statuses. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: number 18 | required: true 19 | schema: 20 | type: integer 21 | description: The deployment number. 22 | responses: 23 | '200': 24 | description: Return the deployment statuses. 25 | content: 26 | application/json: 27 | schema: 28 | type: array 29 | items: 30 | $ref: '../../schemas/DeploymentStatus.yaml' 31 | '401': 32 | $ref: '../../responses.yaml#/401Unauthorized' 33 | '402': 34 | $ref: '../../responses.yaml#/402PaymentRequired' 35 | '403': 36 | $ref: '../../responses.yaml#/403Forbidden' 37 | '422': 38 | $ref: '../../responses.yaml#/422UnprocessableEntity' 39 | '500': 40 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/index.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List repositories the user can access. 5 | parameters: 6 | - in: query 7 | name: sort 8 | schema: 9 | type: boolean 10 | default: false 11 | - in: query 12 | name: q 13 | schema: 14 | type: string 15 | default: "" 16 | - in: query 17 | name: namespace 18 | schema: 19 | type: string 20 | default: "" 21 | - in: query 22 | name: name 23 | schema: 24 | type: string 25 | default: "" 26 | - in: query 27 | name: page 28 | schema: 29 | type: integer 30 | default: 1 31 | description: The page number 32 | - in: query 33 | name: per_page 34 | schema: 35 | type: integer 36 | default: 30 37 | description: The number per page 38 | responses: 39 | '200': 40 | description: Repositories 41 | content: 42 | application/json: 43 | schema: 44 | type: array 45 | items: 46 | $ref: '../../schemas/Repository.yaml' 47 | '400': 48 | $ref: '../../responses.yaml#/400BadRequest' 49 | '401': 50 | $ref: '../../responses.yaml#/401Unauthorized' 51 | '402': 52 | $ref: '../../responses.yaml#/402PaymentRequired' 53 | '500': 54 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/perms.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: Get permissions for the repository 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: query 17 | name: q 18 | schema: 19 | type: string 20 | description: Search perms by login. 21 | - in: query 22 | name: page 23 | schema: 24 | type: number 25 | - in: query 26 | name: per_page 27 | schema: 28 | type: number 29 | responses: 30 | '200': 31 | description: Perms 32 | content: 33 | application/json: 34 | schema: 35 | type: array 36 | items: 37 | $ref: '../../schemas/Perm.yaml' 38 | '401': 39 | $ref: '../../responses.yaml#/401Unauthorized' 40 | '402': 41 | $ref: '../../responses.yaml#/402PaymentRequired' 42 | '403': 43 | $ref: '../../responses.yaml#/403Forbidden' 44 | '500': 45 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/tag.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: Get the tag of the repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: path 17 | name: tag 18 | required: true 19 | schema: 20 | type: string 21 | description: The tag name 22 | responses: 23 | '200': 24 | description: Tag 25 | content: 26 | application/json: 27 | schema: 28 | $ref: '../../schemas/Tag.yaml' 29 | '401': 30 | $ref: '../../responses.yaml#/401Unauthorized' 31 | '402': 32 | $ref: '../../responses.yaml#/402PaymentRequired' 33 | '403': 34 | $ref: '../../responses.yaml#/403Forbidden' 35 | '404': 36 | $ref: '../../responses.yaml#/404NotFound' 37 | '500': 38 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/repos/tags.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Repo 4 | summary: List tags of the repository. 5 | parameters: 6 | - in: path 7 | name: namespace 8 | required: true 9 | schema: 10 | type: string 11 | - in: path 12 | name: name 13 | required: true 14 | schema: 15 | type: string 16 | - in: query 17 | name: page 18 | schema: 19 | type: integer 20 | default: 1 21 | description: The page number 22 | - in: query 23 | name: per_page 24 | schema: 25 | type: integer 26 | default: 30 27 | description: The item count per page 28 | responses: 29 | '200': 30 | description: Tags 31 | content: 32 | application/json: 33 | schema: 34 | type: array 35 | items: 36 | $ref: '../../schemas/Tag.yaml' 37 | '401': 38 | $ref: '../../responses.yaml#/401Unauthorized' 39 | '402': 40 | $ref: '../../responses.yaml#/402PaymentRequired' 41 | '403': 42 | $ref: '../../responses.yaml#/403Forbidden' 43 | '500': 44 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/search/reviews.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Search 4 | summary: Search assigned reviews. 5 | responses: 6 | '200': 7 | description: Returns deployments which matches to conditions. 8 | content: 9 | application/json: 10 | schema: 11 | type: array 12 | items: 13 | $ref: '../../schemas/Review.yaml' 14 | '400': 15 | $ref: '../../responses.yaml#/400BadRequest' 16 | '401': 17 | $ref: '../../responses.yaml#/401Unauthorized' 18 | '402': 19 | $ref: '../../responses.yaml#/402PaymentRequired' 20 | '500': 21 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/stream/events.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - Event 4 | summary: Subscribes streaming event 5 | responses: 6 | '200': 7 | description: Returns events for deployments and reviews 8 | content: 9 | text/event-stream: 10 | schema: 11 | type: array 12 | format: chunked 13 | items: 14 | type: object 15 | format: text 16 | required: 17 | - id 18 | - event 19 | - data 20 | properties: 21 | id: 22 | type: integer 23 | event: 24 | type: string 25 | enum: 26 | - deployment_status 27 | - review 28 | data: 29 | oneOf: 30 | - $ref: '../../schemas/DeploymentStatus.yaml' 31 | - $ref: '../../schemas/Review.yaml' -------------------------------------------------------------------------------- /openapi/v1/paths/sync/index.yaml: -------------------------------------------------------------------------------- 1 | post: 2 | tags: 3 | - Sync 4 | summary: Synchronize with SCM. 5 | responses: 6 | '200': 7 | description: OK 8 | '401': 9 | $ref: '../../responses.yaml#/401Unauthorized' 10 | '402': 11 | $ref: '../../responses.yaml#/402PaymentRequired' 12 | '500': 13 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/users/index.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - User 4 | summary: Get user list. 5 | parameters: 6 | - in: query 7 | name: q 8 | schema: 9 | type: string 10 | description: Search users by login. 11 | - in: query 12 | name: page 13 | schema: 14 | type: number 15 | default: 1 16 | - in: query 17 | name: per_page 18 | schema: 19 | type: number 20 | default: 30 21 | responses: 22 | '200': 23 | description: User 24 | content: 25 | application/json: 26 | schema: 27 | $ref: '../../schemas/User.yaml' 28 | '401': 29 | $ref: '../../responses.yaml#/401Unauthorized' 30 | '402': 31 | $ref: '../../responses.yaml#/402PaymentRequired' 32 | '403': 33 | $ref: '../../responses.yaml#/403Forbidden' 34 | '500': 35 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/users/me.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - User 4 | summary: Get my information. 5 | responses: 6 | '200': 7 | description: User 8 | content: 9 | application/json: 10 | schema: 11 | $ref: '../../schemas/User.yaml' 12 | '401': 13 | $ref: '../../responses.yaml#/401Unauthorized' 14 | '402': 15 | $ref: '../../responses.yaml#/402PaymentRequired' 16 | '500': 17 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/users/ratelimit.yaml: -------------------------------------------------------------------------------- 1 | get: 2 | tags: 3 | - User 4 | summary: Get rate-limit of SCM. 5 | responses: 6 | '200': 7 | description: Rate Limit 8 | content: 9 | application/json: 10 | schema: 11 | $ref: '../../schemas/RateLimit.yaml' 12 | '401': 13 | $ref: '../../responses.yaml#/401Unauthorized' 14 | '403': 15 | $ref: '../../responses.yaml#/403Forbidden' 16 | '500': 17 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/paths/users/user.yaml: -------------------------------------------------------------------------------- 1 | patch: 2 | tags: 3 | - User 4 | summary: Update the user. 5 | parameters: 6 | - in: path 7 | name: id 8 | required: true 9 | schema: 10 | type: string 11 | description: User id. 12 | requestBody: 13 | content: 14 | application/json: 15 | schema: 16 | type: object 17 | properties: 18 | admin: 19 | type: boolean 20 | default: false 21 | responses: 22 | '200': 23 | description: User 24 | content: 25 | application/json: 26 | schema: 27 | $ref: '../../schemas/User.yaml' 28 | '401': 29 | $ref: '../../responses.yaml#/401Unauthorized' 30 | '402': 31 | $ref: '../../responses.yaml#/402PaymentRequired' 32 | '403': 33 | $ref: '../../responses.yaml#/403Forbidden' 34 | '500': 35 | $ref: '../../responses.yaml#/500InternalError' 36 | delete: 37 | tags: 38 | - User 39 | summary: Delete the user. 40 | parameters: 41 | - in: path 42 | name: id 43 | required: true 44 | schema: 45 | type: string 46 | description: User id. 47 | responses: 48 | '200': 49 | description: User 50 | content: 51 | application/json: 52 | schema: 53 | $ref: '../../schemas/User.yaml' 54 | '401': 55 | $ref: '../../responses.yaml#/401Unauthorized' 56 | '402': 57 | $ref: '../../responses.yaml#/402PaymentRequired' 58 | '403': 59 | $ref: '../../responses.yaml#/403Forbidden' 60 | '500': 61 | $ref: '../../responses.yaml#/500InternalError' -------------------------------------------------------------------------------- /openapi/v1/responses.yaml: -------------------------------------------------------------------------------- 1 | 400BadRequest: 2 | description: | 3 | The request could not be understood by the server due to malformed syntax. 4 | content: 5 | application/json: 6 | schema: 7 | $ref: './schemas/Error.yaml' 8 | 9 | 401Unauthorized: 10 | description: Unauthorized access 11 | content: 12 | application/json: 13 | schema: 14 | $ref: './schemas/Error.yaml' 15 | 16 | 402PaymentRequired: 17 | description: License is expired 18 | content: 19 | application/json: 20 | schema: 21 | $ref: './schemas/Error.yaml' 22 | 23 | 403Forbidden: 24 | description: Permisson denied 25 | content: 26 | application/json: 27 | schema: 28 | $ref: './schemas/Error.yaml' 29 | 30 | 404NotFound: 31 | description: The resource is not found 32 | content: 33 | application/json: 34 | schema: 35 | $ref: './schemas/Error.yaml' 36 | 37 | 409Conflict: 38 | description: The conflict occurs 39 | content: 40 | application/json: 41 | schema: 42 | $ref: './schemas/Error.yaml' 43 | 44 | 422UnprocessableEntity: 45 | description: | 46 | the syntax of the request entity is correct but was unable to process the contained instructions. 47 | content: 48 | application/json: 49 | schema: 50 | $ref: './schemas/Error.yaml' 51 | 52 | 500InternalError: 53 | description: Server internal error 54 | content: 55 | application/json: 56 | schema: 57 | $ref: './schemas/Error.yaml' 58 | -------------------------------------------------------------------------------- /openapi/v1/schemas/Branch.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | name: 4 | type: string 5 | commit_sha: 6 | type: string 7 | required: 8 | - name 9 | - commit_sha -------------------------------------------------------------------------------- /openapi/v1/schemas/ChatUser.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: string 5 | created_at: 6 | type: string 7 | updated_at: 8 | type: string 9 | required: 10 | - id 11 | - created_at 12 | - updated_at -------------------------------------------------------------------------------- /openapi/v1/schemas/Commit.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | sha: 4 | type: string 5 | message: 6 | type: string 7 | is_pull_request: 8 | type: boolean 9 | html_url: 10 | type: string 11 | author: 12 | type: object 13 | properties: 14 | login: 15 | type: string 16 | avatar_url: 17 | type: string 18 | date: 19 | type: string 20 | required: 21 | - sha 22 | - message 23 | - is_pull_request 24 | - html_url -------------------------------------------------------------------------------- /openapi/v1/schemas/Deployment.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: integer 5 | type: 6 | type: string 7 | enum: 8 | - commit 9 | - branch 10 | - tag 11 | ref: 12 | type: string 13 | env: 14 | type: string 15 | status: 16 | type: string 17 | enum: 18 | - waiting 19 | - created 20 | - queued 21 | - running 22 | - success 23 | - failure 24 | - canceled 25 | uid: 26 | type: integer 27 | sha: 28 | type: string 29 | html_url: 30 | type: string 31 | production_environment: 32 | type: boolean 33 | is_rollback: 34 | type: boolean 35 | created_at: 36 | type: string 37 | updated_at: 38 | type: string 39 | edges: 40 | type: object 41 | properties: 42 | user: 43 | $ref: 'User.yaml' 44 | repo: 45 | $ref: 'Repository.yaml' 46 | deployment_statuses: 47 | type: array 48 | items: 49 | $ref: 'DeploymentStatus.yaml' 50 | required: 51 | - id 52 | - type 53 | - ref 54 | - env 55 | - status 56 | - production_environment 57 | - is_rollback 58 | - created_at 59 | - updated_at -------------------------------------------------------------------------------- /openapi/v1/schemas/DeploymentStatus.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | status: 6 | type: string 7 | description: 8 | type: string 9 | log_url: 10 | type: string 11 | created_at: 12 | type: string 13 | updated_at: 14 | type: string 15 | deployment_id: 16 | type: number 17 | repo_id: 18 | type: number 19 | edges: 20 | type: object 21 | properties: 22 | deployment: 23 | $ref: 'DeploymentStatus.yaml' 24 | repo: 25 | $ref: 'Repository.yaml' 26 | required: 27 | - id 28 | - status 29 | - created_at 30 | - updated_at 31 | - deployment_id 32 | - repo_id -------------------------------------------------------------------------------- /openapi/v1/schemas/Error.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | code: 4 | type: string 5 | message: 6 | type: string 7 | required: 8 | - message -------------------------------------------------------------------------------- /openapi/v1/schemas/License.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | kind: 4 | type: string 5 | member_count: 6 | type: number 7 | member_limit: 8 | type: number 9 | expired_at: 10 | type: string 11 | required: 12 | - kind 13 | - member_count -------------------------------------------------------------------------------- /openapi/v1/schemas/Lock.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | env: 6 | type: string 7 | expired_at: 8 | type: string 9 | created_at: 10 | type: string 11 | edges: 12 | type: object 13 | properties: 14 | repo: 15 | $ref: 'Repository.yaml' 16 | user: 17 | $ref: 'User.yaml' 18 | required: 19 | - id 20 | - env 21 | - created_at -------------------------------------------------------------------------------- /openapi/v1/schemas/Perm.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | repo_perm: 6 | type: string 7 | synced_at: 8 | type: string 9 | created_at: 10 | type: string 11 | updated_at: 12 | type: string 13 | edges: 14 | type: object 15 | properties: 16 | user: 17 | $ref: './User.yaml' 18 | repo: 19 | $ref: './Repository.yaml' 20 | required: 21 | - id 22 | - repo_perm 23 | - created_at 24 | - updated_at -------------------------------------------------------------------------------- /openapi/v1/schemas/RateLimit.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | limit: 4 | type: number 5 | remaining: 6 | type: number 7 | reset: 8 | type: string 9 | required: 10 | - limit 11 | - remaining 12 | - reset -------------------------------------------------------------------------------- /openapi/v1/schemas/RemoteDeploymentStatus.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | status: 6 | type: string 7 | description: 8 | type: string 9 | log_url: 10 | type: string 11 | required: 12 | - id 13 | - status -------------------------------------------------------------------------------- /openapi/v1/schemas/Repository.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | namespace: 6 | type: string 7 | description: Repository owner 8 | name: 9 | type: string 10 | description: 11 | type: string 12 | config_path: 13 | type: string 14 | active: 15 | type: boolean 16 | webhook_id: 17 | type: integer 18 | synced_at: 19 | type: string 20 | created_at: 21 | type: string 22 | updated_at: 23 | type: string 24 | latest_deployed_at: 25 | type: string 26 | edges: 27 | type: object 28 | properties: 29 | deployments: 30 | type: array 31 | items: 32 | $ref: 'Deployment.yaml' 33 | required: 34 | - id 35 | - namespace 36 | - name 37 | - description 38 | - config_path 39 | - active 40 | - created_at 41 | - updated_at -------------------------------------------------------------------------------- /openapi/v1/schemas/Review.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: integer 5 | status: 6 | type: string 7 | enum: 8 | - pending 9 | - rejected 10 | - approved 11 | comment: 12 | type: string 13 | created_at: 14 | type: string 15 | updated_at: 16 | type: string 17 | edges: 18 | type: object 19 | properties: 20 | user: 21 | $ref: 'User.yaml' 22 | deployment: 23 | $ref: 'Deployment.yaml' 24 | required: 25 | - id 26 | - status 27 | - comment 28 | - created_at 29 | - updated_at -------------------------------------------------------------------------------- /openapi/v1/schemas/Status.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | context: 4 | type: string 5 | avatar_url: 6 | type: string 7 | target_url: 8 | type: string 9 | state: 10 | type: string 11 | description: The state is one of failure, pending, and success. 12 | enum: 13 | - pending 14 | - failure 15 | - success 16 | - cancelled 17 | - skipped 18 | required: 19 | - context 20 | - avatar_url 21 | - target_url 22 | - state -------------------------------------------------------------------------------- /openapi/v1/schemas/Tag.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | name: 4 | type: string 5 | commit_sha: 6 | type: string 7 | required: 8 | - name 9 | - commit_sha -------------------------------------------------------------------------------- /openapi/v1/schemas/User.yaml: -------------------------------------------------------------------------------- 1 | type: object 2 | properties: 3 | id: 4 | type: number 5 | login: 6 | type: string 7 | avatar: 8 | type: string 9 | admin: 10 | type: boolean 11 | created_at: 12 | type: string 13 | updated_at: 14 | type: string 15 | edges: 16 | type: object 17 | properties: 18 | chat_user: 19 | $ref: './ChatUser.yaml' 20 | required: 21 | - id 22 | - login 23 | - admin 24 | - avatar 25 | - created_at 26 | - updated_at 27 | - edges -------------------------------------------------------------------------------- /pkg/api/config.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/gitploy-io/gitploy/model/extent" 8 | ) 9 | 10 | type ( 11 | ConfigService service 12 | ) 13 | 14 | func (s *ConfigService) Get(ctx context.Context, namespace, name string) (*extent.Config, error) { 15 | req, err := s.client.NewRequest( 16 | "GET", 17 | fmt.Sprintf("api/v1/repos/%s/%s/config", namespace, name), 18 | nil, 19 | ) 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | var config *extent.Config 25 | if err := s.client.Do(ctx, req, &config); err != nil { 26 | return nil, err 27 | } 28 | 29 | return config, nil 30 | } 31 | -------------------------------------------------------------------------------- /pkg/api/config_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "reflect" 7 | "testing" 8 | 9 | "github.com/gitploy-io/gitploy/model/extent" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestConfig_Get(t *testing.T) { 14 | t.Run("Return the config.", func(t *testing.T) { 15 | config := &extent.Config{ 16 | Envs: []*extent.Env{ 17 | {Name: "production"}, 18 | }, 19 | } 20 | 21 | gock.New("https://cloud.gitploy.io"). 22 | Get("/api/v1/repos/gitploy-io/gitploy/config"). 23 | Reply(200). 24 | JSON(config) 25 | 26 | c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) 27 | 28 | ret, err := c.Config.Get(context.Background(), "gitploy-io", "gitploy") 29 | if err != nil { 30 | t.Fatalf("Get returns an error: %s", err) 31 | } 32 | 33 | if !reflect.DeepEqual(ret, config) { 34 | t.Fatalf("Get = %v, wanted %v", ret, config) 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /pkg/api/deployment_status_test.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "testing" 7 | 8 | "github.com/gitploy-io/gitploy/model/ent" 9 | "github.com/gitploy-io/gitploy/model/extent" 10 | "gopkg.in/h2non/gock.v1" 11 | ) 12 | 13 | func TestDeploymentStatus_List(t *testing.T) { 14 | t.Run("Return the list of statuses.", func(t *testing.T) { 15 | dss := []*ent.DeploymentStatus{ 16 | {ID: 1}, 17 | } 18 | gock.New("https://cloud.gitploy.io"). 19 | Get("/api/v1/repos/gitploy-io/gitploy/deployments/1/statuses"). 20 | Reply(200). 21 | JSON(dss) 22 | 23 | c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) 24 | 25 | _, err := c.DeploymentStatus.List(context.Background(), "gitploy-io", "gitploy", 1, &ListOptions{}) 26 | if err != nil { 27 | t.Fatalf("Create returns an error: %s", err) 28 | } 29 | }) 30 | } 31 | 32 | func TestDeploymentStatus_Create(t *testing.T) { 33 | t.Run("Return the deployment statuses.", func(t *testing.T) { 34 | gock.New("https://cloud.gitploy.io"). 35 | Post("/api/v1/repos/gitploy-io/gitploy/deployments/1/remote-statuses"). 36 | Reply(201). 37 | JSON(extent.RemoteDeploymentStatus{ID: 1}) 38 | 39 | c := NewClient("https://cloud.gitploy.io/", http.DefaultClient) 40 | 41 | _, err := c.DeploymentStatus.CreateRemote(context.Background(), "gitploy-io", "gitploy", 1, &DeploymentStatusCreateRemoteRequest{}) 42 | if err != nil { 43 | t.Fatalf("Create returns an error: %s", err) 44 | } 45 | }) 46 | } 47 | -------------------------------------------------------------------------------- /pkg/api/shared.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | type ( 4 | ListOptions struct { 5 | Page int 6 | PerPage int 7 | } 8 | ) 9 | -------------------------------------------------------------------------------- /pkg/api/user.go: -------------------------------------------------------------------------------- 1 | package api 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/gitploy-io/gitploy/model/ent" 7 | ) 8 | 9 | type ( 10 | // UserService communicates with the server for users. 11 | UserService service 12 | ) 13 | 14 | // GetMe returns the user information. 15 | func (s *UserService) GetMe(ctx context.Context) (*ent.User, error) { 16 | req, err := s.client.NewRequest( 17 | "GET", 18 | "/api/v1/user", 19 | nil, 20 | ) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | var u *ent.User 26 | if err := s.client.Do(ctx, req, &u); err != nil { 27 | return nil, err 28 | } 29 | 30 | return u, nil 31 | } 32 | -------------------------------------------------------------------------------- /pkg/e/code_test.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "testing" 7 | ) 8 | 9 | func TestError_GetHTTPError(t *testing.T) { 10 | t.Run("Return the matche HTTP code.", func(t *testing.T) { 11 | err := NewError(ErrorCodeInternalError, nil) 12 | if err.GetHTTPCode() != http.StatusInternalServerError { 13 | t.Fatalf("GetHTTPCode = %v, wanted %v", err.GetHTTPCode(), http.StatusInternalServerError) 14 | } 15 | }) 16 | } 17 | 18 | func Test_IsError(t *testing.T) { 19 | t.Run("Return true when the type of error is Error.", func(t *testing.T) { 20 | err := NewError(ErrorCodeInternalError, nil) 21 | if ok := IsError(err); !ok { 22 | t.Fatalf("IsError = %v, wanted %v", ok, true) 23 | } 24 | }) 25 | 26 | t.Run("Return false when the type of error is not Error.", func(t *testing.T) { 27 | err := fmt.Errorf("fmt.Error") 28 | if ok := IsError(err); ok { 29 | t.Fatalf("IsError = %v, wanted %v", ok, false) 30 | } 31 | }) 32 | } 33 | 34 | func Test_HasErrorCode(t *testing.T) { 35 | t.Run("Return true when the type of error is Error and the code is internal_error.", func(t *testing.T) { 36 | err := NewError(ErrorCodeInternalError, nil) 37 | if ok := HasErrorCode(err, ErrorCodeInternalError); !ok { 38 | t.Fatalf("IsError = %v, wanted %v", ok, true) 39 | } 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/e/trans_test.go: -------------------------------------------------------------------------------- 1 | package e 2 | 3 | import "testing" 4 | 5 | func Test_GetMessage(t *testing.T) { 6 | t.Run("Return code when the message is emtpy.", func(t *testing.T) { 7 | const ErrorCodeEmpty ErrorCode = "emtpy" 8 | 9 | message := GetMessage(ErrorCodeEmpty) 10 | if message != string(ErrorCodeEmpty) { 11 | t.Fatalf("GetMessage = %s, wanted %s", message, string(ErrorCodeEmpty)) 12 | } 13 | }) 14 | } 15 | -------------------------------------------------------------------------------- /release/values.dev.yaml: -------------------------------------------------------------------------------- 1 | ingress: 2 | enabled: true 3 | annotations: 4 | kubernetes.io/ingress.class: nginx 5 | cert-manager.io/cluster-issuer: "letsencrypt-prod" 6 | hosts: 7 | - host: dev.gitploy.io 8 | paths: 9 | - path: / 10 | pathType: Prefix 11 | tls: 12 | - secretName: gitploy-dev-tls 13 | hosts: 14 | - dev.gitploy.io 15 | 16 | resources: 17 | requests: 18 | cpu: 50m 19 | memory: 128Mi 20 | 21 | env: 22 | GITPLOY_DEBUG_MODE: "true" 23 | GITPLOY_SERVER_HOST: "dev.gitploy.io" 24 | GITPLOY_SERVER_PROTO: https 25 | GITPLOY_ADMIN_USERS: "hanjunlee" 26 | GITPLOY_MEMBER_ENTRIES: "gitploy-io" 27 | GITPLOY_PROMETHEUS_ENABLED: "true" 28 | 29 | extraSecretNamesForEnvFrom: 30 | - gitploy-dev-secret 31 | 32 | persistentVolume: 33 | enabled: true -------------------------------------------------------------------------------- /release/values.production.yaml: -------------------------------------------------------------------------------- 1 | ingress: 2 | enabled: true 3 | annotations: 4 | kubernetes.io/ingress.class: nginx 5 | cert-manager.io/cluster-issuer: "letsencrypt-prod" 6 | hosts: 7 | - host: cloud.gitploy.io 8 | paths: 9 | - path: / 10 | pathType: Prefix 11 | tls: 12 | - secretName: gitploy-prod-tls 13 | hosts: 14 | - cloud.gitploy.io 15 | 16 | resources: 17 | requests: 18 | cpu: 100m 19 | memory: 256Mi 20 | 21 | env: 22 | GITPLOY_DEBUG_MODE: "true" 23 | GITPLOY_SERVER_HOST: "cloud.gitploy.io" 24 | GITPLOY_SERVER_PROTO: https 25 | GITPLOY_ADMIN_USERS: "hanjunlee" 26 | GITPLOY_GITHUB_SCOPES: "public_repo,read:user,read:org" 27 | 28 | extraSecretNamesForEnvFrom: 29 | - gitploy-prod-secret 30 | 31 | persistentVolume: 32 | enabled: true -------------------------------------------------------------------------------- /tools/tools.go: -------------------------------------------------------------------------------- 1 | // Track tool dependencies for a module. 2 | // https://github.com/golang/go/wiki/Modules#how-can-i-track-tool-dependencies-for-a-module 3 | 4 | package tools 5 | 6 | import ( 7 | _ "entgo.io/ent/entc" 8 | ) 9 | -------------------------------------------------------------------------------- /ui/.eslintignore: -------------------------------------------------------------------------------- 1 | # don't ever lint node_modules 2 | node_modules 3 | # don't lint build output (make sure it's set to your correct build folder name) 4 | dist 5 | # don't lint nyc coverage output 6 | coverage 7 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: '@typescript-eslint/parser', 4 | plugins: ['@typescript-eslint'], 5 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'], 6 | rules: { 7 | '@typescript-eslint/no-explicit-any': 'off', 8 | '@typescript-eslint/no-empty-interface': 'off', 9 | '@typescript-eslint/explicit-module-boundary-types': 'off', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /ui/.nvmrc: -------------------------------------------------------------------------------- 1 | 14.17.0 -------------------------------------------------------------------------------- /ui/.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | coverage -------------------------------------------------------------------------------- /ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "semi": true, 4 | "singleQuote": true 5 | } 6 | -------------------------------------------------------------------------------- /ui/craco.config.js: -------------------------------------------------------------------------------- 1 | const CracoLessPlugin = require('craco-less'); 2 | 3 | module.exports = { 4 | plugins: [ 5 | { 6 | plugin: CracoLessPlugin, 7 | options: { 8 | lessLoaderOptions: { 9 | lessOptions: { 10 | modifyVars: { 11 | // Custom theme 12 | // https://github.com/ant-design/ant-design/blob/master/components/style/themes/default.less 13 | // Colors 14 | '@primary-color': '@purple-6', 15 | '@info-color': '@purple-6', 16 | '@processing-color': '@purple-6', 17 | // Layout 18 | '@layout-header-background': '@purple-10', 19 | '@layout-body-background': '#fff', 20 | // 21 | '@border-radius-base': '5px', 22 | }, 23 | javascriptEnabled: true, 24 | }, 25 | }, 26 | }, 27 | }, 28 | ], 29 | }; 30 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitploy-io/gitploy/37643d4a4ef018a6b19075202a37087cef0c3f8e/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitploy-io/gitploy/37643d4a4ef018a6b19075202a37087cef0c3f8e/ui/public/logo192.png -------------------------------------------------------------------------------- /ui/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /ui/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /ui/public/spinner.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gitploy-io/gitploy/37643d4a4ef018a6b19075202a37087cef0c3f8e/ui/public/spinner.ico -------------------------------------------------------------------------------- /ui/src/App.less: -------------------------------------------------------------------------------- 1 | @import '~antd/dist/antd.less'; 2 | 3 | @gitploy-prefix: ~'gitploy'; 4 | 5 | .@{gitploy-prefix} { 6 | // Link changes color when it is hover. 7 | &-link { 8 | margin-bottom: 4px; 9 | color: @text-color; 10 | font-size: @font-size-base; 11 | line-height: @line-height-base; 12 | > a { 13 | color: @text-color; 14 | transition: all 0.3s; 15 | &:hover { 16 | color: @primary-color; 17 | } 18 | } 19 | } 20 | 21 | // Override border of code. 22 | &-code { 23 | > code { 24 | border: 0; 25 | } 26 | } 27 | 28 | &-quote { 29 | padding: 0 0 0 0.6em; 30 | border-left: 4px solid rgba(100, 100, 100, 0.2); 31 | opacity: 0.85; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /ui/src/App.tsx: -------------------------------------------------------------------------------- 1 | import './App.less'; 2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom'; 3 | 4 | import Home from './views/home'; 5 | import Repo from './views/repo'; 6 | import Deployment from './views/deployment'; 7 | import Settings from './views/settings'; 8 | import Members from './views/members'; 9 | import Activities from './views/activities'; 10 | 11 | function App(): JSX.Element { 12 | return ( 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
40 | ); 41 | } 42 | 43 | export default App; 44 | -------------------------------------------------------------------------------- /ui/src/apis/_base.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { 3 | HttpInternalServerError, 4 | HttpUnauthorizedError, 5 | HttpPaymentRequiredError, 6 | } from '../models/errors'; 7 | 8 | export const _fetch = async ( 9 | input: RequestInfo, 10 | init?: RequestInit 11 | ): Promise => { 12 | const response = await fetch(input, init); 13 | 14 | // Throw exception when the general status code is received. 15 | if (response.status === StatusCodes.INTERNAL_SERVER_ERROR) { 16 | throw new HttpInternalServerError('The internal server error occurs.'); 17 | } else if (response.status === StatusCodes.UNAUTHORIZED) { 18 | throw new HttpUnauthorizedError('The session is expired.'); 19 | } else if (response.status === StatusCodes.PAYMENT_REQUIRED) { 20 | throw new HttpPaymentRequiredError('The license is expired.'); 21 | } 22 | 23 | return response; 24 | }; 25 | -------------------------------------------------------------------------------- /ui/src/apis/chat.ts: -------------------------------------------------------------------------------- 1 | import { _fetch } from './_base'; 2 | import { instance, headers } from './setting'; 3 | import { StatusCodes } from 'http-status-codes'; 4 | 5 | export const checkSlack = async (): Promise => { 6 | const res = await _fetch(`${instance}/slack`, { 7 | headers, 8 | credentials: 'same-origin', 9 | method: 'HEAD', 10 | }); 11 | if (res.status === StatusCodes.NOT_FOUND) { 12 | return false; 13 | } 14 | 15 | return true; 16 | }; 17 | -------------------------------------------------------------------------------- /ui/src/apis/config.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | import { StatusCodes } from 'http-status-codes'; 3 | 4 | import { instance, headers } from './setting'; 5 | import { _fetch } from './_base'; 6 | import { Config, HttpNotFoundError } from '../models'; 7 | 8 | const mapDataToConfig = (data: any): Config => { 9 | return camelcaseKeys(data, { deep: true }); 10 | }; 11 | 12 | export const getConfig = async ( 13 | namespace: string, 14 | name: string 15 | ): Promise => { 16 | const response = await _fetch( 17 | `${instance}/api/v1/repos/${namespace}/${name}/config`, 18 | { 19 | headers, 20 | credentials: 'same-origin', 21 | } 22 | ); 23 | if (response.status === StatusCodes.NOT_FOUND) { 24 | const message = await response.json().then((data) => data.message); 25 | throw new HttpNotFoundError(message); 26 | } 27 | 28 | const conf = await response.json().then((c) => mapDataToConfig(c)); 29 | 30 | return conf; 31 | }; 32 | -------------------------------------------------------------------------------- /ui/src/apis/events.ts: -------------------------------------------------------------------------------- 1 | import { instance } from './setting'; 2 | 3 | import { mapDataToDeploymentStatus } from './deployment'; 4 | import { mapDataToReview } from './review'; 5 | import { DeploymentStatus, Review } from '../models'; 6 | 7 | export const subscribeDeploymentStatusEvents = ( 8 | cb: (status: DeploymentStatus) => void 9 | ): EventSource => { 10 | const sse = new EventSource(`${instance}/api/v1/stream/events`, { 11 | withCredentials: true, 12 | }); 13 | 14 | sse.addEventListener('deployment_status', (e: any) => { 15 | const data = JSON.parse(e.data); 16 | const status = mapDataToDeploymentStatus(data); 17 | 18 | cb(status); 19 | }); 20 | 21 | return sse; 22 | }; 23 | 24 | export const subscribeReviewEvents = ( 25 | cb: (review: Review) => void 26 | ): EventSource => { 27 | const sse = new EventSource(`${instance}/api/v1/stream/events`, { 28 | withCredentials: true, 29 | }); 30 | 31 | sse.addEventListener('review', (e: any) => { 32 | const data = JSON.parse(e.data); 33 | const review = mapDataToReview(data); 34 | 35 | cb(review); 36 | }); 37 | 38 | return sse; 39 | }; 40 | -------------------------------------------------------------------------------- /ui/src/apis/index.ts: -------------------------------------------------------------------------------- 1 | export { sync } from './sync'; 2 | export { 3 | listRepos, 4 | getRepo, 5 | updateRepo, 6 | activateRepo, 7 | deactivateRepo, 8 | lockRepo, 9 | unlockRepo, 10 | } from './repo'; 11 | export { listPerms } from './perm'; 12 | export { 13 | searchDeployments, 14 | listDeployments, 15 | getDeployment, 16 | createDeployment, 17 | createRemoteDeployment, 18 | rollbackDeployment, 19 | listDeploymentChanges, 20 | } from './deployment'; 21 | export { getConfig } from './config'; 22 | export { listCommits, getCommit, listStatuses } from './commit'; 23 | export { listBranches, getBranch, getDefaultBranch } from './branch'; 24 | export { listTags, getTag } from './tag'; 25 | export { listUsers, updateUser, deleteUser, getMe, getRateLimit } from './user'; 26 | export { checkSlack } from './chat'; 27 | export { 28 | searchReviews, 29 | listReviews, 30 | getUserReview, 31 | approveReview, 32 | rejectReview, 33 | } from './review'; 34 | export { listLocks, lock, unlock, updateLock } from './lock'; 35 | export { getLicense } from './license'; 36 | export { 37 | subscribeDeploymentStatusEvents, 38 | subscribeReviewEvents, 39 | } from './events'; 40 | -------------------------------------------------------------------------------- /ui/src/apis/license.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | import { instance, headers } from './setting'; 3 | import { _fetch } from './_base'; 4 | 5 | import { License } from '../models'; 6 | 7 | function mapDataToLicense(data: any): License { 8 | const license: License = camelcaseKeys(data); 9 | 10 | license.expiredAt = new Date(data.expired_at); 11 | 12 | return license; 13 | } 14 | 15 | export const getLicense = async (): Promise => { 16 | const lic = await _fetch(`${instance}/api/v1/license`, { 17 | headers, 18 | credentials: 'same-origin', 19 | }) 20 | .then((res) => res.json()) 21 | .then((data) => mapDataToLicense(data)); 22 | 23 | return lic; 24 | }; 25 | -------------------------------------------------------------------------------- /ui/src/apis/perm.ts: -------------------------------------------------------------------------------- 1 | import camelcaseKeys from 'camelcase-keys'; 2 | import { instance, headers } from './setting'; 3 | import { _fetch } from './_base'; 4 | import { mapDataToUser } from './user'; 5 | import { mapDataToRepo } from './repo'; 6 | import { Perm } from '../models'; 7 | 8 | const mapDataToPerm = (data: any): Perm => { 9 | const perm: Perm = camelcaseKeys(data); 10 | 11 | perm.syncedAt = new Date(data.synced_at); 12 | perm.createdAt = new Date(data.created_at); 13 | perm.updatedAt = new Date(data.updated_at); 14 | 15 | // Edges 16 | perm.user = mapDataToUser(data.edges.user); 17 | perm.repo = mapDataToRepo(data.edges.repo); 18 | 19 | return perm; 20 | }; 21 | 22 | export const listPerms = async ( 23 | namespace: string, 24 | name: string, 25 | q: string, 26 | page = 1, 27 | perPage = 30 28 | ): Promise => { 29 | const perms: Perm[] = await _fetch( 30 | `${instance}/api/v1/repos/${namespace}/${name}/perms?q=${q}&page=${page}&per_page=${perPage}`, 31 | { 32 | headers, 33 | credentials: 'same-origin', 34 | } 35 | ) 36 | .then((res) => res.json()) 37 | .then((data) => data.map((d: any) => mapDataToPerm(d))); 38 | 39 | return perms; 40 | }; 41 | -------------------------------------------------------------------------------- /ui/src/apis/setting.ts: -------------------------------------------------------------------------------- 1 | // default authentication credentials. In product cookie-based 2 | // authentication is being used. 3 | export const headers = new Headers( 4 | process.env.REACT_APP_GITPLOY_TOKEN 5 | ? { 6 | Authorization: `Bearer ${process.env.REACT_APP_GITPLOY_TOKEN}`, 7 | 'Content-Type': 'application/json', 8 | } 9 | : { 10 | 'Content-Type': 'application/json', 11 | } 12 | ); 13 | 14 | // default server api token. 15 | export const token: string | undefined = process.env.REACT_APP_GITPLOY_TOKEN; 16 | 17 | // default server address. 18 | export const instance: string = process.env.REACT_APP_GITPLOY_SERVER || ''; 19 | -------------------------------------------------------------------------------- /ui/src/apis/sync.ts: -------------------------------------------------------------------------------- 1 | import { instance, headers } from './setting'; 2 | import { _fetch } from './_base'; 3 | 4 | export const sync = async (): Promise => { 5 | await _fetch(`${instance}/api/v1/sync`, { 6 | headers, 7 | credentials: 'same-origin', 8 | method: 'POST', 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /ui/src/apis/tag.ts: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | 3 | import { instance, headers } from './setting'; 4 | import { _fetch } from './_base'; 5 | import { Tag, HttpNotFoundError } from '../models'; 6 | 7 | export const listTags = async ( 8 | namespace: string, 9 | name: string, 10 | page = 1, 11 | perPage = 30 12 | ): Promise => { 13 | const tags: Tag[] = await _fetch( 14 | `${instance}/api/v1/repos/${namespace}/${name}/tags?page=${page}&per_page=${perPage}`, 15 | { 16 | headers, 17 | credentials: 'same-origin', 18 | } 19 | ) 20 | .then((response) => response.json()) 21 | .then((tags) => 22 | tags.map((t: any): Tag => { 23 | return { 24 | name: t.name, 25 | commitSha: t.commit_sha, 26 | }; 27 | }) 28 | ); 29 | 30 | return tags; 31 | }; 32 | 33 | export const getTag = async ( 34 | namespace: string, 35 | name: string, 36 | tag: string 37 | ): Promise => { 38 | const response = await _fetch( 39 | `${instance}/api/v1/repos/${namespace}/${name}/tags/${tag}`, 40 | { 41 | headers, 42 | credentials: 'same-origin', 43 | } 44 | ); 45 | if (response.status === StatusCodes.NOT_FOUND) { 46 | const message = await response.json().then((data) => data.message); 47 | throw new HttpNotFoundError(message); 48 | } 49 | 50 | const ret: Tag = await response.json().then((t) => ({ 51 | name: t.name, 52 | commitSha: t.commit_sha, 53 | })); 54 | return ret; 55 | }; 56 | -------------------------------------------------------------------------------- /ui/src/components/ActivateButton.tsx: -------------------------------------------------------------------------------- 1 | import { Result, Button } from 'antd'; 2 | 3 | export interface ActivateButtonProps { 4 | onClickActivate(): void; 5 | } 6 | 7 | export default function ActivateButton( 8 | props: ActivateButtonProps 9 | ): JSX.Element { 10 | return ( 11 | 20 | ACTIVATE 21 | , 22 | ]} 23 | /> 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/DeploymentRefCode.tsx: -------------------------------------------------------------------------------- 1 | import { Typography } from 'antd'; 2 | 3 | import { Deployment, DeploymentType } from '../models'; 4 | 5 | const { Text } = Typography; 6 | 7 | interface DeploymentRefCodeProps { 8 | deployment: Deployment; 9 | } 10 | 11 | export default function DeploymentRefCode( 12 | props: DeploymentRefCodeProps 13 | ): JSX.Element { 14 | let ref: string; 15 | if (props.deployment.type === DeploymentType.Commit) { 16 | ref = props.deployment.ref.substring(0, 7); 17 | } else if ( 18 | props.deployment.type === DeploymentType.Branch && 19 | props.deployment.sha !== '' 20 | ) { 21 | ref = `${props.deployment.ref}(${props.deployment.sha.substring(0, 7)})`; 22 | } else { 23 | ref = props.deployment.ref; 24 | } 25 | 26 | return ( 27 | 28 | {ref} 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /ui/src/components/DeploymentStatusBadge.tsx: -------------------------------------------------------------------------------- 1 | import { Badge } from 'antd'; 2 | 3 | import { Deployment, DeploymentStatusEnum } from '../models'; 4 | 5 | interface DeploymentStatusBadgeProps { 6 | deployment: Deployment; 7 | } 8 | 9 | export default function DeploymentStatusBadge( 10 | props: DeploymentStatusBadgeProps 11 | ): JSX.Element { 12 | const deployment = props.deployment; 13 | return ( 14 | 15 | ); 16 | } 17 | 18 | // https://ant.design/components/timeline/#Timeline.Item 19 | const getStatusColor = (status: DeploymentStatusEnum) => { 20 | switch (status) { 21 | case DeploymentStatusEnum.Waiting: 22 | return 'gray'; 23 | case DeploymentStatusEnum.Created: 24 | return 'purple'; 25 | case DeploymentStatusEnum.Queued: 26 | return 'purple'; 27 | case DeploymentStatusEnum.Running: 28 | return 'purple'; 29 | case DeploymentStatusEnum.Success: 30 | return 'green'; 31 | case DeploymentStatusEnum.Failure: 32 | return 'red'; 33 | case DeploymentStatusEnum.Canceled: 34 | return 'gray'; 35 | default: 36 | return 'gray'; 37 | } 38 | }; 39 | -------------------------------------------------------------------------------- /ui/src/components/Pagination.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from 'antd'; 2 | 3 | export interface PaginationProps { 4 | disabledPrev: boolean; 5 | disabledNext: boolean; 6 | onClickPrev(): void; 7 | onClickNext(): void; 8 | } 9 | 10 | export default function Pagination(props: PaginationProps): JSX.Element { 11 | return ( 12 |
13 | 20 | 27 |
28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/components/Spin.tsx: -------------------------------------------------------------------------------- 1 | import { Spin as S } from 'antd'; 2 | import { LoadingOutlined } from '@ant-design/icons'; 3 | 4 | const antIcon = ; 5 | 6 | export default function Spin(): JSX.Element { 7 | return ; 8 | } 9 | -------------------------------------------------------------------------------- /ui/src/components/UserAvatar.tsx: -------------------------------------------------------------------------------- 1 | import { Avatar, Typography } from 'antd'; 2 | 3 | import { User } from '../models'; 4 | 5 | const { Text } = Typography; 6 | 7 | interface UserAvatarProps { 8 | boldName?: boolean; 9 | user?: User; 10 | } 11 | 12 | export default function UserAvatar(props: UserAvatarProps): JSX.Element { 13 | const boldName = props.boldName === undefined ? true : props.boldName; 14 | return props.user ? ( 15 | 16 | 17 |   18 | {props.user.login} 19 | 20 | ) : ( 21 | 22 | U 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /ui/src/components/partials/deploymentStatus.tsx: -------------------------------------------------------------------------------- 1 | import { DeploymentStatusEnum } from '../../models'; 2 | 3 | // https://ant.design/components/timeline/#Timeline.Item 4 | export const getStatusColor = (status: DeploymentStatusEnum): string => { 5 | switch (status) { 6 | case DeploymentStatusEnum.Waiting: 7 | return 'gray'; 8 | case DeploymentStatusEnum.Created: 9 | return 'purple'; 10 | case DeploymentStatusEnum.Queued: 11 | return 'purple'; 12 | case DeploymentStatusEnum.Running: 13 | return 'purple'; 14 | case DeploymentStatusEnum.Success: 15 | return 'green'; 16 | case DeploymentStatusEnum.Failure: 17 | return 'red'; 18 | case DeploymentStatusEnum.Canceled: 19 | return 'gray'; 20 | default: 21 | return 'gray'; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /ui/src/components/partials/index.tsx: -------------------------------------------------------------------------------- 1 | import { getStatusColor } from './deploymentStatus'; 2 | 3 | export { getStatusColor }; 4 | -------------------------------------------------------------------------------- /ui/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /ui/src/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | import { store } from './redux/store'; 7 | import { Provider } from 'react-redux'; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById('root') 14 | ); 15 | 16 | // If you want to start measuring performance in your app, pass a function 17 | // to log results (for example: reportWebVitals(console.log)) 18 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 19 | reportWebVitals(); 20 | -------------------------------------------------------------------------------- /ui/src/libs/index.ts: -------------------------------------------------------------------------------- 1 | import { Deployment, DeploymentType } from '../models'; 2 | 3 | /** 4 | * The function returns the short-formatted ref string. 5 | * @param deployment 6 | * @returns 7 | */ 8 | export const getShortRef = (deployment: Deployment): string => { 9 | return deployment.type === DeploymentType.Commit 10 | ? deployment.ref.substring(0, 7) 11 | : deployment.ref; 12 | }; 13 | -------------------------------------------------------------------------------- /ui/src/models/Branch.ts: -------------------------------------------------------------------------------- 1 | export default interface Branch { 2 | name: string; 3 | commitSha: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/models/Commit.ts: -------------------------------------------------------------------------------- 1 | export default interface Commit { 2 | sha: string; 3 | message: string; 4 | isPullRequest: boolean; 5 | htmlUrl: string; 6 | author?: Author; 7 | } 8 | 9 | export interface Author { 10 | login: string; 11 | avatarUrl: string; 12 | date: Date; 13 | } 14 | 15 | export interface Status { 16 | context: string; 17 | avatarUrl: string; 18 | targetUrl: string; 19 | state: StatusState; 20 | } 21 | 22 | export enum StatusState { 23 | Null = 'null', 24 | Pending = 'pending', 25 | Success = 'success', 26 | Failure = 'failure', 27 | Cancelled = 'cancelled', 28 | Skipped = 'skipped', 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/models/Config.ts: -------------------------------------------------------------------------------- 1 | export default interface Config { 2 | envs: Env[]; 3 | } 4 | 5 | export interface Env { 6 | name: string; 7 | requiredContexts?: string[]; 8 | dynamicPayload?: DynamicPayload; 9 | review?: { 10 | enabled: boolean; 11 | reviewers: string[]; 12 | }; 13 | } 14 | 15 | export interface DynamicPayload { 16 | enabled: boolean; 17 | inputs: { 18 | [key: string]: DynamicPayloadInput; 19 | }; 20 | } 21 | 22 | export interface DynamicPayloadInput { 23 | type: DynamicPayloadInputTypeEnum; 24 | required?: boolean; 25 | default?: any; 26 | description?: string; 27 | options?: string[]; 28 | } 29 | 30 | export enum DynamicPayloadInputTypeEnum { 31 | Select = 'select', 32 | String = 'string', 33 | Number = 'number', 34 | Boolean = 'boolean', 35 | } 36 | -------------------------------------------------------------------------------- /ui/src/models/Deployment.ts: -------------------------------------------------------------------------------- 1 | import User from './User'; 2 | import Repo from './Repo'; 3 | 4 | export default interface Deployment { 5 | id: number; 6 | number: number; 7 | type: DeploymentType; 8 | ref: string; 9 | sha: string; 10 | env: string; 11 | status: DeploymentStatusEnum; 12 | uid: number; 13 | isRollback: boolean; 14 | createdAt: Date; 15 | updatedAt: Date; 16 | deployer?: User; 17 | repo?: Repo; 18 | statuses?: DeploymentStatus[]; 19 | } 20 | 21 | export enum DeploymentType { 22 | Commit = 'commit', 23 | Branch = 'branch', 24 | Tag = 'tag', 25 | } 26 | 27 | export enum DeploymentStatusEnum { 28 | Waiting = 'waiting', 29 | Created = 'created', 30 | Queued = 'queued', 31 | Running = 'running', 32 | Success = 'success', 33 | Failure = 'failure', 34 | Canceled = 'canceled', 35 | } 36 | 37 | export interface DeploymentStatus { 38 | id: number; 39 | status: string; 40 | description: string; 41 | logUrl: string; 42 | createdAt: string; 43 | updatedAt: string; 44 | deploymentId: number; 45 | repoId: number; 46 | edges?: { 47 | deployment?: Deployment; 48 | repo?: Repo; 49 | }; 50 | } 51 | -------------------------------------------------------------------------------- /ui/src/models/Event.ts: -------------------------------------------------------------------------------- 1 | import Deployment from './Deployment'; 2 | import { Review } from './Review'; 3 | 4 | export default interface Event { 5 | id: number; 6 | kind: EventKindEnum; 7 | type: EventTypeEnum; 8 | deployment?: Deployment; 9 | review?: Review; 10 | deletedId: number; 11 | } 12 | 13 | export enum EventKindEnum { 14 | Deployment = 'deployment', 15 | Review = 'review', 16 | } 17 | 18 | export enum EventTypeEnum { 19 | Created = 'created', 20 | Updated = 'updated', 21 | Deleted = 'deleted', 22 | } 23 | -------------------------------------------------------------------------------- /ui/src/models/License.ts: -------------------------------------------------------------------------------- 1 | export default interface License { 2 | kind: string; 3 | memberCount: number; 4 | memberLimit: number; 5 | expiredAt: Date; 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/models/Lock.ts: -------------------------------------------------------------------------------- 1 | import User from './User'; 2 | 3 | export default interface Lock { 4 | id: number; 5 | env: string; 6 | expiredAt?: Date; 7 | createdAt: Date; 8 | user?: User; 9 | } 10 | -------------------------------------------------------------------------------- /ui/src/models/Perm.ts: -------------------------------------------------------------------------------- 1 | import User from './User'; 2 | import Repo from './Repo'; 3 | 4 | export default interface Perm { 5 | repoPerm: string; 6 | syncedAt: Date; 7 | createdAt: Date; 8 | updatedAt: Date; 9 | user: User; 10 | repo: Repo; 11 | } 12 | -------------------------------------------------------------------------------- /ui/src/models/Repo.ts: -------------------------------------------------------------------------------- 1 | import Deployment from './Deployment'; 2 | 3 | export default interface Repo { 4 | id: number; 5 | namespace: string; 6 | name: string; 7 | description: string; 8 | configPath: string; 9 | active: boolean; 10 | webhookId: number; 11 | createdAt: Date; 12 | updatedAt: Date; 13 | deployments?: Deployment[]; 14 | } 15 | -------------------------------------------------------------------------------- /ui/src/models/Request.ts: -------------------------------------------------------------------------------- 1 | export enum RequestStatus { 2 | Idle = 'idle', 3 | Pending = 'pending', 4 | Success = 'success', 5 | Failure = 'failure', 6 | } 7 | -------------------------------------------------------------------------------- /ui/src/models/Review.ts: -------------------------------------------------------------------------------- 1 | import User from './User'; 2 | import Deployment from './Deployment'; 3 | 4 | export interface Review { 5 | id: number; 6 | status: ReviewStatusEnum; 7 | comment: string; 8 | createdAt: Date; 9 | updatedAt: Date; 10 | user?: User; 11 | deployment?: Deployment; 12 | } 13 | 14 | export enum ReviewStatusEnum { 15 | Pending = 'pending', 16 | Approved = 'approved', 17 | Rejected = 'rejected', 18 | } 19 | -------------------------------------------------------------------------------- /ui/src/models/Tag.ts: -------------------------------------------------------------------------------- 1 | export default interface Tag { 2 | name: string; 3 | commitSha: string; 4 | } 5 | -------------------------------------------------------------------------------- /ui/src/models/User.ts: -------------------------------------------------------------------------------- 1 | export default interface User { 2 | id: number; 3 | login: string; 4 | avatar: string; 5 | admin: boolean; 6 | // It exists only when getting self user. 7 | hash?: string; 8 | createdAt: Date; 9 | updatedAt: Date; 10 | chatUser: ChatUser | null; 11 | } 12 | 13 | export interface ChatUser { 14 | id: string; 15 | createdAt: Date; 16 | updatedAt: Date; 17 | } 18 | 19 | export interface RateLimit { 20 | limit: number; 21 | remaining: number; 22 | reset: Date; 23 | } 24 | -------------------------------------------------------------------------------- /ui/src/react-app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /ui/src/redux/hooks.ts: -------------------------------------------------------------------------------- 1 | import { TypedUseSelectorHook, useDispatch, useSelector } from 'react-redux'; 2 | import type { RootState, AppDispatch } from './store'; 3 | 4 | // Use throughout your app instead of plain `useDispatch` and `useSelector` 5 | export const useAppDispatch = () => useDispatch(); // eslint-disable-line 6 | export const useAppSelector: TypedUseSelectorHook = useSelector; 7 | -------------------------------------------------------------------------------- /ui/src/redux/store.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@reduxjs/toolkit'; 2 | 3 | import { mainSlice, apiMiddleware } from './main'; 4 | import { homeSlice } from './home'; 5 | import { repoSlice } from './repo'; 6 | import { repoHomeSlice } from './repoHome'; 7 | import { repoDeploySlice } from './repoDeploy'; 8 | import { repoLockSlice } from './repoLock'; 9 | import { repoRollbackSlice } from './repoRollback'; 10 | import { repoSettingsSlice } from './repoSettings'; 11 | import { settingsSlice } from './settings'; 12 | import { deploymentSlice } from './deployment'; 13 | import { membersSlice } from './members'; 14 | import { activitiesSlice } from './activities'; 15 | 16 | export const store = configureStore({ 17 | reducer: { 18 | main: mainSlice.reducer, 19 | home: homeSlice.reducer, 20 | repo: repoSlice.reducer, 21 | repoHome: repoHomeSlice.reducer, 22 | repoLock: repoLockSlice.reducer, 23 | repoDeploy: repoDeploySlice.reducer, 24 | repoRollback: repoRollbackSlice.reducer, 25 | repoSettings: repoSettingsSlice.reducer, 26 | settings: settingsSlice.reducer, 27 | deployment: deploymentSlice.reducer, 28 | members: membersSlice.reducer, 29 | activities: activitiesSlice.reducer, 30 | }, 31 | middleware: (getDefaultMiddleware) => 32 | getDefaultMiddleware({ 33 | serializableCheck: false, 34 | }).concat(apiMiddleware), 35 | devTools: true, 36 | }); 37 | 38 | export type RootState = ReturnType; 39 | 40 | export type AppDispatch = typeof store.dispatch; 41 | -------------------------------------------------------------------------------- /ui/src/reportWebVitals.ts: -------------------------------------------------------------------------------- 1 | import { ReportHandler } from 'web-vitals'; 2 | 3 | const reportWebVitals = (onPerfEntry?: ReportHandler): void => { 4 | if (onPerfEntry && onPerfEntry instanceof Function) { 5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 6 | getCLS(onPerfEntry); 7 | getFID(onPerfEntry); 8 | getFCP(onPerfEntry); 9 | getLCP(onPerfEntry); 10 | getTTFB(onPerfEntry); 11 | }); 12 | } 13 | }; 14 | 15 | export default reportWebVitals; 16 | -------------------------------------------------------------------------------- /ui/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /ui/src/views/activities/ActivityHistory.tsx: -------------------------------------------------------------------------------- 1 | import { Timeline, Typography } from 'antd'; 2 | import moment from 'moment'; 3 | 4 | import { Deployment } from '../../models'; 5 | import DeploymentStatusBadge from '../../components/DeploymentStatusBadge'; 6 | import UserAvatar from '../../components/UserAvatar'; 7 | import DeploymentRefCode from '../../components/DeploymentRefCode'; 8 | import { getStatusColor } from '../../components/partials'; 9 | 10 | const { Text } = Typography; 11 | 12 | export interface ActivityHistoryProps { 13 | deployments: Deployment[]; 14 | } 15 | 16 | export default function ActivityHistory( 17 | props: ActivityHistoryProps 18 | ): JSX.Element { 19 | return ( 20 | 21 | {props.deployments.map((d, idx) => { 22 | return ( 23 | 24 |

25 | {`${d.repo?.namespace} / ${d.repo?.name}`} 26 |   27 | 30 | #{d.number} 31 | 32 |

33 |

34 | deployed{' '} 35 | to{' '} 36 | {d.env} on {moment(d.createdAt).format('LLL')}{' '} 37 | 38 |

39 |
40 | ); 41 | })} 42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /ui/src/views/deployment/DeploymentStatusSteps.tsx: -------------------------------------------------------------------------------- 1 | import { Timeline, Typography } from 'antd'; 2 | import { ClockCircleOutlined } from '@ant-design/icons'; 3 | import moment from 'moment'; 4 | 5 | import { DeploymentStatus } from '../../models'; 6 | 7 | const { Text, Link } = Typography; 8 | 9 | export interface DeploymentStatusStepsProps { 10 | statuses: DeploymentStatus[]; 11 | } 12 | 13 | export default function DeploymentStatusSteps( 14 | props: DeploymentStatusStepsProps 15 | ): JSX.Element { 16 | return ( 17 | 18 | {props.statuses.map((status, idx) => { 19 | return ( 20 | 21 | {' '} 22 | {moment(status.createdAt).format('YYYY-MM-DD HH:mm:ss')} 23 |
24 | {status.description}   25 | {status.logUrl !== '' ? ( 26 | 27 | View Detail 28 | 29 | ) : ( 30 | <> 31 | )} 32 |
33 | Updated{' '} 34 | 35 | {status.status} 36 | {' '} 37 | {moment(status.createdAt).fromNow()} 38 |
39 | ); 40 | })} 41 |
42 | ); 43 | } 44 | 45 | const getStatusColor = (status: string) => { 46 | switch (status) { 47 | case 'success': 48 | return 'green'; 49 | case 'failure': 50 | return 'red'; 51 | default: 52 | return 'purple'; 53 | } 54 | }; 55 | -------------------------------------------------------------------------------- /ui/src/views/deployment/HeaderBreadcrumb.tsx: -------------------------------------------------------------------------------- 1 | import { Breadcrumb } from 'antd'; 2 | 3 | export interface HeaderBreadcrumbProps { 4 | namespace: string; 5 | name: string; 6 | number: string; 7 | } 8 | 9 | export default function HeaderBreadcrumb({ 10 | namespace, 11 | name, 12 | number, 13 | }: HeaderBreadcrumbProps): JSX.Element { 14 | return ( 15 | 16 | 17 | Repositories 18 | 19 | {namespace} 20 | 21 | {name} 22 | 23 | Deployments 24 | {number} 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /ui/src/views/main/LicenseWarningFooter.tsx: -------------------------------------------------------------------------------- 1 | import moment from 'moment'; 2 | 3 | import { License } from '../../models'; 4 | 5 | export interface LicenseWarningFooterProps { 6 | license?: License; 7 | } 8 | 9 | export default function LicenseWarningFooter({ 10 | license, 11 | }: LicenseWarningFooterProps): JSX.Element { 12 | if (!license) { 13 | return <>; 14 | } 15 | 16 | let expired = false; 17 | let message = ''; 18 | 19 | if (license.kind === 'trial') { 20 | expired = license.memberCount >= license.memberLimit; 21 | message = 'There is no more seats. You need to purchase the license.'; 22 | } else if (license.kind === 'standard') { 23 | if (license.memberCount >= license.memberLimit) { 24 | expired = true; 25 | message = 'There is no more seats. You need to purchase more seats.'; 26 | } else if (moment(license.expiredAt).isBefore(new Date())) { 27 | expired = true; 28 | message = 'The license is expired. You need to renew the license.'; 29 | } 30 | } 31 | 32 | return ( 33 | <> 34 | {expired ? ( 35 |

44 | {message} 45 |

46 | ) : ( 47 | <> 48 | )} 49 | 50 | ); 51 | } 52 | -------------------------------------------------------------------------------- /ui/src/views/members/MemberList.tsx: -------------------------------------------------------------------------------- 1 | import { List, Switch, Button, Avatar } from 'antd'; 2 | 3 | import { User } from '../../models'; 4 | 5 | export interface MemberListProps { 6 | users: User[]; 7 | onChangeSwitch(user: User, checked: boolean): void; 8 | onClickDelete(user: User): void; 9 | } 10 | 11 | export default function MemberList(props: MemberListProps): JSX.Element { 12 | return ( 13 | ( 17 | { 24 | props.onChangeSwitch(user, checked); 25 | }} 26 | />, 27 | , 36 | ]} 37 | > 38 | } 40 | title={user.login} 41 | /> 42 | 43 | )} 44 | /> 45 | ); 46 | } 47 | -------------------------------------------------------------------------------- /ui/src/views/settings/SlackDescriptions.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Descriptions } from 'antd'; 2 | 3 | import { User } from '../../models'; 4 | 5 | export interface SlackDescriptionsProps { 6 | user?: User; 7 | } 8 | 9 | export default function SlackDescriptions({ 10 | user, 11 | }: SlackDescriptionsProps): JSX.Element { 12 | const connected = user?.chatUser ? true : false; 13 | 14 | return ( 15 | 16 | 17 | {connected ? ( 18 | 21 | ) : ( 22 | 25 | )} 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /ui/src/views/settings/UserDescriptions.tsx: -------------------------------------------------------------------------------- 1 | import { Tag, Descriptions, Input } from 'antd'; 2 | 3 | import { User } from '../../models'; 4 | 5 | export interface UserDescriptionsProps { 6 | user?: User; 7 | } 8 | 9 | export default function UserDescriptions({ 10 | user, 11 | }: UserDescriptionsProps): JSX.Element { 12 | return ( 13 | 19 | {user?.login} 20 | 21 | {user?.admin ? ( 22 | Admin 23 | ) : ( 24 | Member 25 | )} 26 | 27 | 28 | 34 | 35 | 36 | ); 37 | } 38 | -------------------------------------------------------------------------------- /ui/src/views/settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { shallowEqual } from 'react-redux'; 3 | import { Helmet } from 'react-helmet'; 4 | 5 | import { useAppSelector, useAppDispatch } from '../../redux/hooks'; 6 | import { fetchMe, checkSlack } from '../../redux/settings'; 7 | 8 | import Main from '../main'; 9 | import UserDescription, { UserDescriptionsProps } from './UserDescriptions'; 10 | import SlackDescriptions from './SlackDescriptions'; 11 | 12 | export default (): JSX.Element => { 13 | const { user, isSlackEnabled } = useAppSelector( 14 | (state) => state.settings, 15 | shallowEqual 16 | ); 17 | 18 | const dispatch = useAppDispatch(); 19 | 20 | useEffect(() => { 21 | dispatch(fetchMe()); 22 | dispatch(checkSlack()); 23 | }, [dispatch]); 24 | 25 | return ( 26 |
27 | 28 |
29 | ); 30 | }; 31 | 32 | interface SettingsProps extends UserDescriptionsProps { 33 | isSlackEnabled: boolean; 34 | } 35 | 36 | function Settings({ user, isSlackEnabled }: SettingsProps): JSX.Element { 37 | return ( 38 | <> 39 | 40 | Settings 41 | 42 |

Settings

43 |
44 | 45 |
46 | {isSlackEnabled ? ( 47 |
48 | 49 |
50 | ) : ( 51 | <> 52 | )} 53 | 54 | ); 55 | } 56 | -------------------------------------------------------------------------------- /ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "esnext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": true, 17 | "noImplicitAny": false, 18 | "jsx": "react-jsx" 19 | }, 20 | "include": ["src"] 21 | } 22 | --------------------------------------------------------------------------------