├── .dockerignore ├── .env.example ├── .eslintrc.json ├── .github ├── dependabot.yml └── workflows │ ├── checks.yml │ ├── deploy.yml │ ├── docker.yml │ ├── lint.yml │ ├── playwright.yml │ ├── release.yml │ └── scorecard.yml ├── .gitignore ├── .husky └── pre-commit ├── .lintstagedrc.js ├── .prettierignore ├── .prettierrc ├── .trunk ├── .gitignore ├── configs │ ├── .hadolint.yaml │ ├── .markdownlint.yaml │ ├── .yamllint.yaml │ └── svgo.config.js └── trunk.yaml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Dockerfile ├── Dockerfile.dev ├── LICENSE ├── README.md ├── SECURITY.MD ├── captain-definition ├── compose.yaml ├── jsconfig.json ├── next.config.mjs ├── package-lock.json ├── package.json ├── playwright.config.ts ├── postcss.config.mjs ├── prisma ├── migrations │ ├── 20240713201818_initial │ │ └── migration.sql │ ├── 20240713211224_github │ │ └── migration.sql │ ├── 20240713211758_checks │ │ └── migration.sql │ ├── 20240714110657_repository_checks │ │ └── migration.sql │ ├── 20240718101209_check_github_response │ │ └── migration.sql │ ├── 20240724121344_issues │ │ └── migration.sql │ ├── 20240724144614_branches │ │ └── migration.sql │ ├── 20240726175842_release │ │ └── migration.sql │ ├── 20240806121744_community │ │ └── migration.sql │ ├── 20240807055406_labels │ │ └── migration.sql │ ├── 20240807085914_projects │ │ └── migration.sql │ ├── 20240809093608_sources_views │ │ └── migration.sql │ ├── 20240810205354_ignore_checks │ │ └── migration.sql │ ├── 20240811100810_filtered_checks │ │ └── migration.sql │ ├── 20240813102040_is_maintainer │ │ └── migration.sql │ └── migration_lock.toml └── schema.prisma ├── public └── logo.svg ├── src ├── app │ ├── account │ │ └── repo │ │ │ ├── add │ │ │ ├── action.js │ │ │ ├── form.js │ │ │ └── page.js │ │ │ ├── checks │ │ │ └── [id] │ │ │ │ ├── action.js │ │ │ │ ├── form.js │ │ │ │ └── page.js │ │ │ ├── list │ │ │ └── page.js │ │ │ └── settings │ │ │ └── [id] │ │ │ ├── action.js │ │ │ ├── form.js │ │ │ └── page.js │ ├── api │ │ ├── auth │ │ │ └── [...nextauth] │ │ │ │ └── route.js │ │ ├── badges │ │ │ └── report │ │ │ │ └── [id] │ │ │ │ └── route.js │ │ ├── health │ │ │ └── route.js │ │ ├── report │ │ │ └── latest │ │ │ │ └── [id] │ │ │ │ └── route.js │ │ └── system │ │ │ └── checks │ │ │ └── route.js │ ├── favicon.ico │ ├── globals.css │ ├── layout.js │ ├── page.js │ ├── providers.js │ └── repo │ │ ├── list │ │ └── page.js │ │ └── report │ │ └── [id] │ │ └── page.js ├── components │ ├── ActionPanel.js │ ├── Alert.js │ ├── Banner.js │ ├── Button.js │ ├── Footer.js │ ├── Graph.js │ ├── Header.js │ ├── Heading.js │ ├── List.js │ ├── News.js │ ├── Stats.js │ ├── Tagline.js │ ├── Title.js │ ├── forms │ │ ├── Checkbox.js │ │ ├── Input.js │ │ ├── Select.js │ │ └── SubmitButton.js │ └── stats │ │ └── Platform.js ├── config │ ├── app.json │ ├── flagsmith.js │ └── flagsmith.json ├── instrumentation.js ├── models │ ├── Repository.js │ └── db.js └── utils │ ├── checks │ ├── activity.js │ ├── branches.js │ ├── codeOfConduct.js │ ├── contributing.js │ ├── defaultBranch.js │ ├── description.js │ ├── goodFirstIssue.js │ ├── index.js │ ├── issueTemplates.js │ ├── issues.js │ ├── labels.js │ ├── license.js │ ├── projects.js │ ├── pullRequestTemplate.js │ ├── readme.js │ ├── release.js │ ├── topics.js │ └── url.js │ ├── classNames.js │ └── github │ ├── extractOwnerRepo.js │ ├── getBranchesApi.js │ ├── getCommunityMetrics.js │ ├── getIssuesApi.js │ ├── getLabelsApi.js │ ├── getProjectsApi.js │ ├── getReferrersApi.js │ ├── getReleaseApi.js │ ├── getRepoApi.js │ ├── getViewsApi.js │ └── index.js ├── tailwind.config.js └── tests ├── account └── repo │ └── add.spec.ts ├── data ├── flagsmith │ ├── flags.json │ └── identities.json └── github │ ├── branches.json │ ├── community.json │ ├── issues.json │ ├── labels.json │ ├── projects.json │ ├── referrers.json │ ├── release.json │ ├── repo.json │ └── views.json ├── homepage.spec.ts └── setup ├── auth.js └── mocks ├── handlers.js └── node.js /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.classpath 2 | **/.dockerignore 3 | **/.git 4 | **/.gitignore 5 | **/.project 6 | **/.settings 7 | **/.toolstarget 8 | **/.vs 9 | **/.vscode 10 | **/.next 11 | **/.cache 12 | **/*.*proj.user 13 | **/*.dbmdl 14 | **/*.jfm 15 | **/charts 16 | **/docker-compose* 17 | **/compose.y*ml 18 | **/Dockerfile* 19 | **/node_modules 20 | **/npm-debug.log 21 | **/obj 22 | **/secrets.dev.yaml 23 | **/values.dev.yaml 24 | **/build 25 | **/dist 26 | LICENSE 27 | README.md -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID= 2 | FLAGSMITH_ENVIRONMENT_API_KEY= 3 | 4 | NEXTAUTH_URL=http://localhost:3000 5 | NEXTAUTH_SECRET=abcdefg 6 | 7 | GITHUB_ID= 8 | GITHUB_SECRET= 9 | 10 | DATABASE_URL="postgresql://user:password@localhost:5432/healthcheck?schema=public" 11 | 12 | NEXT_PUBLIC_GITHUB_CACHE=4 13 | API_TOKEN=abcdefg 14 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next/core-web-vitals", "next", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /.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://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "npm" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Run checks 2 | permissions: read-all 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: 0 3 * * * 7 | jobs: 8 | api: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: hit api 12 | uses: tyrrrz/action-http-request@master 13 | with: 14 | url: "https://healthcheck.eddiehubcommunity.org/api/system/checks?token=${{ secrets.API_TOKEN }}" 15 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Caprover 2 | permissions: read-all 3 | on: 4 | registry_package: 5 | types: [published] 6 | 7 | jobs: 8 | build-and-deploy: 9 | runs-on: ubuntu-latest 10 | 11 | strategy: 12 | matrix: 13 | node-version: [18.x] 14 | 15 | steps: 16 | - name: Deploy Image to CapRrover 17 | uses: caprover/deploy-from-github@v1.1.2 18 | with: 19 | server: "${{ secrets.CAPROVER_SERVER }}" 20 | app: healthcheck 21 | token: "${{ secrets.CAPROVER_TOKEN }}" 22 | image: ghcr.io/eddiehubcommunity/healthcheck:latest 23 | 24 | migrations: 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: 22.4 31 | - name: install dependencies 32 | run: npm ci 33 | - name: run migrations 34 | run: npm run db:migrate:prod 35 | env: 36 | DATABASE_URL: ${{ secrets.DATABASE_URL }} 37 | -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Publish Docker 2 | permissions: read-all 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: npm install and build 13 | run: | 14 | npm ci 15 | echo "NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID=${{ secrets.NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID }}" >> .env 16 | echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env 17 | npm run build 18 | - name: Save .env as artifact 19 | uses: actions/upload-artifact@v4 20 | with: 21 | name: env-file 22 | path: .env 23 | - uses: actions/upload-artifact@v4 24 | with: 25 | name: artifacts 26 | path: prod/ 27 | 28 | push_to_registry: 29 | name: Push Docker image to GitHub Packages 30 | needs: build 31 | runs-on: ubuntu-latest 32 | steps: 33 | - name: check out the repo 34 | uses: actions/checkout@v4 35 | # - name: Download .env artifact 36 | # uses: actions/download-artifact@v4 37 | # with: 38 | # name: env-file 39 | # path: . 40 | - name: npm install 41 | run: | 42 | npm ci 43 | echo "NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID=${{ secrets.NEXT_PUBLIC_FLAGSMITH_ENVIRONMENT_ID }}" >> .env 44 | echo "DATABASE_URL=${{ secrets.DATABASE_URL }}" >> .env 45 | - name: get-npm-version 46 | id: package-version 47 | uses: martinbeentjes/npm-get-version-action@master 48 | - name: version dockerfile 49 | uses: docker/setup-buildx-action@v3 50 | - name: log into GitHub Container Registry 51 | uses: docker/login-action@v3 52 | with: 53 | registry: ghcr.io 54 | username: ${{ github.repository_owner }} 55 | password: ${{ secrets.CR_PAT }} 56 | - name: push to Github Container Registry 57 | uses: docker/build-push-action@v6 58 | with: 59 | context: . 60 | push: true 61 | tags: | 62 | ghcr.io/eddiehubcommunity/healthcheck:v${{ steps.package-version.outputs.current-version }} 63 | ghcr.io/eddiehubcommunity/healthcheck:latest 64 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Linter 2 | on: 3 | push: 4 | branches: main 5 | pull_request: 6 | branches: main 7 | jobs: 8 | test: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - uses: actions/setup-node@v4 13 | with: 14 | node-version: 22.4 15 | - name: Install dependencies 16 | run: npm ci 17 | - name: Trunk Check 18 | uses: trunk-io/trunk-action@v1 19 | # with: 20 | # post-annotations: true # only for fork PRs 21 | -------------------------------------------------------------------------------- /.github/workflows/playwright.yml: -------------------------------------------------------------------------------- 1 | name: Playwright Tests 2 | on: 3 | push: 4 | branches: main 5 | pull_request: 6 | branches: main 7 | jobs: 8 | test: 9 | timeout-minutes: 60 10 | runs-on: ubuntu-latest 11 | services: 12 | postgres: 13 | image: postgres:16.3-alpine 14 | env: 15 | POSTGRES_DB: healthcheck 16 | POSTGRES_USER: user 17 | POSTGRES_PASSWORD: password 18 | # Set health checks to wait until postgres has started 19 | options: >- 20 | --health-cmd pg_isready 21 | --health-interval 10s 22 | --health-timeout 5s 23 | --health-retries 5 24 | ports: 25 | - 5432:5432 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | node-version: 22.4 31 | - name: Install dependencies 32 | run: npm ci 33 | - name: Run database migrations 34 | run: npm run db:migrate:dev 35 | - name: Install Playwright Browsers 36 | run: npx playwright install --with-deps 37 | - name: Run Playwright tests 38 | run: npm test 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | changelog: 9 | if: github.repository == 'EddieHubCommunity/HealthCheck' 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | # check out the repository with all releases 14 | - uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | # Create a temporary, uniquely named branch to push release info to 19 | - name: create temporary branch 20 | run: git branch "release-from-${{ github.sha }}" "${{ github.sha }}" 21 | 22 | # switch to the temporary branch 23 | - name: switch to new branch 24 | run: git checkout release-from-${{ github.sha }} 25 | 26 | # update app config with version 27 | - name: get-npm-version 28 | id: package-version 29 | run: | 30 | LF_VERSION=$(cat package.json | jq -r '.version') 31 | echo "current-version=$LF_VERSION" >> "$GITHUB_OUTPUT" 32 | - name: update app config 33 | run: sed -i 's/0.0.0/${{ steps.package-version.outputs.current-version}}/g' src/config/app.json 34 | 35 | # create release info and push it upstream 36 | - name: conventional Changelog Action 37 | id: changelog 38 | uses: TriPSs/conventional-changelog-action@v5 39 | with: 40 | github-token: ${{ secrets.GITHUB_TOKEN }} 41 | version-file: "./package.json,./package-lock.json,./src/config/app.json" 42 | git-branch: "release-from-${{ github.sha }}" 43 | skip-git-pull: true 44 | 45 | # create PR using GitHub CLI 46 | - name: create PR with release info 47 | if: steps.changelog.outputs.skipped == 'false' 48 | id: create-pr 49 | run: gh pr create --base main --head release-from-${{ github.sha }} --title 'Merge new release into main' --body 'Created by Github action' 50 | env: 51 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 52 | 53 | # merge PR using GitHub CLI 54 | - name: merge PR with release info 55 | if: steps.changelog.outputs.skipped == 'false' 56 | id: merge-pr 57 | run: gh pr merge --admin --merge --subject 'Merge release info' --delete-branch 58 | env: 59 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 60 | 61 | # release info is now in main so we can continue as before 62 | - name: create release with last commit 63 | if: steps.changelog.outputs.skipped == 'false' 64 | uses: ncipollo/release-action@v1 65 | with: 66 | token: ${{ secrets.CHANGELOG_RELEASE }} 67 | tag: ${{ steps.changelog.outputs.tag }} 68 | name: ${{ steps.changelog.outputs.tag }} 69 | body: ${{ steps.changelog.outputs.clean_changelog }} 70 | -------------------------------------------------------------------------------- /.github/workflows/scorecard.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. They are provided 2 | # by a third-party and are governed by separate terms of service, privacy 3 | # policy, and support documentation. 4 | 5 | name: Scorecard supply-chain security 6 | on: 7 | # For Branch-Protection check. Only the default branch is supported. See 8 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#branch-protection 9 | branch_protection_rule: 10 | # To guarantee Maintained check is occasionally updated. See 11 | # https://github.com/ossf/scorecard/blob/main/docs/checks.md#maintained 12 | schedule: 13 | - cron: 24 10 * * 4 14 | push: 15 | branches: [main] 16 | 17 | # Declare default permissions as read only. 18 | permissions: read-all 19 | 20 | jobs: 21 | analysis: 22 | name: Scorecard analysis 23 | runs-on: ubuntu-latest 24 | permissions: 25 | # Needed to upload the results to code-scanning dashboard. 26 | security-events: write 27 | # Needed to publish results and get a badge (see publish_results below). 28 | id-token: write 29 | # Uncomment the permissions below if installing in a private repository. 30 | # contents: read 31 | # actions: read 32 | 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 36 | with: 37 | persist-credentials: false 38 | 39 | - name: Run analysis 40 | uses: ossf/scorecard-action@0864cf19026789058feabb7e87baa5f140aac736 # v2.3.1 41 | with: 42 | results_file: results.sarif 43 | results_format: sarif 44 | # (Optional) "write" PAT token. Uncomment the `repo_token` line below if: 45 | # - you want to enable the Branch-Protection check on a *public* repository, or 46 | # - you are installing Scorecard on a *private* repository 47 | # To create the PAT, follow the steps in https://github.com/ossf/scorecard-action?tab=readme-ov-file#authentication-with-fine-grained-pat-optional. 48 | # repo_token: ${{ secrets.SCORECARD_TOKEN }} 49 | 50 | # Public repositories: 51 | # - Publish results to OpenSSF REST API for easy access by consumers 52 | # - Allows the repository to include the Scorecard badge. 53 | # - See https://github.com/ossf/scorecard-action#publishing-results. 54 | # For private repositories: 55 | # - `publish_results` will always be set to `false`, regardless 56 | # of the value entered here. 57 | publish_results: true 58 | 59 | # Upload the results as artifacts (optional). Commenting out will disable uploads of run results in SARIF 60 | # format to the repository Actions tab. 61 | - name: Upload artifact 62 | uses: actions/upload-artifact@97a0fba1372883ab732affbe8f94b823f91727db # v3.pre.node20 63 | with: 64 | name: SARIF file 65 | path: results.sarif 66 | retention-days: 5 67 | 68 | # Upload the results to GitHub's code scanning dashboard (optional). 69 | # Commenting out will disable upload of results to your repo's Code Scanning dashboard 70 | - name: Upload to code-scanning 71 | uses: github/codeql-action/upload-sarif@v3 72 | with: 73 | sarif_file: results.sarif 74 | -------------------------------------------------------------------------------- /.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 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | 39 | # playwright 40 | /test-results/ 41 | /playwright-report/ 42 | /blob-report/ 43 | /playwright/.cache/ 44 | 45 | .vscode 46 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | npm run lint 2 | # npm test 3 | 4 | npm run format:check 5 | -------------------------------------------------------------------------------- /.lintstagedrc.js: -------------------------------------------------------------------------------- 1 | const path = require("path"); 2 | 3 | const buildEslintCommand = (filenames) => 4 | `next lint --fix --file ${filenames 5 | .map((f) => path.relative(process.cwd(), f)) 6 | .join(" --file ")}`; 7 | 8 | module.exports = { 9 | "*.{js,jsx,ts,tsx}": [buildEslintCommand], 10 | }; 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # this file is autogenerated 2 | CHANGELOG.md 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "tabWidth": 2, 3 | "useTabs": false 4 | } 5 | -------------------------------------------------------------------------------- /.trunk/.gitignore: -------------------------------------------------------------------------------- 1 | *out 2 | *logs 3 | *actions 4 | *notifications 5 | *tools 6 | plugins 7 | user_trunk.yaml 8 | user.yaml 9 | tmp 10 | -------------------------------------------------------------------------------- /.trunk/configs/.hadolint.yaml: -------------------------------------------------------------------------------- 1 | # Following source doesn't work in most setups 2 | ignored: 3 | - SC1090 4 | - SC1091 5 | -------------------------------------------------------------------------------- /.trunk/configs/.markdownlint.yaml: -------------------------------------------------------------------------------- 1 | # Prettier friendly markdownlint config (all formatting rules disabled) 2 | extends: markdownlint/style/prettier 3 | 4 | # Disable MD024 rule 5 | MD024: false 6 | -------------------------------------------------------------------------------- /.trunk/configs/.yamllint.yaml: -------------------------------------------------------------------------------- 1 | rules: 2 | quoted-strings: 3 | required: only-when-needed 4 | extra-allowed: ["{|}"] 5 | key-duplicates: {} 6 | octal-values: 7 | forbid-implicit-octal: true 8 | -------------------------------------------------------------------------------- /.trunk/configs/svgo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | name: "preset-default", 5 | params: { 6 | overrides: { 7 | removeViewBox: false, // https://github.com/svg/svgo/issues/1128 8 | sortAttrs: true, 9 | removeOffCanvasPaths: true, 10 | }, 11 | }, 12 | }, 13 | ], 14 | }; 15 | -------------------------------------------------------------------------------- /.trunk/trunk.yaml: -------------------------------------------------------------------------------- 1 | # This file controls the behavior of Trunk: https://docs.trunk.io/cli 2 | # To learn more about the format of this file, see https://docs.trunk.io/reference/trunk-yaml 3 | version: 0.1 4 | cli: 5 | version: 1.22.2 6 | # Trunk provides extensibility via plugins. (https://docs.trunk.io/plugins) 7 | plugins: 8 | sources: 9 | - id: trunk 10 | ref: v1.6.1 11 | uri: https://github.com/trunk-io/plugins 12 | # Many linters and tools depend on runtimes - configure them here. (https://docs.trunk.io/runtimes) 13 | runtimes: 14 | enabled: 15 | - node@18.12.1 16 | - python@3.10.8 17 | # This is the section where you manage your linters. (https://docs.trunk.io/check/configuration) 18 | lint: 19 | enabled: 20 | # Prisma 21 | - prisma@5.17.0 22 | 23 | # Security + IaS config 24 | - actionlint@1.7.1 25 | - checkov@3.2.208 26 | - hadolint@2.12.0 27 | - trufflehog@3.80.1 28 | # These scan dependencies, but require network access. Enable if it fits your project. 29 | # - osv-scanner@1.8.2 30 | # - trivy@0.53.0 31 | 32 | # Toml, Yaml, Markdown, etc. 33 | # - taplo@0.9.2 34 | - markdownlint@0.41.0 35 | - yamllint@1.35.1 36 | 37 | # Formatters and linters 38 | - eslint@8.57.0 39 | - git-diff-check 40 | - prettier@3.3.3 41 | 42 | # Optimize SVGs and PNGs 43 | - svgo@3.3.2 44 | - oxipng@9.1.2 # Manually added in case you add PNGs later 45 | ignore: 46 | - linters: [markdownlint] 47 | paths: 48 | - CHANGELOG.md 49 | actions: 50 | # Optional githooks that help you run check and format before push. 51 | enabled: 52 | - trunk-announce 53 | - trunk-check-pre-push 54 | - trunk-upgrade-available 55 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [0.37.4](https://github.com/EddieHubCommunity/HealthCheck/compare/v0.37.3...v0.37.4) (2024-10-17) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * change icon from the eye to eyeslash hidden checks ([#277](https://github.com/EddieHubCommunity/HealthCheck/issues/277)) ([0f6c985](https://github.com/EddieHubCommunity/HealthCheck/commit/0f6c98556c0829ce57c4b582c7ee36103c85a101)) 7 | 8 | 9 | 10 | ## [0.37.3](https://github.com/EddieHubCommunity/HealthCheck/compare/v0.37.2...v0.37.3) (2024-10-04) 11 | 12 | 13 | ### Bug Fixes 14 | 15 | * typos in license check ([#268](https://github.com/EddieHubCommunity/HealthCheck/issues/268)) ([09b4283](https://github.com/EddieHubCommunity/HealthCheck/commit/09b428353b4be0df4aa808f7d478c061a55c3e26)) 16 | 17 | 18 | 19 | ## [0.37.2](https://github.com/EddieHubCommunity/HealthCheck/compare/v0.37.1...v0.37.2) (2024-10-04) 20 | 21 | 22 | ### Bug Fixes 23 | 24 | * button to allow props like target ([#272](https://github.com/EddieHubCommunity/HealthCheck/issues/272)) ([b592035](https://github.com/EddieHubCommunity/HealthCheck/commit/b5920350519b9186114ae1462565f9ccca85a53b)) 25 | 26 | 27 | 28 | ## [0.37.1](https://github.com/EddieHubCommunity/HealthCheck/compare/v0.37.0...v0.37.1) (2024-10-01) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * boundary conditions in checks ([227a476](https://github.com/EddieHubCommunity/HealthCheck/commit/227a4767aac87eb65ec06d52d7ef4681ac5b93e0)) 34 | 35 | 36 | 37 | # [0.37.0](https://github.com/EddieHubCommunity/HealthCheck/compare/v0.36.0...v0.37.0) (2024-10-01) 38 | 39 | 40 | ### Features 41 | 42 | * gh projects v2 to GraphQL ([#267](https://github.com/EddieHubCommunity/HealthCheck/issues/267)) ([3db9e0f](https://github.com/EddieHubCommunity/HealthCheck/commit/3db9e0fa8353477f16809c5b582f157199cfd0d7)) 43 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guide 2 | 3 | [![Contributing Guide](https://user-images.githubusercontent.com/82668196/242340741-a0124ead-97b6-488f-9271-10f2b0e1f577.jpg)](https://youtu.be/dfeSpGd8leU) 4 | 5 | 🎥 You can watch the video that Eddie created as a contributing guide. Click on the image to watch the video. 6 | 7 | ## Issues & Pull Requests 8 | 9 | ### Creating an Issue 10 | 11 | Before **creating** an Issue for `features`/`bugs`/`improvements` please follow these steps: 12 | 13 | 1. search existing Issues before creating a new issue (has someone raised this already) 14 | 1. if it doesn't exist create a new issue giving as much context as possible (please select the correct Issue type, for example `bug` or `feature`) 15 | 1. all Issues are automatically given the label `status: waiting for triage` and are automatically locked so no comments can be made 16 | 1. if you wish to work on the Issue once it has been triaged and label changed to `status: ready for dev`, please include this in your Issue description 17 | 18 | ### Working on an Issue (get it assigned to you) 19 | 20 | Before working on an existing Issue please follow these steps: 21 | 22 | 1. only ask to be assigned 1 **open** issue at a time 23 | 1. look out for the Issue label `status: ready for dev` (if it does not have this label, your work might not be accepted) 24 | 1. comment asking for the issue to be assigned to you (do not tag maintainers on GitHub or Discord as all maintainers receive your comment notifications) 25 | 1. after the Issue is assigned to you, you can start working on it 26 | 1. **only** start working on this Issue (and open a Pull Request) when it has been assigned to you - this will prevent confusion, multiple people working on the same issue and work not being used 27 | 1. do **not** enable GitHub Actions on your fork 28 | 1. reference the Issue in your Pull Request (for example `closes #123`) 29 | 1. please do **not** force push to your PR branch, this makes it very difficult to re-review - commits will be squashed when merged 30 | 31 | > Notes: 32 | > 33 | > - it is not sustainable for maintainers to review historical comments asking for assignments before the Issue label `status: ready for dev` was added; only requests for assignment of an Issue after this label has been added will be considered 34 | > - check the `Assignees` box at the top of the page to see if the issue has been assigned to someone else before requesting this be assigned to you 35 | > - if an Issue is unclear, ask questions to get more clarity before asking to have the Issue assigned to you 36 | > - only request to be assigned an Issue if you know how to work on it 37 | > - an Issue can be assigned to multiple people, if you all agree to collaborate on the issue (the Pull Request can contain commits from different collaborators) 38 | > - any Issues that have no activity after 2 weeks will be unassigned and re-assigned to someone else 39 | 40 | ## Reviewing Pull Requests 41 | 42 | We welcome everyone to review Pull Requests, it is a great way to learn, network and support each other. 43 | 44 | ### DOs 45 | 46 | - be kind and respectful, we use inclusive, gender neutral language (for example `they/them` instead of `guy/man`) 47 | - use inline comments to explain your suggestions 48 | - use inline suggestions to propose changes 49 | 50 | ### DON'Ts 51 | 52 | - do not be rude, disrespectful or aggressive 53 | - do not repeat feedback, this creates more noise than value (check the existing conversation), use GitHub reactions if you agree/disagree with a comment 54 | - do not blindly approve pull requests to improve your GitHub contributors graph 55 | 56 | > Note: Persistent non-compliance with this Contributing Guide can lead to a warning and/or ban under the [Code of Conduct](https://github.com/EddieHubCommunity/CreatorsRegistry/blob/main/CODE_OF_CONDUCT.md) 57 | 58 | ## Maintainers 59 | 60 | ### Criteria for becoming a Maintainer 61 | 62 | 1. You are a member of the EddieHub Community 63 | 2. You are active on the EddieHub Discord in helping other members of the community, particularly in the #contributor-support channel 64 | 3. You have a consistent track record of: 65 | i. reviewing Pull Requests to the Project. This includes: 66 | 67 | - adding value in your comments 68 | - helping authors with any difficulties they have 69 | - referencing the official documentation when appropriate 70 | - your tone and language is in line with that used in EddieHub 71 | ii. creating Issues which have been approved as adding value to the project 72 | iii. successfully have Pull Requests merged in relation to an Issue created either by you or another contributor 73 | 74 | You will be contacted via DM on Discord to invite you to become a maintainer on the project. 75 | 76 | We will ask you to confirm that you are happy to be given this role and have the capacity to become a maintainer. If you confirm you are happy to proceed then we will start the Onboarding process. 77 | 78 | ### Role 79 | 80 | 1. Pull Requests: comment, review and merge 81 | 2. Issues 82 | 83 | - Assigning: once the label is `status: ready for dev` please assign to a contributor in accordance with the [Contributing Guide](https://github.com/EddieHubCommunity/CreatorsRegistry/blob/main/CONTRIBUTING.md). 84 | - Bugs: if you agree that the bug is valid then please change the label from `status: awaiting triage` to `status: ready for dev`. 85 | 86 | 3. Documentation: whilst this always need improving we want to avoid contributions which are aimed only at chasing green squares (for example: making changes that add no value), and we want to ensure that the Documentation remains in the same style and standard. Example: fixing typos adds value, but changing a word from British English (the style used) to American English does not. 87 | 4. Features: so that the project remains manageable, we have a steady roll out of Features and Pull Requests are not left open for too long - please do not remove the `status: awaiting triage` label and assign. 88 | 89 | 5. Keeping the conversation going 90 | 91 | A big part of being a Maintainer on this project is reviewing and merging PRs and we also want you to contribute with new ideas through Issues, and putting them into place. However as a Maintainer we are looking for you to help us with keeping the project moving and making sure that we do not have a backlog of Pull Requests and Issues. 92 | 93 | This includes: 94 | 95 | - identifying and closing any duplicate Pull Requests and Issues 96 | - reminding contributors of our Contributing Guide 97 | 98 | _Note: Keep an eye on the `⁠#repository-maintainers` channel on Discord where we make maintainer decisions._ 99 | 100 | ### Review 101 | 102 | We will consistently review how you are doing as a Maintainer and here are things we will consider: 103 | 104 | - are you consistent (this does not mean you need to be involved every day) 105 | - are you welcoming and supportive to new contributors 106 | - are your comments clear to the author of the PR/Issue 107 | - do your comments make it easier for the author to make the necessary changes and for another maintainer to accept/merge the PR (e.g. inline comments with code change suggestions) 108 | - are you able to firmly and professionally enforce our Contributing Guide and Code of Conduct 109 | 110 | ### Recognition 111 | 112 | At this time we are unable to provide financial compensation to our Maintainers, but we are keen to be able to reward our Maintainers where we can. 113 | 114 | By becoming a Maintainer you will have access to: 115 | 116 | - Invitation to Discord Maintainers channel 117 | - Invitation to participate in Maintainers calls 118 | - Unofficial mentorship from other Maintainers 119 | - LinkedIn recommendation from Eddie (and potentially other maintainers) 120 | - Eddie's paid resources for free, for example digital courses 121 | 122 | ### Renew 123 | 124 | A project's maintainers are integral to keep it going and progressing. We do realise that this is a voluntary position and whilst Maintainers may have the time availability now, this may not be the case in the future. 125 | 126 | At the time, we have chosen not to have a specific duration for the role, however: 127 | 128 | - if there is no consistent activity by the Maintainer for an extended period of time, they will be removed from the role and and notified (this does not preclude them from becoming a Maintainer on the project again in the future). 129 | - if at any time a Maintainer wishes to no longer have this role, then they can DM `eddiejaoude` to be removed 130 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG NODE_ENV=production 2 | ARG NODE_VERSION=22 3 | FROM node:${NODE_VERSION} AS builder 4 | WORKDIR /usr/src/app 5 | 6 | COPY package*.json ./ 7 | 8 | # --omit=dev 9 | RUN npm ci --ignore-scripts 10 | 11 | COPY . . 12 | 13 | RUN npx prisma generate && ls -la 14 | RUN npm run build 15 | 16 | # Production image 17 | FROM node:${NODE_VERSION} AS production 18 | WORKDIR /usr/src/app 19 | 20 | COPY --from=builder /usr/src/app /usr/src/app 21 | 22 | # Create a non-root user and group 23 | RUN groupadd -r appuser && useradd -r -g appuser -d /usr/src/app -s /sbin/nologin appuser 24 | 25 | # Copy application files from builder 26 | COPY --from=builder /usr/src/app /usr/src/app 27 | 28 | # Change ownership of the application directory 29 | RUN chown -R appuser:appuser /usr/src/app 30 | 31 | # Switch to the non-root user 32 | USER appuser 33 | 34 | # Added healthcheck to satisfy checkov lint, you should configure this according to your application 35 | HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 CMD curl -f http://localhost:3000/api/health || exit 1 36 | 37 | EXPOSE 3000 38 | 39 | CMD ["npm", "start"] 40 | -------------------------------------------------------------------------------- /Dockerfile.dev: -------------------------------------------------------------------------------- 1 | ARG NODE_VERSION=22 2 | FROM node:${NODE_VERSION} AS builder 3 | WORKDIR /usr/src/app 4 | 5 | COPY . . 6 | 7 | RUN npm ci 8 | 9 | EXPOSE 3000 10 | 11 | CMD ["npm", "run", "dev"] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 EddieHub 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > [!NOTE] 2 | > This project is sponsored by the Open Source project Flagsmith https://github.com/Flagsmith/flagsmith 3 | > 4 | > Feature flags have so many benefits, remote config, testing in production and so much more! 5 | 6 | # HealthCheck 7 | 8 | [![HealthCheck](https://healthcheck.eddiehubcommunity.org/api/badges/report/clzr3h1ti0000daor5pydhz8y)](https://healthcheck.eddiehubcommunity.org/api/report/latest/clzr3h1ti0000daor5pydhz8y) 9 | [![RepoRater](https://repo-rater.eddiehub.org/api/badge?owner=EddieHubCommunity&name=HealthCheck)](https://repo-rater.eddiehub.org/rate?owner=EddieHubCommunity&name=HealthCheck) 10 | ![GitHub code size in bytes](https://img.shields.io/github/languages/code-size/EddieHubCommunity/HealthCheck?style=plastic) 11 | ![GitHub contributors](https://img.shields.io/github/contributors/EddieHubCommunity/HealthCheck) 12 | [![Discord](https://img.shields.io/badge/Discord-%235865F2.svg?style=plastic&logo=discord&logoColor=white)](http://discord.eddiehub.org) 13 | [![OpenSSF Scorecard](https://api.scorecard.dev/projects/github.com/EddieHubCommunity/HealthCheck/badge)](https://scorecard.dev/viewer/?uri=github.com/EddieHubCommunity/HealthCheck) 14 | 15 | How friendly is your GitHub Open Source Repo? This project will check to make sure you are using Best Practices to attract more users, contributors and Stars, as well as suggest steps as to how you can improve the Repo to achieve this. 16 | 17 | ![Screenshot of repo checks](https://github.com/user-attachments/assets/8a687b08-d380-42d7-9013-12641da4842e) 18 | 19 | ## Features 20 | 21 | - [x] GitHub OAuth 22 | - [x] Integration with Flagsmith 23 | - [x] Add GitHub repo URL 24 | - [x] List of checks and show detailed report 25 | - [x] Badges to show latest status in project's README 26 | - [ ] ... 27 | 28 | ## Usage 29 | 30 | 1. Login with your GitHub _(only public info required)_ 31 | 2. Add GitHub repo URL 32 | 3. Run HealthCheck against repo and view the report (if you have permissions on the repo, you will see more statistics) 33 | 4. Add a HealthCheck badge to your project 34 | 35 | ## Quickstart guide for local development 36 | 37 | > [!CAUTION] 38 | > Node `v21+` is required 39 | 40 | 1. Clone this GitHub Repo 41 | 2. Install the dependencies with `npm ci` 42 | 3. Copy `.env.example` to `.env` (you will need an environment key from Flagsmith, this is shown later on) 43 | 4. Create a **free** account on Flagsmith https://www.flagsmith.com (you can also sign in with GitHub) 44 | 5. Create an Organisation and Project 45 | 6. Create the Feature Flags with these steps 46 | 47 | a. Create the feature `tagline` by clicking `Create Feature` 48 | 49 | ![Create feature screenshot](https://github.com/EddieHubCommunity/HealthCheck/assets/624760/20bcf62c-a4be-487c-80ee-f5d39bcafde6) 50 | 51 | b. Fill in the Feature Flag form with these details and click `Create Feature` 52 | 53 | ![Save feature flag screenshot](https://github.com/EddieHubCommunity/HealthCheck/assets/624760/f0399aae-2b2f-4e47-83e2-9d3d21797a42) 54 | 55 | c. (OPTIONAL) Import the flags to your Flagsmith account using the file `src/config/flagsmith.json` (note this will be per environment, for example `development`) 56 | 57 | ![Import flags on Flagsmith](https://github.com/user-attachments/assets/825525e2-11ec-48a5-9c89-a45353142c29) 58 | 59 | 7. Flagsmith keys 60 | 61 | a. Get your environment client-side key from Flagsmith and add to `.env` file 62 | 63 | ![How to get environment key](https://github.com/EddieHubCommunity/HealthCheck/assets/624760/0fb56934-2d27-486a-9859-365672771407) 64 | 65 | b. Also create your environment server-side key from Flagsmith and add to `.env` file 66 | 67 | 8. To be able to log in using GitHub OAuth 68 | 69 | a. create a GitHub OAuth app on GitHub https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app 70 | 71 | ![GitHub OAuth app](https://github.com/user-attachments/assets/cf2e4358-4b13-4c86-a8bb-5c6d83c37b4a) 72 | 73 | b. In your GitHub OAuth app, GitHub will generate the `client id` and `client secret`, add these to your the `.env` file 74 | 75 | 9. Run the project with one of these 76 | 77 | a. If you have Postgres installed, you can run the app locally `npm run dev` OR 78 | 79 | b. Running with Docker Compose (Recommended if you don't have Postgres installed) 80 | 81 | 1. Once you done above steps and have the `.env` file ready 82 | 2. Run the command `docker compose up -d` 83 | 3. Once the containers are ready, run the command `npm run db:migrate:dev` 84 | 4. Visit `http://localhost:3000` in your browser 85 | 86 | c. Running in Github Codespaces 87 | 88 | 1. Start a new codespace 89 | 2. Run `npm ci` and `npm run dev` 90 | 3. When the project is running in the browser visit this and copy the generated URL. You will use this URL in place of localhost in step 8 above for creating your OAuth app 91 | 4. Update the environment file with your client id and secret. Make sure to also update the `NEXTAUTH_URL=` to the generated codespace URL as well. 92 | 5. Continue with step `9b` to run the docker container and database migration. 93 | -------------------------------------------------------------------------------- /SECURITY.MD: -------------------------------------------------------------------------------- 1 | - Please do not create GitHub issues to report security vulnerabilities. 2 | - Instead, report them via . 3 | -------------------------------------------------------------------------------- /captain-definition: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 2, 3 | "imageName": "ghcr.io/eddiehubcommunity/healthcheck:latest" 4 | } 5 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | build: 4 | context: . 5 | dockerfile: Dockerfile.dev 6 | volumes: 7 | - .:/usr/src/app 8 | - /usr/src/app/node_modules 9 | env_file: 10 | - .env 11 | environment: # To override the DB URL in the .env file 12 | - DATABASE_URL=postgresql://user:password@postgres:5432/healthcheck?schema=public 13 | depends_on: 14 | postgres: 15 | condition: service_healthy 16 | ports: 17 | - 3000:3000 18 | networks: 19 | - healthcheck 20 | restart: always 21 | container_name: app 22 | 23 | postgres: 24 | image: postgres:16.3-alpine 25 | ports: 26 | - 5432:5432 27 | restart: always 28 | environment: 29 | - POSTGRES_DB=healthcheck 30 | - POSTGRES_USER=user 31 | - POSTGRES_PASSWORD=password 32 | networks: 33 | - healthcheck 34 | healthcheck: 35 | test: [CMD, pg_isready, -U, user, -d, healthcheck] 36 | interval: 10s 37 | timeout: 5s 38 | retries: 5 39 | container_name: postgres 40 | 41 | networks: 42 | healthcheck: 43 | driver: bridge 44 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "paths": { 4 | "@/*": ["./src/*"] 5 | } 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | dangerouslyAllowSVG: true, 5 | remotePatterns: [ 6 | { 7 | protocol: "http", 8 | hostname: "localhost", 9 | }, 10 | { 11 | protocol: "https", 12 | hostname: "healthcheck.eddiehubcommunity.org", 13 | }, 14 | { 15 | protocol: "https", 16 | hostname: "github.com", 17 | }, 18 | { 19 | protocol: "https", 20 | hostname: "avatars.githubusercontent.com", 21 | }, 22 | ], 23 | }, 24 | experimental: { 25 | instrumentationHook: true, 26 | }, 27 | }; 28 | 29 | export default nextConfig; 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "healthcheck", 3 | "version": "0.37.4", 4 | "private": true, 5 | "engines": { 6 | "node": ">=21" 7 | }, 8 | "scripts": { 9 | "dev": "next dev", 10 | "dev-wsl": "HOST=$(hostname -I | awk '{print $1}') && next dev --hostname ${HOST}", 11 | "build": "next build", 12 | "start": "next start", 13 | "lint": "set TRUNK_TELEMETRY=off && trunk check", 14 | "fmt": "set TRUNK_TELEMETRY=off && trunk fmt", 15 | "lint-old": "next lint", 16 | "test": "npx playwright test", 17 | "prepare": "husky", 18 | "postinstall": "shx cp -n ./.env.example ./.env", 19 | "format:write": "prettier . --write", 20 | "format:check": "prettier . --check", 21 | "db:migrate:dev": "npx prisma migrate dev", 22 | "db:migrate:prod": "prisma migrate deploy" 23 | }, 24 | "dependencies": { 25 | "@auth/prisma-adapter": "^2.4.2", 26 | "@headlessui/react": "^2.1.5", 27 | "@heroicons/react": "^2.1.5", 28 | "@octokit/graphql": "^8.1.1", 29 | "@octokit/rest": "^21.0.0", 30 | "@prisma/client": "^5.19.0", 31 | "@tailwindcss/aspect-ratio": "^0.4.2", 32 | "@tailwindcss/forms": "^0.5.7", 33 | "@trunkio/launcher": "^1.3.1", 34 | "apexcharts": "^3.53.0", 35 | "badge-maker": "^4.0.0", 36 | "date-fns": "^3.6.0", 37 | "flagsmith": "^4.0.3", 38 | "flagsmith-nodejs": "^3.3.3", 39 | "flowbite-react": "^0.10.1", 40 | "next": "14.2.4", 41 | "next-auth": "^4.24.7", 42 | "react": "^18", 43 | "react-apexcharts": "^1.4.1", 44 | "react-dom": "^18", 45 | "zod": "^3.23.8" 46 | }, 47 | "devDependencies": { 48 | "@playwright/test": "^1.45.1", 49 | "@types/node": "^20.16.5", 50 | "cross-env": "^7.0.3", 51 | "eslint": "^8", 52 | "eslint-config-next": "14.2.4", 53 | "eslint-config-prettier": "^9.1.0", 54 | "flagsmith-cli": "^0.2.0", 55 | "husky": "^9.1.5", 56 | "msw": "^2.3.4", 57 | "postcss": "^8", 58 | "prettier": "3.3.3", 59 | "prisma": "^5.16.2", 60 | "shx": "^0.3.4", 61 | "tailwindcss": "^3.4.1" 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | /** 4 | * Read environment variables from file. 5 | * https://github.com/motdotla/dotenv 6 | */ 7 | // import dotenv from 'dotenv'; 8 | // dotenv.config({ path: path.resolve(__dirname, '.env') }); 9 | 10 | /** 11 | * See https://playwright.dev/docs/test-configuration. 12 | */ 13 | export default defineConfig({ 14 | testDir: "./tests", 15 | /* Run tests in files in parallel */ 16 | fullyParallel: true, 17 | /* Fail the build on CI if you accidentally left test.only in the source code. */ 18 | forbidOnly: !!process.env.CI, 19 | /* Retry on CI only */ 20 | retries: process.env.CI ? 2 : 0, 21 | /* Opt out of parallel tests on CI. */ 22 | workers: process.env.CI ? 1 : undefined, 23 | /* Reporter to use. See https://playwright.dev/docs/test-reporters */ 24 | reporter: process.env.CI ? "github" : "html", 25 | /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ 26 | use: { 27 | /* Base URL to use in actions like `await page.goto('/')`. */ 28 | baseURL: "http://127.0.0.1:3000", 29 | 30 | /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ 31 | trace: "on-first-retry", 32 | }, 33 | 34 | /* Configure projects for major browsers */ 35 | projects: [ 36 | { 37 | name: "chromium", 38 | use: { ...devices["Desktop Chrome"] }, 39 | }, 40 | 41 | // { 42 | // name: "firefox", 43 | // use: { ...devices["Desktop Firefox"] }, 44 | // }, 45 | 46 | // { 47 | // name: "webkit", 48 | // use: { ...devices["Desktop Safari"] }, 49 | // }, 50 | 51 | /* Test against mobile viewports. */ 52 | // { 53 | // name: 'Mobile Chrome', 54 | // use: { ...devices['Pixel 5'] }, 55 | // }, 56 | // { 57 | // name: 'Mobile Safari', 58 | // use: { ...devices['iPhone 12'] }, 59 | // }, 60 | 61 | /* Test against branded browsers. */ 62 | // { 63 | // name: 'Microsoft Edge', 64 | // use: { ...devices['Desktop Edge'], channel: 'msedge' }, 65 | // }, 66 | // { 67 | // name: 'Google Chrome', 68 | // use: { ...devices['Desktop Chrome'], channel: 'chrome' }, 69 | // }, 70 | ], 71 | 72 | /* Run your local dev server before starting the tests */ 73 | webServer: { 74 | command: "cross-env APP_ENV=test npm run dev", 75 | url: "http://127.0.0.1:3000", 76 | reuseExistingServer: !process.env.CI, 77 | }, 78 | }); 79 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /prisma/migrations/20240713201818_initial/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "User" ( 3 | "id" TEXT NOT NULL, 4 | "name" TEXT, 5 | "email" TEXT NOT NULL, 6 | "emailVerified" TIMESTAMP(3), 7 | "image" TEXT, 8 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 9 | "updatedAt" TIMESTAMP(3) NOT NULL, 10 | 11 | CONSTRAINT "User_pkey" PRIMARY KEY ("id") 12 | ); 13 | 14 | -- CreateTable 15 | CREATE TABLE "Account" ( 16 | "userId" TEXT NOT NULL, 17 | "type" TEXT NOT NULL, 18 | "provider" TEXT NOT NULL, 19 | "providerAccountId" TEXT NOT NULL, 20 | "refresh_token" TEXT, 21 | "access_token" TEXT, 22 | "expires_at" INTEGER, 23 | "token_type" TEXT, 24 | "scope" TEXT, 25 | "id_token" TEXT, 26 | "session_state" TEXT, 27 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 28 | "updatedAt" TIMESTAMP(3) NOT NULL, 29 | 30 | CONSTRAINT "Account_pkey" PRIMARY KEY ("provider","providerAccountId") 31 | ); 32 | 33 | -- CreateTable 34 | CREATE TABLE "Session" ( 35 | "sessionToken" TEXT NOT NULL, 36 | "userId" TEXT NOT NULL, 37 | "expires" TIMESTAMP(3) NOT NULL, 38 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 39 | "updatedAt" TIMESTAMP(3) NOT NULL 40 | ); 41 | 42 | -- CreateTable 43 | CREATE TABLE "VerificationToken" ( 44 | "identifier" TEXT NOT NULL, 45 | "token" TEXT NOT NULL, 46 | "expires" TIMESTAMP(3) NOT NULL, 47 | 48 | CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("identifier","token") 49 | ); 50 | 51 | -- CreateTable 52 | CREATE TABLE "Authenticator" ( 53 | "credentialID" TEXT NOT NULL, 54 | "userId" TEXT NOT NULL, 55 | "providerAccountId" TEXT NOT NULL, 56 | "credentialPublicKey" TEXT NOT NULL, 57 | "counter" INTEGER NOT NULL, 58 | "credentialDeviceType" TEXT NOT NULL, 59 | "credentialBackedUp" BOOLEAN NOT NULL, 60 | "transports" TEXT, 61 | 62 | CONSTRAINT "Authenticator_pkey" PRIMARY KEY ("userId","credentialID") 63 | ); 64 | 65 | -- CreateIndex 66 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); 67 | 68 | -- CreateIndex 69 | CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); 70 | 71 | -- CreateIndex 72 | CREATE UNIQUE INDEX "Authenticator_credentialID_key" ON "Authenticator"("credentialID"); 73 | 74 | -- AddForeignKey 75 | ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 76 | 77 | -- AddForeignKey 78 | ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 79 | 80 | -- AddForeignKey 81 | ALTER TABLE "Authenticator" ADD CONSTRAINT "Authenticator_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 82 | -------------------------------------------------------------------------------- /prisma/migrations/20240713211224_github/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Repository" ( 3 | "id" TEXT NOT NULL, 4 | "url" TEXT NOT NULL, 5 | "userId" TEXT NOT NULL, 6 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 7 | "updatedAt" TIMESTAMP(3) NOT NULL, 8 | 9 | CONSTRAINT "Repository_pkey" PRIMARY KEY ("id") 10 | ); 11 | 12 | -- CreateTable 13 | CREATE TABLE "GithubResponses" ( 14 | "id" TEXT NOT NULL, 15 | "repo" JSONB NOT NULL, 16 | "repositoryId" TEXT NOT NULL, 17 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 | "updatedAt" TIMESTAMP(3) NOT NULL, 19 | 20 | CONSTRAINT "GithubResponses_pkey" PRIMARY KEY ("id") 21 | ); 22 | 23 | -- AddForeignKey 24 | ALTER TABLE "Repository" ADD CONSTRAINT "Repository_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; 25 | 26 | -- AddForeignKey 27 | ALTER TABLE "GithubResponses" ADD CONSTRAINT "GithubResponses_repositoryId_fkey" FOREIGN KEY ("repositoryId") REFERENCES "Repository"("id") ON DELETE CASCADE ON UPDATE CASCADE; 28 | -------------------------------------------------------------------------------- /prisma/migrations/20240713211758_checks/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - You are about to drop the `GithubResponses` table. If the table is not empty, all the data it contains will be lost. 5 | 6 | */ 7 | -- DropForeignKey 8 | ALTER TABLE "GithubResponses" DROP CONSTRAINT "GithubResponses_repositoryId_fkey"; 9 | 10 | -- DropTable 11 | DROP TABLE "GithubResponses"; 12 | 13 | -- CreateTable 14 | CREATE TABLE "GithubResponse" ( 15 | "id" TEXT NOT NULL, 16 | "repo" JSONB NOT NULL, 17 | "repositoryId" TEXT NOT NULL, 18 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 19 | "updatedAt" TIMESTAMP(3) NOT NULL, 20 | 21 | CONSTRAINT "GithubResponse_pkey" PRIMARY KEY ("id") 22 | ); 23 | 24 | -- CreateTable 25 | CREATE TABLE "Check" ( 26 | "id" TEXT NOT NULL, 27 | "red" INTEGER NOT NULL, 28 | "amber" INTEGER NOT NULL, 29 | "green" INTEGER NOT NULL, 30 | "data" JSONB NOT NULL, 31 | "repositoryId" TEXT NOT NULL, 32 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 33 | "updatedAt" TIMESTAMP(3) NOT NULL, 34 | 35 | CONSTRAINT "Check_pkey" PRIMARY KEY ("id") 36 | ); 37 | 38 | -- AddForeignKey 39 | ALTER TABLE "GithubResponse" ADD CONSTRAINT "GithubResponse_repositoryId_fkey" FOREIGN KEY ("repositoryId") REFERENCES "Repository"("id") ON DELETE CASCADE ON UPDATE CASCADE; 40 | 41 | -- AddForeignKey 42 | ALTER TABLE "Check" ADD CONSTRAINT "Check_repositoryId_fkey" FOREIGN KEY ("repositoryId") REFERENCES "Repository"("id") ON DELETE CASCADE ON UPDATE CASCADE; 43 | -------------------------------------------------------------------------------- /prisma/migrations/20240714110657_repository_checks/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `healthchecks` to the `Check` table without a default value. This is not possible if the table is not empty. 5 | - Added the required column `owner` to the `Repository` table without a default value. This is not possible if the table is not empty. 6 | - Added the required column `repo` to the `Repository` table without a default value. This is not possible if the table is not empty. 7 | 8 | */ 9 | -- AlterTable 10 | ALTER TABLE "Check" ADD COLUMN "healthchecks" JSONB NOT NULL; 11 | 12 | -- AlterTable 13 | ALTER TABLE "Repository" ADD COLUMN "owner" TEXT NOT NULL, 14 | ADD COLUMN "repo" TEXT NOT NULL; 15 | -------------------------------------------------------------------------------- /prisma/migrations/20240718101209_check_github_response/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `githubResponseId` to the `Check` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Check" ADD COLUMN "githubResponseId" TEXT NOT NULL; 9 | 10 | -- AddForeignKey 11 | ALTER TABLE "Check" ADD CONSTRAINT "Check_githubResponseId_fkey" FOREIGN KEY ("githubResponseId") REFERENCES "GithubResponse"("id") ON DELETE CASCADE ON UPDATE CASCADE; 12 | -------------------------------------------------------------------------------- /prisma/migrations/20240724121344_issues/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "issues" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240724144614_branches/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "branches" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240726175842_release/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "release" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240806121744_community/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "communityMetrics" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240807055406_labels/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "labels" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240807085914_projects/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "projects" JSONB; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240809093608_sources_views/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "GithubResponse" ADD COLUMN "referrers" JSONB, 3 | ADD COLUMN "views" JSONB; 4 | -------------------------------------------------------------------------------- /prisma/migrations/20240810205354_ignore_checks/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Repository" ADD COLUMN "ignoreChecks" TEXT[]; 3 | -------------------------------------------------------------------------------- /prisma/migrations/20240811100810_filtered_checks/migration.sql: -------------------------------------------------------------------------------- 1 | /* 2 | Warnings: 3 | 4 | - Added the required column `allData` to the `Check` table without a default value. This is not possible if the table is not empty. 5 | 6 | */ 7 | -- AlterTable 8 | ALTER TABLE "Check" ADD COLUMN "allData" JSONB NOT NULL, 9 | ADD COLUMN "ignoreChecks" TEXT[]; 10 | -------------------------------------------------------------------------------- /prisma/migrations/20240813102040_is_maintainer/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "User" ADD COLUMN "isMaintainer" BOOLEAN NOT NULL DEFAULT false; 3 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | // Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? 5 | // Try Prisma Accelerate: https://pris.ly/cli/accelerate-init 6 | 7 | datasource db { 8 | provider = "postgresql" 9 | url = env("DATABASE_URL") 10 | } 11 | 12 | generator client { 13 | provider = "prisma-client-js" 14 | } 15 | 16 | model User { 17 | id String @id @default(cuid()) 18 | name String? 19 | email String @unique 20 | emailVerified DateTime? 21 | image String? 22 | isMaintainer Boolean @default(false) 23 | accounts Account[] 24 | sessions Session[] 25 | repositories Repository[] 26 | // Optional for WebAuthn support 27 | Authenticator Authenticator[] 28 | 29 | createdAt DateTime @default(now()) 30 | updatedAt DateTime @updatedAt 31 | } 32 | 33 | model Account { 34 | userId String 35 | type String 36 | provider String 37 | providerAccountId String 38 | refresh_token String? 39 | access_token String? 40 | expires_at Int? 41 | token_type String? 42 | scope String? 43 | id_token String? 44 | session_state String? 45 | 46 | createdAt DateTime @default(now()) 47 | updatedAt DateTime @updatedAt 48 | 49 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 50 | 51 | @@id([provider, providerAccountId]) 52 | } 53 | 54 | model Session { 55 | sessionToken String @unique 56 | userId String 57 | expires DateTime 58 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 59 | 60 | createdAt DateTime @default(now()) 61 | updatedAt DateTime @updatedAt 62 | } 63 | 64 | model VerificationToken { 65 | identifier String 66 | token String 67 | expires DateTime 68 | 69 | @@id([identifier, token]) 70 | } 71 | 72 | // Optional for WebAuthn support 73 | model Authenticator { 74 | credentialID String @unique 75 | userId String 76 | providerAccountId String 77 | credentialPublicKey String 78 | counter Int 79 | credentialDeviceType String 80 | credentialBackedUp Boolean 81 | transports String? 82 | 83 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 84 | 85 | @@id([userId, credentialID]) 86 | } 87 | 88 | model Repository { 89 | id String @id @default(cuid()) 90 | url String 91 | owner String 92 | repo String 93 | ignoreChecks String[] 94 | 95 | githubResponses GithubResponse[] 96 | checks Check[] 97 | userId String 98 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 99 | 100 | createdAt DateTime @default(now()) 101 | updatedAt DateTime @updatedAt 102 | } 103 | 104 | model GithubResponse { 105 | id String @id @default(cuid()) 106 | repo Json 107 | issues Json? 108 | branches Json? 109 | release Json? 110 | communityMetrics Json? 111 | labels Json? 112 | projects Json? 113 | referrers Json? 114 | views Json? 115 | 116 | checks Check[] 117 | repositoryId String 118 | repository Repository @relation(fields: [repositoryId], references: [id], onDelete: Cascade) 119 | 120 | createdAt DateTime @default(now()) 121 | updatedAt DateTime @updatedAt 122 | } 123 | 124 | model Check { 125 | id String @id @default(cuid()) 126 | red Int 127 | amber Int 128 | green Int 129 | healthchecks Json 130 | data Json 131 | allData Json 132 | ignoreChecks String[] 133 | 134 | repositoryId String 135 | repository Repository @relation(fields: [repositoryId], references: [id], onDelete: Cascade) 136 | githubResponseId String 137 | githubResponse GithubResponse @relation(fields: [githubResponseId], references: [id], onDelete: Cascade) 138 | 139 | createdAt DateTime @default(now()) 140 | updatedAt DateTime @updatedAt 141 | } 142 | -------------------------------------------------------------------------------- /public/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/account/repo/add/action.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { getServerSession } from "next-auth/next"; 5 | 6 | import prisma from "@/models/db"; 7 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 8 | import Repository from "@/models/Repository"; 9 | import getRepoApi from "@/utils/github/getRepoApi"; 10 | import getAllRepoData from "@/utils/github"; 11 | import checks from "@/utils/checks"; 12 | 13 | export async function getRepo(prevState, formData) { 14 | // check authentication 15 | const session = await getServerSession(authOptions); 16 | if (!session) { 17 | throw new Error("Not authenticated"); 18 | } 19 | 20 | const url = formData.get("url"); 21 | 22 | const validate = Repository({ url }); 23 | 24 | if (!validate.success) { 25 | return validate; 26 | } 27 | 28 | // get user's account 29 | const user = await prisma.user.findUnique({ 30 | where: { id: session.user.id }, 31 | include: { 32 | repositories: true, 33 | accounts: true, 34 | }, 35 | }); 36 | 37 | const response = await getRepoApi(url, user.accounts[0].access_token); 38 | 39 | // if repo not found show error 40 | if (!response || response.status !== 200) { 41 | console.log( 42 | "repo not found ------------>>>>", 43 | url, 44 | `...by user ${session.user.name}`, 45 | ); 46 | return { 47 | data: { url }, 48 | succcess: false, 49 | errors: { url: ["GitHub repository not found"] }, 50 | }; 51 | } 52 | 53 | // check if user has repo url already 54 | let userRepo = await prisma.repository.findFirst({ 55 | where: { url, userId: session.user.id }, 56 | }); 57 | 58 | // save repo url to db if doesn't exist for user 59 | if (!userRepo) { 60 | userRepo = await prisma.repository.create({ 61 | data: { 62 | user: { 63 | connect: { 64 | id: session.user.id, 65 | }, 66 | }, 67 | url, 68 | owner: response.data.owner.login, 69 | repo: response.data.name, 70 | }, 71 | }); 72 | } 73 | 74 | const responses = await getAllRepoData(url, user.accounts[0].access_token); 75 | 76 | // save github api data 77 | const githubResponseRepo = await prisma.githubResponse.create({ 78 | data: { 79 | repository: { 80 | connect: { 81 | id: userRepo.id, 82 | }, 83 | }, 84 | ...responses, 85 | }, 86 | }); 87 | 88 | // perform check 89 | const results = checks(githubResponseRepo); 90 | 91 | // save results 92 | const check = await prisma.check.create({ 93 | data: { 94 | repository: { 95 | connect: { id: userRepo.id }, 96 | }, 97 | githubResponse: { 98 | connect: { id: githubResponseRepo.id }, 99 | }, 100 | red: results.summary.error?.length || 0, 101 | amber: results.summary.warning?.length || 0, 102 | green: results.summary.success?.length || 0, 103 | healthchecks: results.checks.map((check) => check.id), 104 | data: results.checks, 105 | allData: results.allChecks, 106 | ignoreChecks: results.ignoreChecks, 107 | }, 108 | }); 109 | 110 | redirect(`/repo/report/${check.id}`); 111 | } 112 | -------------------------------------------------------------------------------- /src/app/account/repo/add/form.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { useFlags } from "flagsmith/react"; 5 | 6 | import { getRepo } from "./action"; 7 | import { SubmitButton } from "@/components/forms/SubmitButton"; 8 | import Input from "@/components/forms/Input"; 9 | import Checkbox from "@/components/forms/Checkbox"; 10 | import Alert from "@/components/Alert"; 11 | 12 | const initialState = { 13 | data: undefined, 14 | success: undefined, 15 | errors: undefined, 16 | }; 17 | 18 | export default function Form({ usage }) { 19 | const { repolimit } = useFlags(["repolimit"]); 20 | const [state, formAction] = useFormState(getRepo, initialState); 21 | const disabled = usage >= repolimit.value; 22 | 23 | return ( 24 |
25 | 26 | You have ({usage}/{repolimit.value}) repos remaining 27 | 28 |
29 |
30 |

31 | This will load the GitHub repo information from the GitHub API 32 |

33 | 34 |
35 | 43 |
44 |
45 |
46 | 47 |
48 | Visibility 49 |
50 | 58 |
59 |
60 | 61 |
62 | 63 | Notifications 64 | 65 |
66 | 73 | 80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | ); 88 | } 89 | -------------------------------------------------------------------------------- /src/app/account/repo/add/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import prisma from "@/models/db"; 3 | import { getServerSession } from "next-auth/next"; 4 | 5 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 6 | import Title from "@/components/Title"; 7 | import Form from "./form"; 8 | 9 | export default async function Page() { 10 | // check authentication 11 | const session = await getServerSession(authOptions); 12 | if (!session) { 13 | redirect("/"); 14 | } 15 | 16 | const user = await prisma.user.findUnique({ 17 | where: { id: session.user.id }, 18 | include: { 19 | repositories: true, 20 | }, 21 | }); 22 | 23 | return ( 24 |
25 | 26 | <Form usage={user.repositories.length} /> 27 | </div> 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /src/app/account/repo/checks/[id]/action.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { getServerSession } from "next-auth/next"; 5 | import { differenceInHours } from "date-fns"; 6 | import Flagsmith from "flagsmith-nodejs"; 7 | 8 | import prisma from "@/models/db"; 9 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 10 | import checks from "@/utils/checks"; 11 | import getAllRepoData from "@/utils/github"; 12 | 13 | // TODO: move to a reusable location (server-side only) 14 | const flagsmith = new Flagsmith({ 15 | environmentKey: process.env.FLAGSMITH_ENVIRONMENT_API_KEY, 16 | }); 17 | 18 | export async function performChecks(formData) { 19 | const session = await getServerSession(authOptions); 20 | 21 | if (!session) { 22 | throw new Error("Not authenticated"); 23 | } 24 | 25 | let flags; 26 | let githbCacheDuration; 27 | try { 28 | flags = await flagsmith.getEnvironmentFlags(); 29 | githbCacheDuration = flags.getFeatureValue("github_cache"); 30 | } catch (e) { 31 | console.log(e); 32 | githbCacheDuration = process.env.NEXT_PUBLIC_GITHUB_CACHE; 33 | } 34 | 35 | const id = formData.get("id"); 36 | 37 | const repository = await prisma.repository.findUnique({ 38 | where: { id, user: { id: session.user.id } }, 39 | include: { 40 | user: true, 41 | githubResponses: { 42 | orderBy: { 43 | createdAt: "desc", 44 | }, 45 | take: 1, 46 | }, 47 | }, 48 | }); 49 | 50 | // if github data is older than 24 hours fetch again 51 | let githubResponseRepo = repository.githubResponses[0]; 52 | if ( 53 | !githubResponseRepo || 54 | differenceInHours(new Date(), githubResponseRepo.createdAt) >= 55 | githbCacheDuration 56 | ) { 57 | const user = await prisma.user.findUnique({ 58 | where: { id: session.user.id }, 59 | include: { 60 | accounts: true, 61 | }, 62 | }); 63 | const responses = await getAllRepoData( 64 | repository.url, 65 | user.accounts[0].access_token, 66 | ); 67 | 68 | githubResponseRepo = await prisma.githubResponse.create({ 69 | data: { 70 | repository: { 71 | connect: { 72 | id: repository.id, 73 | }, 74 | }, 75 | ...responses, 76 | }, 77 | }); 78 | } 79 | 80 | // perform check 81 | const results = checks(githubResponseRepo, repository.ignoreChecks); 82 | 83 | // save results 84 | const check = await prisma.check.create({ 85 | data: { 86 | repository: { 87 | connect: { id }, 88 | }, 89 | githubResponse: { 90 | connect: { id: githubResponseRepo.id }, 91 | }, 92 | red: results.summary.error?.length || 0, 93 | amber: results.summary.warning?.length || 0, 94 | green: results.summary.success?.length || 0, 95 | healthchecks: results.checks.map((check) => check.id), 96 | data: results.checks, 97 | allData: results.allChecks, 98 | ignoreChecks: results.ignoreChecks, 99 | }, 100 | }); 101 | 102 | redirect(`/repo/report/${check.id}`); 103 | } 104 | -------------------------------------------------------------------------------- /src/app/account/repo/checks/[id]/form.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { 4 | DocumentDuplicateIcon, 5 | CheckBadgeIcon, 6 | } from "@heroicons/react/20/solid"; 7 | import { useState } from "react"; 8 | 9 | import { performChecks } from "./action"; 10 | import Input from "@/components/forms/Input"; 11 | import { SubmitButton } from "@/components/forms/SubmitButton"; 12 | import classNames from "@/utils/classNames"; 13 | import Image from "next/image"; 14 | 15 | export default function Form({ id }) { 16 | return ( 17 | <form action={performChecks}> 18 | <Input type="hidden" id="id" name="id" value={id} /> 19 | <SubmitButton text="Run Checks" /> 20 | </form> 21 | ); 22 | } 23 | 24 | export function FormBadge({ id, baseUrl }) { 25 | const src = `${baseUrl}/api/badges/report/${id}`; 26 | const url = `![HealthCheck](${src})`; 27 | const clickUrl = `[${url}](${baseUrl}/api/report/latest/${id})`; 28 | const [copy, setCopy] = useState(false); 29 | const copyHandle = async () => { 30 | await navigator.clipboard.writeText(clickUrl); 31 | setCopy(true); 32 | }; 33 | 34 | return ( 35 | <> 36 | <div> 37 | <label 38 | htmlFor="badge" 39 | className="block text-sm font-medium leading-6 text-white" 40 | > 41 | Add badge to your Repo‘s README to show the latest check status 42 | </label> 43 | <div className="mt-2 flex rounded-md shadow-sm"> 44 | <div className="relative flex flex-grow items-stretch focus-within:z-10"> 45 | <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3"> 46 | <CheckBadgeIcon 47 | aria-hidden="true" 48 | className="h-5 w-5 text-gray-400" 49 | /> 50 | </div> 51 | <input 52 | readOnly={true} 53 | id="badge" 54 | name="badge" 55 | type="text" 56 | value={clickUrl} 57 | className="block w-full rounded-md border-0 py-1.5 pl-10 text-gray-900 ring-1 ring-inset ring-gray-300 placeholder:text-gray-400 focus:ring-2 focus:ring-inset focus:ring-indigo-600 sm:text-sm sm:leading-6" 58 | /> 59 | </div> 60 | <button 61 | type="button" 62 | className="relative -ml-px inline-flex items-center gap-x-1.5 rounded-r-md px-3 py-2 text-sm font-semibold text-gray-900 ring-1 ring-inset ring-gray-300 hover:bg-gray-50" 63 | onClick={copyHandle} 64 | > 65 | <DocumentDuplicateIcon 66 | aria-hidden="true" 67 | className={classNames( 68 | "-ml-0.5 h-5 w-5 text-gray-400", 69 | copy && "text-green-400", 70 | )} 71 | /> 72 | {copy === true ? ( 73 | <span className="text-green-400">Copied!</span> 74 | ) : ( 75 | <span className="text-gray-400">Copy</span> 76 | )} 77 | </button> 78 | </div> 79 | </div> 80 | <Image 81 | src={src} 82 | className="mt-2" 83 | alt="HealthCheck latest status badge" 84 | width={200} 85 | height={40} 86 | /> 87 | </> 88 | ); 89 | } 90 | -------------------------------------------------------------------------------- /src/app/account/repo/checks/[id]/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getServerSession } from "next-auth/next"; 3 | import { format, formatDistance } from "date-fns"; 4 | 5 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 6 | import prisma from "@/models/db"; 7 | import List from "@/components/List"; 8 | import Title from "@/components/Title"; 9 | import Form, { FormBadge } from "./form"; 10 | import { worstCheck } from "@/utils/checks"; 11 | import ActionPanel from "@/components/ActionPanel"; 12 | import Graph from "@/components/Graph"; 13 | import { Card } from "flowbite-react"; 14 | import Alert from "@/components/Alert"; 15 | import Button from "@/components/Button"; 16 | 17 | export default async function Page({ params }) { 18 | const { id } = params; 19 | const session = await getServerSession(authOptions); 20 | if (!session) { 21 | redirect("/"); 22 | } 23 | 24 | const repository = await prisma.repository.findUnique({ 25 | where: { id, user: { id: session.user.id } }, 26 | include: { 27 | user: true, 28 | checks: { 29 | orderBy: { 30 | createdAt: "desc", 31 | }, 32 | }, 33 | githubResponses: { 34 | orderBy: { 35 | createdAt: "desc", 36 | }, 37 | take: 1, 38 | }, 39 | }, 40 | }); 41 | 42 | return ( 43 | <> 44 | <Title 45 | text={`Check list for the repo: ${repository.owner} / ${repository.repo}`} 46 | > 47 | <Button url={`/account/repo/settings/${id}`} type="secondary"> 48 | Settings 49 | </Button> 50 | <Form id={id} /> 51 | 52 | 53 | {repository.githubResponses[0].referrers?.length && 54 | repository.githubResponses[0].views.views ? ( 55 |
56 | 57 |
58 |
59 |
60 | {repository.githubResponses[0].views.count} 61 |
62 |

63 | Views this week 64 |

65 |
66 |
67 | format(timestamp, "MM/dd/yyyy"), 72 | ), 73 | }} 74 | series={[ 75 | { 76 | name: "Views", 77 | data: repository.githubResponses[0].views.views.map( 78 | ({ count }) => count, 79 | ), 80 | }, 81 | { 82 | name: "Unique", 83 | data: repository.githubResponses[0].views.views.map( 84 | ({ uniques }) => uniques, 85 | ), 86 | }, 87 | ]} 88 | /> 89 |
90 | 91 |
92 |
93 |
94 | {repository.githubResponses[0].referrers.reduce( 95 | (n, { count }) => n + count, 96 | 0, 97 | )} 98 |
99 |

100 | Referred this week 101 |

102 |
103 |
104 | referrer, 109 | ), 110 | }} 111 | series={[ 112 | { 113 | name: "Referrers", 114 | data: repository.githubResponses[0].referrers.map( 115 | ({ count }) => count, 116 | ), 117 | }, 118 | { 119 | name: "Unique", 120 | data: repository.githubResponses[0].referrers.map( 121 | ({ uniques }) => uniques, 122 | ), 123 | }, 124 | ]} 125 | /> 126 |
127 |
128 | ) : ( 129 | 130 | You do not have permission to see the analytics for this repo 131 | 132 | )} 133 | 134 | 135 | 136 | 137 | 138 | ({ 140 | id: check.id, 141 | href: `/repo/report/${check.id}`, 142 | title: `Error: ${check.red}, Warning: ${check.amber}, Successful: ${check.green}`, 143 | status: worstCheck(check), 144 | extra: `Added ${formatDistance(check.createdAt, new Date(), { 145 | addSuffix: true, 146 | })}`, 147 | description: `Checks performed ${check.healthchecks?.length} (${check.ignoreChecks?.length} checks ignored)`, 148 | }))} 149 | /> 150 | 151 | ); 152 | } 153 | -------------------------------------------------------------------------------- /src/app/account/repo/list/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getServerSession } from "next-auth/next"; 3 | import { formatDistance } from "date-fns"; 4 | 5 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 6 | import prisma from "@/models/db"; 7 | import List from "@/components/List"; 8 | import Title from "@/components/Title"; 9 | import { worstCheck } from "@/utils/checks"; 10 | import Stats from "@/components/Stats"; 11 | 12 | export default async function Page() { 13 | const session = await getServerSession(authOptions); 14 | if (!session) { 15 | redirect("/"); 16 | } 17 | 18 | const user = await prisma.user.findUnique({ 19 | where: { id: session.user.id }, 20 | include: { 21 | repositories: { 22 | include: { 23 | checks: { 24 | orderBy: { 25 | createdAt: "desc", 26 | }, 27 | take: 1, 28 | }, 29 | }, 30 | orderBy: { 31 | createdAt: "desc", 32 | }, 33 | }, 34 | }, 35 | }); 36 | 37 | const summary = Object.groupBy(user.repositories, (repo) => 38 | worstCheck(repo.checks[0]), 39 | ); 40 | 41 | return ( 42 | <> 43 | 44 | <Stats 45 | data={[ 46 | { 47 | name: "Success", 48 | stat: summary.success?.length || 0, 49 | status: "success", 50 | }, 51 | { 52 | name: "Warning", 53 | stat: summary.warning?.length || 0, 54 | status: "warning", 55 | }, 56 | { name: "Error", stat: summary.error?.length || 0, status: "error" }, 57 | ]} 58 | /> 59 | <List 60 | data={user.repositories.map((repo) => ({ 61 | id: repo.id, 62 | href: `/account/repo/checks/${repo.id}`, 63 | title: `${repo.owner} / ${repo.repo}`, 64 | status: repo.checks[0] ? worstCheck(repo.checks[0]) : "-", 65 | description: `Added ${formatDistance(repo.createdAt, new Date(), { 66 | addSuffix: true, 67 | })}`, 68 | extra: repo.checks[0] 69 | ? `Last check performed ${formatDistance( 70 | repo.checks[0].createdAt, 71 | new Date(), 72 | { 73 | addSuffix: true, 74 | }, 75 | )} with ${repo.checks[0].red} error(s), ${ 76 | repo.checks[0].amber 77 | } warning(s), ${repo.checks[0].green} success(es) (${repo.ignoreChecks.length} checks ignored)` 78 | : "No checks performed yet", 79 | }))} 80 | /> 81 | </> 82 | ); 83 | } 84 | -------------------------------------------------------------------------------- /src/app/account/repo/settings/[id]/action.js: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { redirect } from "next/navigation"; 4 | import { getServerSession } from "next-auth/next"; 5 | 6 | import config from "@/config/app.json"; 7 | import prisma from "@/models/db"; 8 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 9 | 10 | export async function saveSettings(prevState, formData) { 11 | // check authentication 12 | const session = await getServerSession(authOptions); 13 | if (!session) { 14 | throw new Error("Not authenticated"); 15 | } 16 | 17 | const id = formData.get("id"); 18 | const ignoreChecks = config.checks 19 | .map((option) => (formData.get(option) ? option : null)) 20 | .filter((item) => item); 21 | 22 | // they are owner of the repo 23 | const repository = await prisma.repository.findUnique({ 24 | where: { id, user: { id: session.user.id } }, 25 | include: { 26 | user: true, 27 | }, 28 | }); 29 | 30 | if (!repository) { 31 | throw new Error("Authentication failed"); 32 | } 33 | 34 | // update the repo with ignore checklist 35 | await prisma.repository.update({ 36 | where: { id }, 37 | data: { ignoreChecks }, 38 | }); 39 | 40 | redirect(`/account/repo/checks/${id}`); 41 | } 42 | -------------------------------------------------------------------------------- /src/app/account/repo/settings/[id]/form.js: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useFormState } from "react-dom"; 4 | import { useFlags } from "flagsmith/react"; 5 | 6 | import { saveSettings } from "./action"; 7 | import { SubmitButton } from "@/components/forms/SubmitButton"; 8 | import Input from "@/components/forms/Input"; 9 | import Checkbox from "@/components/forms/Checkbox"; 10 | import Select from "@/components/forms/Select"; 11 | 12 | const initialState = { 13 | ignore: undefined, 14 | success: undefined, 15 | errors: undefined, 16 | }; 17 | 18 | export default function Form({ id, ignore, schedule, disabled = false }) { 19 | let { optionalchecks } = useFlags(["optionalchecks"]); 20 | optionalchecks = JSON.parse(optionalchecks.value); 21 | const [state, formAction] = useFormState(saveSettings, initialState); 22 | 23 | return ( 24 | <form action={formAction}> 25 | <Input type="hidden" id="id" name="id" value={id} /> 26 | 27 | <div className="border-b border-gray-700 pb-12 pt-4"> 28 | <div className="border-b border-gray-900/10"> 29 | <p className="mt-1 text-sm leading-6 text-gray-300"> 30 | What checks do you wish to ignore from your report? 31 | </p> 32 | </div> 33 | 34 | <fieldset> 35 | <legend className="text-sm font-semibold leading-6"> 36 | Hide these checks 37 | </legend> 38 | {optionalchecks?.map((option) => ( 39 | <div className="mt-2" key={option.id}> 40 | <Checkbox 41 | id={option.id} 42 | name={option.id} 43 | text={option.name} 44 | value={true} 45 | disabled={disabled} 46 | defaultChecked={ignore.includes(option.id)} 47 | /> 48 | </div> 49 | ))} 50 | </fieldset> 51 | </div> 52 | 53 | <div className="border-b border-gray-700 pb-12 pt-4"> 54 | <fieldset className="mb-6"> 55 | <legend className="text-sm font-semibold leading-6">Automate</legend> 56 | <div className="mt-2"> 57 | <Checkbox 58 | id="schedule" 59 | name="schedule" 60 | text="Schedule" 61 | value={true} 62 | disabled={true} 63 | defaultChecked={true} 64 | /> 65 | </div> 66 | </fieldset> 67 | <fieldset> 68 | <legend className="text-sm font-semibold leading-6">Frequency</legend> 69 | <div className="mt-2"> 70 | <Select 71 | id="schedule" 72 | name="schedule" 73 | text="How often to perform these checks? (days)" 74 | options={[ 75 | { text: "1 day", value: 1 }, 76 | { text: "7 days", value: 7 }, 77 | { text: "30 days", value: 30 }, 78 | ]} 79 | value={7} 80 | disabled={true} 81 | // defaultChecked={schedule} 82 | classNameSelect="max-w-32" 83 | /> 84 | </div> 85 | </fieldset> 86 | </div> 87 | 88 | <div className="mt-6 flex items-center justify-end gap-x-6"> 89 | <SubmitButton text="SAVE" /> 90 | </div> 91 | </form> 92 | ); 93 | } 94 | -------------------------------------------------------------------------------- /src/app/account/repo/settings/[id]/page.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | import { getServerSession } from "next-auth/next"; 3 | 4 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 5 | import prisma from "@/models/db"; 6 | import Title from "@/components/Title"; 7 | import Button from "@/components/Button"; 8 | import Form from "./form"; 9 | 10 | export default async function Page({ params }) { 11 | const { id } = params; 12 | const session = await getServerSession(authOptions); 13 | if (!session) { 14 | redirect("/"); 15 | } 16 | 17 | const repository = await prisma.repository.findUnique({ 18 | where: { id, user: { id: session.user.id } }, 19 | include: { 20 | user: true, 21 | }, 22 | }); 23 | 24 | return ( 25 | <> 26 | <Title 27 | text={`Check list for the repo: ${repository.owner} / ${repository.repo}`} 28 | > 29 | <Button url={`/account/repo/checks/${id}`} type="secondary"> 30 | Repo Check List 31 | </Button> 32 | 33 |
34 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.js: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | import GithubProvider from "next-auth/providers/github"; 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; 4 | import { PrismaClient } from "@prisma/client"; 5 | 6 | const prisma = new PrismaClient(); 7 | 8 | const authOptions = { 9 | adapter: PrismaAdapter(prisma), 10 | providers: [ 11 | GithubProvider({ 12 | clientId: process.env.GITHUB_ID, 13 | clientSecret: process.env.GITHUB_SECRET, 14 | authorization: { 15 | params: { 16 | scope: "read:user user:email public_repo read:project", 17 | }, 18 | }, 19 | }), 20 | ], 21 | callbacks: { 22 | async session({ session, token, user }) { 23 | session.user.id = user.id; 24 | 25 | return session; 26 | }, 27 | async signIn({ user, account, profile, email, credentials }) { 28 | const lookup = await prisma.account.findUnique({ 29 | where: { 30 | provider_providerAccountId: { 31 | provider: "github", 32 | providerAccountId: account.providerAccountId, 33 | }, 34 | }, 35 | }); 36 | 37 | if (!lookup) { 38 | return true; 39 | } 40 | 41 | await prisma.account.update({ 42 | where: { 43 | provider_providerAccountId: { 44 | provider: "github", 45 | providerAccountId: account.providerAccountId, 46 | }, 47 | }, 48 | data: { access_token: account.access_token }, 49 | }); 50 | 51 | return true; 52 | }, 53 | }, 54 | }; 55 | 56 | const handler = NextAuth(authOptions); 57 | 58 | export { authOptions, handler as GET, handler as POST }; 59 | -------------------------------------------------------------------------------- /src/app/api/badges/report/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { makeBadge } from "badge-maker"; 2 | 3 | import prisma from "@/models/db"; 4 | import { worstCheck } from "@/utils/checks"; 5 | 6 | export const dynamic = "force-dynamic"; 7 | 8 | export async function GET(request, { params }) { 9 | const { id } = params; 10 | 11 | // get latest repo check 12 | const repository = await prisma.repository.findUnique({ 13 | where: { id }, 14 | include: { 15 | checks: { 16 | orderBy: { 17 | createdAt: "desc", 18 | }, 19 | take: 1, 20 | }, 21 | }, 22 | }); 23 | 24 | // default badge settings 25 | let config = { 26 | message: "No checks", 27 | label: "HealthCheck", 28 | style: "flat", 29 | }; 30 | 31 | if (repository?.checks[0]) { 32 | const check = repository.checks[0]; 33 | 34 | // if check report found 35 | config = { 36 | ...config, 37 | message: worstCheck( 38 | check, 39 | `Error (${check.red})`, 40 | `Warning (${check.amber})`, 41 | `Success (${check.green})`, 42 | ), 43 | color: worstCheck(check, "red", "orange", "green"), 44 | }; 45 | } else { 46 | // if check report not found 47 | config = { 48 | ...config, 49 | message: "Not found", 50 | color: "lightgrey", 51 | }; 52 | } 53 | 54 | let svg = ""; 55 | try { 56 | svg = makeBadge(config); 57 | } catch (e) { 58 | console.log(e); 59 | // TODO: return error badge 60 | } 61 | 62 | return new Response(svg, { 63 | headers: { 64 | "Content-Type": "image/svg+xml", 65 | }, 66 | }); 67 | } 68 | -------------------------------------------------------------------------------- /src/app/api/health/route.js: -------------------------------------------------------------------------------- 1 | export const dynamic = "force-dynamic"; 2 | 3 | export async function GET() { 4 | return Response.json({ status: "I am alive!" }); 5 | } 6 | -------------------------------------------------------------------------------- /src/app/api/report/latest/[id]/route.js: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation"; 2 | 3 | import prisma from "@/models/db"; 4 | 5 | export const dynamic = "force-dynamic"; 6 | 7 | export async function GET(request, { params }) { 8 | const { id } = params; 9 | 10 | const repository = await prisma.repository.findUnique({ 11 | where: { id }, 12 | include: { 13 | checks: { 14 | orderBy: { 15 | createdAt: "desc", 16 | }, 17 | take: 1, 18 | }, 19 | }, 20 | }); 21 | 22 | if (repository?.checks[0]) { 23 | const check = repository.checks[0]; 24 | redirect(`/repo/report/${check.id}`); 25 | } else { 26 | redirect("/repo/list"); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/api/system/checks/route.js: -------------------------------------------------------------------------------- 1 | import { differenceInHours } from "date-fns"; 2 | 3 | import prisma from "@/models/db"; 4 | import getAllRepoData from "@/utils/github"; 5 | import checks from "@/utils/checks"; 6 | 7 | export const dynamic = "force-dynamic"; 8 | 9 | export async function GET(request, { params }) { 10 | // protect for system calls only with valid token 11 | if (request.nextUrl.searchParams.get("token") !== process.env.API_TOKEN) { 12 | return Response.json({ error: "permission denied" }, { status: 401 }); 13 | } 14 | 15 | // get all repositories 16 | const repos = await prisma.repository.findMany({ 17 | include: { 18 | checks: { 19 | orderBy: { 20 | createdAt: "desc", 21 | }, 22 | take: 1, 23 | }, 24 | user: { 25 | include: { 26 | accounts: true, 27 | }, 28 | }, 29 | }, 30 | }); 31 | 32 | // filter out repositories that had checks in the last X hours 33 | let repoStatus = { ignore: [], run: [], error: [] }; 34 | repos.forEach((repo) => { 35 | try { 36 | if ( 37 | !repo.checks[0] || 38 | differenceInHours(new Date(), repo.checks[0].createdAt) >= 24 * 7 // TODO: move to Flagsmith 39 | ) { 40 | repoStatus.run.push({ 41 | id: repo.id, 42 | url: repo.url, 43 | token: repo.user.accounts[0].access_token, 44 | lastChecked: repo.checks[0].createdAt, 45 | }); 46 | } else { 47 | repoStatus.ignore.push({ 48 | id: repo.id, 49 | url: repo.url, 50 | lastChecked: repo.checks[0].createdAt, 51 | }); 52 | } 53 | } catch (e) { 54 | console.error(e); 55 | repoStatus.error.push({ 56 | id: repo.id, 57 | url: repo.url, 58 | }); 59 | } 60 | }); 61 | 62 | console.log("CHECKS IGNORED", repoStatus.ignore); 63 | console.log("CHECKS ERRORED", repoStatus.error); 64 | 65 | // perform checks on these repositories 66 | // use the owners github token (repository->user->account.access_token) 67 | let runs = { attempted: [], successful: [] }; 68 | repoStatus.run.map(async (repo) => { 69 | try { 70 | const responses = await getAllRepoData(repo.url, repo.token); 71 | 72 | // save github api data 73 | const githubResponseRepo = await prisma.githubResponse.create({ 74 | data: { 75 | repository: { 76 | connect: { 77 | id: repo.id, 78 | }, 79 | }, 80 | ...responses, 81 | }, 82 | }); 83 | 84 | // perform check 85 | const results = checks(githubResponseRepo); 86 | 87 | // save results 88 | await prisma.check.create({ 89 | data: { 90 | repository: { 91 | connect: { id: repo.id }, 92 | }, 93 | githubResponse: { 94 | connect: { id: githubResponseRepo.id }, 95 | }, 96 | red: results.summary.error?.length || 0, 97 | amber: results.summary.warning?.length || 0, 98 | green: results.summary.success?.length || 0, 99 | healthchecks: results.checks.map((check) => check.id), 100 | data: results.checks, 101 | allData: results.allChecks, 102 | ignoreChecks: results.ignoreChecks, 103 | }, 104 | }); 105 | 106 | runs.successful.push({ url: repo.url }); 107 | } catch (e) { 108 | console.error(e); 109 | runs.attempted.push({ url: repo.url }); 110 | } 111 | }); 112 | 113 | console.log("CHECKS PERFORMED", runs); 114 | 115 | return Response.json({ 116 | runs: runs.successful.length, 117 | failed: runs.attempted.length, 118 | ignores: repoStatus.ignore.length, 119 | errors: repoStatus.error.length, 120 | }); 121 | } 122 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EddieHubCommunity/HealthCheck/c92d0141ff1290dfaa326099281ef6bebbd9e736/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/layout.js: -------------------------------------------------------------------------------- 1 | import { getServerSession } from "next-auth/next"; 2 | import { Inter } from "next/font/google"; 3 | 4 | import "./globals.css"; 5 | import Providers from "./providers"; 6 | import Header from "@/components/Header"; 7 | import classNames from "@/utils/classNames"; 8 | import Banner from "@/components/Banner"; 9 | import Footer from "@/components/Footer"; 10 | import flagsmith from "@/config/flagsmith"; 11 | import { authOptions } from "@/app/api/auth/[...nextauth]/route"; 12 | 13 | const inter = Inter({ subsets: ["latin"] }); 14 | 15 | export const metadata = { 16 | title: "Open Source GitHub repo HealthCheck", 17 | description: "Is your Open Source GitHub repo friendly?", 18 | }; 19 | 20 | export default async function RootLayout({ children }) { 21 | const session = await getServerSession(authOptions); 22 | const flagsmithServerState = await flagsmith(session); 23 | 24 | return ( 25 | 26 | 27 | 33 | 34 |
35 |
36 | {children} 37 |
38 |