From 9595400266a27fface9f09ebefa481abb0a409ae Mon Sep 17 00:00:00 2001 From: Josh Goldberg Date: Thu, 21 Dec 2023 23:44:31 -0500 Subject: [PATCH] =?UTF-8?q?feat:=20initial=20getGitHubUsernameEmails=20?= =?UTF-8?q?=E2=9C=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 49 +++++- cspell.json | 1 + package.json | 5 + pnpm-lock.yaml | 294 +++++++++++++++++++++++++++++---- src/EmailNamesStore.test.ts | 65 ++++++++ src/EmailNamesStore.ts | 33 ++++ src/getAccountEmail.test.ts | 28 ++++ src/getAccountEmail.ts | 17 ++ src/getEventsEmails.test.ts | 90 ++++++++++ src/getEventsEmails.ts | 57 +++++++ src/getGitHubUsernameEmails.ts | 33 ++++ src/greet.test.ts | 44 ----- src/greet.ts | 13 -- src/index.ts | 4 +- src/isEventWithCommits.test.ts | 13 ++ src/isEventWithCommits.ts | 26 +++ src/options.ts | 13 ++ src/types.ts | 5 - 18 files changed, 696 insertions(+), 94 deletions(-) create mode 100644 src/EmailNamesStore.test.ts create mode 100644 src/EmailNamesStore.ts create mode 100644 src/getAccountEmail.test.ts create mode 100644 src/getAccountEmail.ts create mode 100644 src/getEventsEmails.test.ts create mode 100644 src/getEventsEmails.ts create mode 100644 src/getGitHubUsernameEmails.ts delete mode 100644 src/greet.test.ts delete mode 100644 src/greet.ts create mode 100644 src/isEventWithCommits.test.ts create mode 100644 src/isEventWithCommits.ts create mode 100644 src/options.ts delete mode 100644 src/types.ts diff --git a/README.md b/README.md index 91894217..4949bd51 100644 --- a/README.md +++ b/README.md @@ -23,11 +23,56 @@ npm i github-username-to-emails ``` ```ts -import { greet } from "github-username-to-emails"; +import { getGitHubUsernameEmails } from "github-username-to-emails"; -greet("Hello, world! 💖"); +await getGitHubUsernameEmails({ username: "joshuakgoldberg" }); + +/* +{ + account: 'github@joshuakgoldberg.com', + events: { 'git@joshuakgoldberg.com': [ 'Josh Goldberg ✨', 'Josh Goldberg' ] } +} +*/ ``` +Calling `getGitHubUsernameEmails` will try to find the user's email from two public data points: + +- [`/users/${username}`](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-a-user): public account information +- [`/users/{username}/events`](https://docs.github.com/en/rest/activity/events?apiVersion=2022-11-28#list-public-events-for-a-user): + +Note that `account` might be `undefined` and `events` might be `{}`. +Only publicly visible emails can be retrieved. + +### Options + +`auth` must be provided as an option or via `process.env.GH_TOKEN`. + +| Option | Type | Description | Default | +| -------------- | -------- | ---------------------------------- | ---------------------- | +| `auth` | `string` | Auth token for Octokit REST calls. | `process.env.GH_TOKEN` | +| `historyLimit` | `number` | How many public events to look at. | `500` | +| `username` | `string` | GitHub user to check emails of. | | + +```ts +await getGitHubUsernameEmails({ + auth: "gho_abc123", + historyLimit: 9001, + username: "joshuakgoldberg", +}); +``` + +## Email Privacy + +This package doesn't expose any data users aren't already providing to GitHub. +You can manually check same data from: + +1. A user's public GitHub profile +2. `https://api.github.com/users//events` + +This package only serves as a convenience to same time searching through that data. + +To hide your email from public view, see [GitHub's _Setting your commit email address_ docs](https://docs.github.com/en/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-email-preferences/setting-your-commit-email-address). + ## Contributors diff --git a/cspell.json b/cspell.json index 494dd2ec..a8d218e7 100644 --- a/cspell.json +++ b/cspell.json @@ -16,6 +16,7 @@ "conventionalcommits", "knip", "lcov", + "joshuakgoldberg", "markdownlintignore", "npmpackagejsonlintrc", "outro", diff --git a/package.json b/package.json index b3a697ba..e7916eb4 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ }, "license": "MIT", "author": { + "name": "Josh Goldberg", "email": "npm@joshuakgoldberg.com" }, "type": "module", @@ -35,9 +36,13 @@ "lint-staged": { "*": "prettier --ignore-unknown --write" }, + "dependencies": { + "octokit": "^3.1.2" + }, "devDependencies": { "@release-it/conventional-changelog": "^8.0.1", "@types/eslint": "^8.56.0", + "@types/node": "^20.10.5", "@typescript-eslint/eslint-plugin": "^6.15.0", "@typescript-eslint/parser": "^6.15.0", "@vitest/coverage-v8": "^1.1.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5245eb56..6ac45b62 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,11 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +dependencies: + octokit: + specifier: ^3.1.2 + version: 3.1.2 + devDependencies: '@release-it/conventional-changelog': specifier: ^8.0.1 @@ -11,6 +16,9 @@ devDependencies: '@types/eslint': specifier: ^8.56.0 version: 8.56.0 + '@types/node': + specifier: ^20.10.5 + version: 20.10.5 '@typescript-eslint/eslint-plugin': specifier: ^6.15.0 version: 6.15.0(@typescript-eslint/parser@6.15.0)(eslint@8.56.0)(typescript@5.3.3) @@ -979,10 +987,80 @@ packages: which: 4.0.0 dev: true + /@octokit/app@14.0.2: + resolution: {integrity: sha512-NCSCktSx+XmjuSUVn2dLfqQ9WIYePGP95SDJs4I9cn/0ZkeXcPkaoCLl64Us3dRKL2ozC7hArwze5Eu+/qt1tg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-app': 6.0.2 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.0.2 + '@octokit/oauth-app': 6.0.0 + '@octokit/plugin-paginate-rest': 9.1.5(@octokit/core@5.0.2) + '@octokit/types': 12.4.0 + '@octokit/webhooks': 12.0.10 + dev: false + + /@octokit/auth-app@6.0.2: + resolution: {integrity: sha512-HYuRX3Fvhs2y9i7a4F8f+A5HWfacRWmpERHGBEOtgvKVjJkOQZKUY2v6HiSszYecHAF8Ojqngp2iraSP3SvNpQ==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.0.1 + '@octokit/auth-oauth-user': 4.0.1 + '@octokit/request': 8.1.6 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + deprecation: 2.3.1 + lru-cache: 10.1.0 + universal-github-app-jwt: 1.1.1 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-app@7.0.1: + resolution: {integrity: sha512-RE0KK0DCjCHXHlQBoubwlLijXEKfhMhKm9gO56xYvFmP1QTMb+vvwRPmQLLx0V+5AvV9N9I3lr1WyTzwL3rMDg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.0.1 + '@octokit/auth-oauth-user': 4.0.1 + '@octokit/request': 8.1.6 + '@octokit/types': 12.4.0 + '@types/btoa-lite': 1.0.2 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-device@6.0.1: + resolution: {integrity: sha512-yxU0rkL65QkjbqQedgVx3gmW7YM5fF+r5uaSj9tM/cQGVqloXcqP2xK90eTyYvl29arFVCW8Vz4H/t47mL0ELw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-methods': 4.0.1 + '@octokit/request': 8.1.6 + '@octokit/types': 12.4.0 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/auth-oauth-user@4.0.1: + resolution: {integrity: sha512-N94wWW09d0hleCnrO5wt5MxekatqEJ4zf+1vSe8MKMrhZ7gAXKFOKrDEZW2INltvBWJCyDUELgGRv8gfErH1Iw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-device': 6.0.1 + '@octokit/oauth-methods': 4.0.1 + '@octokit/request': 8.1.6 + '@octokit/types': 12.4.0 + btoa-lite: 1.0.0 + universal-user-agent: 6.0.1 + dev: false + /@octokit/auth-token@4.0.0: resolution: {integrity: sha512-tY/msAuJo6ARbK6SPIxZrPBms3xPbfwBrulZe0Wtr/DIY9lje2HeV1uoebShn6mx7SjCHif6EjMvoREj+gZ+SA==} engines: {node: '>= 18'} - dev: true + + /@octokit/auth-unauthenticated@5.0.1: + resolution: {integrity: sha512-oxeWzmBFxWd+XolxKTc4zr+h3mt+yofn4r7OfoIkR/Cj/o70eEGmPsFbueyJE2iBAGpjgTnEOKM3pnuEGVmiqg==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + dev: false /@octokit/core@5.0.2: resolution: {integrity: sha512-cZUy1gUvd4vttMic7C0lwPed8IYXWYp8kHIMatyhY8t8n3Cpw2ILczkV5pGMPqef7v0bLo0pOHrEHarsau2Ydg==} @@ -995,7 +1073,6 @@ packages: '@octokit/types': 12.4.0 before-after-hook: 2.2.3 universal-user-agent: 6.0.1 - dev: true /@octokit/endpoint@9.0.4: resolution: {integrity: sha512-DWPLtr1Kz3tv8L0UvXTDP1fNwM0S+z6EJpRcvH66orY6Eld4XBMCSYsaWp4xIm61jTWxK68BrR7ibO+vSDnZqw==} @@ -1003,7 +1080,6 @@ packages: dependencies: '@octokit/types': 12.4.0 universal-user-agent: 6.0.1 - dev: true /@octokit/graphql@7.0.2: resolution: {integrity: sha512-OJ2iGMtj5Tg3s6RaXH22cJcxXRi7Y3EBqbHTBRq+PQAqfaS8f/236fUrWhfSn8P4jovyzqucxme7/vWSSZBX2Q==} @@ -1012,11 +1088,48 @@ packages: '@octokit/request': 8.1.6 '@octokit/types': 12.4.0 universal-user-agent: 6.0.1 - dev: true + + /@octokit/oauth-app@6.0.0: + resolution: {integrity: sha512-bNMkS+vJ6oz2hCyraT9ZfTpAQ8dZNqJJQVNaKjPLx4ue5RZiFdU1YWXguOPR8AaSHS+lKe+lR3abn2siGd+zow==} + engines: {node: '>= 18'} + dependencies: + '@octokit/auth-oauth-app': 7.0.1 + '@octokit/auth-oauth-user': 4.0.1 + '@octokit/auth-unauthenticated': 5.0.1 + '@octokit/core': 5.0.2 + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/oauth-methods': 4.0.1 + '@types/aws-lambda': 8.10.130 + universal-user-agent: 6.0.1 + dev: false + + /@octokit/oauth-authorization-url@6.0.2: + resolution: {integrity: sha512-CdoJukjXXxqLNK4y/VOiVzQVjibqoj/xHgInekviUJV73y/BSIcwvJ/4aNHPBPKcPWFnd4/lO9uqRV65jXhcLA==} + engines: {node: '>= 18'} + dev: false + + /@octokit/oauth-methods@4.0.1: + resolution: {integrity: sha512-1NdTGCoBHyD6J0n2WGXg9+yDLZrRNZ0moTEex/LSPr49m530WNKcCfXDghofYptr3st3eTii+EHoG5k/o+vbtw==} + engines: {node: '>= 18'} + dependencies: + '@octokit/oauth-authorization-url': 6.0.2 + '@octokit/request': 8.1.6 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + btoa-lite: 1.0.0 + dev: false /@octokit/openapi-types@19.1.0: resolution: {integrity: sha512-6G+ywGClliGQwRsjvqVYpklIfa7oRPA0vyhPQG/1Feh+B+wU0vGH1JiJ5T25d3g1JZYBHzR2qefLi9x8Gt+cpw==} - dev: true + + /@octokit/plugin-paginate-graphql@4.0.0(@octokit/core@5.0.2): + resolution: {integrity: sha512-7HcYW5tP7/Z6AETAPU14gp5H5KmCPT3hmJrS/5tO7HIgbwenYmgw4OY9Ma54FDySuxMwD+wsJlxtuGWwuZuItA==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.0.2 + dev: false /@octokit/plugin-paginate-rest@9.1.5(@octokit/core@5.0.2): resolution: {integrity: sha512-WKTQXxK+bu49qzwv4qKbMMRXej1DU2gq017euWyKVudA6MldaSSQuxtz+vGbhxV4CjxpUxjZu6rM2wfc1FiWVg==} @@ -1026,7 +1139,6 @@ packages: dependencies: '@octokit/core': 5.0.2 '@octokit/types': 12.4.0 - dev: true /@octokit/plugin-request-log@4.0.0(@octokit/core@5.0.2): resolution: {integrity: sha512-2uJI1COtYCq8Z4yNSnM231TgH50bRkheQ9+aH8TnZanB6QilOnx8RMD2qsnamSOXtDj0ilxvevf5fGsBhBBzKA==} @@ -1045,7 +1157,29 @@ packages: dependencies: '@octokit/core': 5.0.2 '@octokit/types': 12.4.0 - dev: true + + /@octokit/plugin-retry@6.0.1(@octokit/core@5.0.2): + resolution: {integrity: sha512-SKs+Tz9oj0g4p28qkZwl/topGcb0k0qPNX/i7vBKmDsjoeqnVfFUquqrE/O9oJY7+oLzdCtkiWSXLpLjvl6uog==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': '>=5' + dependencies: + '@octokit/core': 5.0.2 + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + bottleneck: 2.19.5 + dev: false + + /@octokit/plugin-throttling@8.1.3(@octokit/core@5.0.2): + resolution: {integrity: sha512-pfyqaqpc0EXh5Cn4HX9lWYsZ4gGbjnSmUILeu4u2gnuM50K/wIk9s1Pxt3lVeVwekmITgN/nJdoh43Ka+vye8A==} + engines: {node: '>= 18'} + peerDependencies: + '@octokit/core': ^5.0.0 + dependencies: + '@octokit/core': 5.0.2 + '@octokit/types': 12.4.0 + bottleneck: 2.19.5 + dev: false /@octokit/request-error@5.0.1: resolution: {integrity: sha512-X7pnyTMV7MgtGmiXBwmO6M5kIPrntOXdyKZLigNfQWSEQzVxR4a4vo49vJjTWX70mPndj8KhfT4Dx+2Ng3vnBQ==} @@ -1054,7 +1188,6 @@ packages: '@octokit/types': 12.4.0 deprecation: 2.3.1 once: 1.4.0 - dev: true /@octokit/request@8.1.6: resolution: {integrity: sha512-YhPaGml3ncZC1NfXpP3WZ7iliL1ap6tLkAp6MvbK2fTTPytzVUyUesBBogcdMm86uRYO5rHaM1xIWxigWZ17MQ==} @@ -1064,7 +1197,6 @@ packages: '@octokit/request-error': 5.0.1 '@octokit/types': 12.4.0 universal-user-agent: 6.0.1 - dev: true /@octokit/rest@20.0.2: resolution: {integrity: sha512-Ux8NDgEraQ/DMAU1PlAohyfBBXDwhnX2j33Z1nJNziqAfHi70PuxkFYIcIt8aIAxtRE7KVuKp8lSR8pA0J5iOQ==} @@ -1080,7 +1212,25 @@ packages: resolution: {integrity: sha512-FLWs/AvZllw/AGVs+nJ+ELCDZZJk+kY0zMen118xhL2zD0s1etIUHm1odgjP7epxYU1ln7SZxEUWYop5bhsdgQ==} dependencies: '@octokit/openapi-types': 19.1.0 - dev: true + + /@octokit/webhooks-methods@4.0.0: + resolution: {integrity: sha512-M8mwmTXp+VeolOS/kfRvsDdW+IO0qJ8kYodM/sAysk093q6ApgmBXwK1ZlUvAwXVrp/YVHp6aArj4auAxUAOFw==} + engines: {node: '>= 18'} + dev: false + + /@octokit/webhooks-types@7.1.0: + resolution: {integrity: sha512-y92CpG4kFFtBBjni8LHoV12IegJ+KFxLgKRengrVjKmGE5XMeCuGvlfRe75lTRrgXaG6XIWJlFpIDTlkoJsU8w==} + dev: false + + /@octokit/webhooks@12.0.10: + resolution: {integrity: sha512-Q8d26l7gZ3L1SSr25NFbbP0B431sovU5r0tIqcvy8Z4PrD1LBv0cJEjvDLOieouzPSTzSzufzRIeXD7S+zAESA==} + engines: {node: '>= 18'} + dependencies: + '@octokit/request-error': 5.0.1 + '@octokit/webhooks-methods': 4.0.0 + '@octokit/webhooks-types': 7.1.0 + aggregate-error: 3.1.0 + dev: false /@pkgjs/parseargs@0.11.0: resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} @@ -1396,6 +1546,14 @@ packages: resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} dev: true + /@types/aws-lambda@8.10.130: + resolution: {integrity: sha512-HxTfLeGvD1wTJqIGwcBCpNmHKenja+We1e0cuzeIDFfbEj3ixnlTInyPR/81zAe0Ss/Ip12rFK6XNeMLVucOSg==} + dev: false + + /@types/btoa-lite@1.0.2: + resolution: {integrity: sha512-ZYbcE2x7yrvNFJiU7xJGrpF/ihpkM7zKgw8bha3LNJSesvTtUNxbpzaT7WXBIryf6jovisrxTBvymxMeLLj1Mg==} + dev: false + /@types/eslint@8.56.0: resolution: {integrity: sha512-FlsN0p4FhuYRjIxpbdXovvHQhtlG05O1GG/RNWvdAxTboR438IOTwmrY/vLA+Xfgg06BTkP045M3vpFwTMv1dg==} dependencies: @@ -1419,6 +1577,12 @@ packages: resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} dev: true + /@types/jsonwebtoken@9.0.5: + resolution: {integrity: sha512-VRLSGzik+Unrup6BsouBeHsf4d1hOEgYWTm/7Nmw1sXoN1+tRly/Gy/po3yeahnP4jfnQWWAhQAqcNfH7ngOkA==} + dependencies: + '@types/node': 20.10.5 + dev: false + /@types/mdast@3.0.15: resolution: {integrity: sha512-LnwD+mUEfxWMa1QpDraczIn6k0Ee3SMicuYSSzS6ZYl2gKS09EClnJYGd8Du6rfc5r/GZEk5o1mRb8TaTj03sQ==} dependencies: @@ -1433,7 +1597,6 @@ packages: resolution: {integrity: sha512-nNPsNE65wjMxEKI93yOP+NPGGBJz/PoN3kZsVLee0XMiJolxSekEVD8wRwBUBqkwc7UWop0edW50yrCQW4CyRw==} dependencies: undici-types: 5.26.5 - dev: true /@types/normalize-package-data@2.4.4: resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1701,7 +1864,6 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true /ajv-errors@1.0.1(ajv@6.12.6): resolution: {integrity: sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==} @@ -1903,7 +2065,6 @@ packages: /before-after-hook@2.2.3: resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==} - dev: true /big-integer@1.6.52: resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} @@ -1938,6 +2099,10 @@ packages: individual: 3.0.0 dev: true + /bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + dev: false + /boxen@7.1.1: resolution: {integrity: sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==} engines: {node: '>=14.16'} @@ -1979,6 +2144,14 @@ packages: fill-range: 7.0.1 dev: true + /btoa-lite@1.0.0: + resolution: {integrity: sha512-gvW7InbIyF8AicrqWoptdW08pUxuhq8BEgowNajy9RhiE86fmGAGl+bLKo6oB8QP0CkqHLowfN0oJdKC/J6LbA==} + dev: false + + /buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + dev: false + /buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true @@ -2167,7 +2340,6 @@ packages: /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true /clear-module@4.1.2: resolution: {integrity: sha512-LWAxzHqdHsAZlPlEyJ2Poz6AIs384mPeqLVCru2p0BrP9G/kVGuhNyZYClLO6cXlnuJjzC8xtsJIuMjKqLXoAw==} @@ -2745,7 +2917,6 @@ packages: /deprecation@2.3.1: resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} - dev: true /detect-indent@7.0.1: resolution: {integrity: sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g==} @@ -2802,6 +2973,12 @@ packages: wcwidth: 1.0.1 dev: true + /ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + dependencies: + safe-buffer: 5.2.1 + dev: false + /emoji-regex@10.3.0: resolution: {integrity: sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==} dev: true @@ -4007,7 +4184,6 @@ packages: /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - dev: true /individual@3.0.0: resolution: {integrity: sha512-rUY5vtT748NMRbEMrTNiFfy29BgGZwGXUi2NFUVMWQrogSLzlJvQV9eeMWi+g1aVaQ53tpyLAQtd5x/JH0Nh1g==} @@ -4573,6 +4749,37 @@ packages: engines: {'0': node >= 0.2.0} dev: true + /jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.2 + semver: 7.5.4 + dev: false + + /jwa@1.4.1: + resolution: {integrity: sha512-qiLX/xhEEFKUAJ6FiBMbes3w9ATzyk5W7Hvzpa/SLYdxNtng+gcurvrI7TbACjIXlsJyr05/S1oUhZrc63evQA==} + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + dev: false + + /jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + dependencies: + jwa: 1.4.1 + safe-buffer: 5.2.1 + dev: false + /keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} dependencies: @@ -4747,18 +4954,36 @@ packages: resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} dev: true + /lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + dev: false + + /lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + dev: false + + /lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + dev: false + + /lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + dev: false + /lodash.isplainobject@4.0.6: resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} - dev: true /lodash.isstring@4.0.1: resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} - dev: true /lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} dev: true + /lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + dev: false + /lodash.sortby@4.7.0: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true @@ -4812,14 +5037,12 @@ packages: /lru-cache@10.1.0: resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==} engines: {node: 14 || >=16.14} - dev: true /lru-cache@6.0.0: resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} engines: {node: '>=10'} dependencies: yallist: 4.0.0 - dev: true /lru-cache@7.18.3: resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} @@ -5108,7 +5331,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /mute-stream@1.0.0: resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} @@ -5343,11 +5565,26 @@ packages: object-keys: 1.1.1 dev: true + /octokit@3.1.2: + resolution: {integrity: sha512-MG5qmrTL5y8KYwFgE1A4JWmgfQBaIETE/lOlfwNYx1QOtCQHGVxkRJmdUJltFc1HVn73d61TlMhMyNTOtMl+ng==} + engines: {node: '>= 18'} + dependencies: + '@octokit/app': 14.0.2 + '@octokit/core': 5.0.2 + '@octokit/oauth-app': 6.0.0 + '@octokit/plugin-paginate-graphql': 4.0.0(@octokit/core@5.0.2) + '@octokit/plugin-paginate-rest': 9.1.5(@octokit/core@5.0.2) + '@octokit/plugin-rest-endpoint-methods': 10.2.0(@octokit/core@5.0.2) + '@octokit/plugin-retry': 6.0.1(@octokit/core@5.0.2) + '@octokit/plugin-throttling': 8.1.3(@octokit/core@5.0.2) + '@octokit/request-error': 5.0.1 + '@octokit/types': 12.4.0 + dev: false + /once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} dependencies: wrappy: 1.0.2 - dev: true /onetime@5.1.2: resolution: {integrity: sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==} @@ -6209,7 +6446,6 @@ packages: /safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: true /safe-regex-test@1.0.0: resolution: {integrity: sha512-JBUUzyOgEwXQY1NuPtvcj/qcBDbDmEvWufhlnXZIm75DEHp+afM1r1ujJpJsV/gSM4t59tpDyPi1sd6ZaPFfsA==} @@ -6255,7 +6491,6 @@ packages: hasBin: true dependencies: lru-cache: 6.0.0 - dev: true /sentences-per-line@0.2.1: resolution: {integrity: sha512-6hlyKBwqoaZJ5+RBTKNNem2kBGAboh9e9KfFw5KYKA+64xaTYWbv5C6XnOudx8xk1Sg6f/4yalhJtCZFSLWIsQ==} @@ -6997,7 +7232,6 @@ packages: /undici-types@5.26.5: resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} - dev: true /unescape-js@1.1.4: resolution: {integrity: sha512-42SD8NOQEhdYntEiUQdYq/1V/YHwr1HLwlHuTJB5InVVdOSbgI6xu8jK5q65yIzuFCfczzyDF/7hbGzVbyCw0g==} @@ -7030,9 +7264,15 @@ packages: '@types/unist': 2.0.10 dev: true + /universal-github-app-jwt@1.1.1: + resolution: {integrity: sha512-G33RTLrIBMFmlDV4u4CBF7dh71eWwykck4XgaxaIVeZKOYZRAAxvcGMRFTUclVY6xoUPQvO4Ne5wKGxYm/Yy9w==} + dependencies: + '@types/jsonwebtoken': 9.0.5 + jsonwebtoken: 9.0.2 + dev: false + /universal-user-agent@6.0.1: resolution: {integrity: sha512-yCzhz6FN2wU1NiiQRogkTQszlQSlpWaw8SvVegAc+bDxbzHgh1vX8uIe8OYyMH6DwH+sdTJsgMl36+mSMdRJIQ==} - dev: true /universalify@0.1.2: resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} @@ -7374,7 +7614,6 @@ packages: /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true /write-file-atomic@3.0.3: resolution: {integrity: sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==} @@ -7392,7 +7631,6 @@ packages: /yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true /yaml-eslint-parser@1.2.2: resolution: {integrity: sha512-pEwzfsKbTrB8G3xc/sN7aw1v6A6c/pKxLAkjclnAyo5g5qOh6eL9WGu0o3cSDQZKrTNk4KL4lQSwZW+nBkANEg==} diff --git a/src/EmailNamesStore.test.ts b/src/EmailNamesStore.test.ts new file mode 100644 index 00000000..501bb453 --- /dev/null +++ b/src/EmailNamesStore.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; + +import { EmailNamesStore } from "./EmailNamesStore.js"; + +describe("EmailNamesStore", () => { + it("produces no entries when no emails were added", () => { + const store = new EmailNamesStore(); + + const actual = store.toEntries(); + + expect(actual).toEqual({}); + }); + + it("stores emails by name when emails are added with names", () => { + const store = new EmailNamesStore(); + const email1 = "abc-1@test.com"; + const email2 = "abc-2@test.com"; + const name1 = "Abc 123 1"; + const name2 = "Abc 123 2"; + + store.add(email1, name1); + store.add(email2, name2); + + const actual = store.toEntries(); + + expect(actual).toEqual({ [email1]: [name1], [email2]: [name2] }); + }); + + it("deduplicates emails by name when emails are added with name multiple times", () => { + const store = new EmailNamesStore(); + const email = "abc@test.com"; + const name = "Abc 123"; + + store.add(email, name); + store.add(email, name); + + const actual = store.toEntries(); + + expect(actual).toEqual({ [email]: [name] }); + }); + + it("stores emails without name when emails are added without name", () => { + const store = new EmailNamesStore(); + const email = "abc@test.com"; + + store.add(email, undefined); + + const actual = store.toEntries(); + + expect(actual).toEqual({ [email]: [] }); + }); + + it("deduplicates emails by name when emails are added with and without name", () => { + const store = new EmailNamesStore(); + const email = "abc@test.com"; + const name = "Abc 123"; + + store.add(email, undefined); + store.add(email, name); + + const actual = store.toEntries(); + + expect(actual).toEqual({ [email]: [name] }); + }); +}); diff --git a/src/EmailNamesStore.ts b/src/EmailNamesStore.ts new file mode 100644 index 00000000..373eceb2 --- /dev/null +++ b/src/EmailNamesStore.ts @@ -0,0 +1,33 @@ +export class EmailNamesStore { + #emailNames = new Map>(); + + #getNames(email: string) { + const existing = this.#emailNames.get(email); + if (existing) { + return existing; + } + + const created = new Set(); + + this.#emailNames.set(email, created); + + return created; + } + + add(email: string, name: string | undefined) { + const names = this.#getNames(email); + + if (name) { + names.add(name); + } + } + + toEntries() { + return Object.fromEntries( + Array.from(this.#emailNames).map(([key, value]) => [ + key, + Array.from(value), + ]), + ); + } +} diff --git a/src/getAccountEmail.test.ts b/src/getAccountEmail.test.ts new file mode 100644 index 00000000..8ef1da03 --- /dev/null +++ b/src/getAccountEmail.test.ts @@ -0,0 +1,28 @@ +import { Octokit } from "octokit"; +import { MockInstance, describe, expect, it, vi } from "vitest"; + +import { getAccountEmail } from "./getAccountEmail.js"; + +const createMockOctokit = (request: MockInstance) => + ({ request }) as unknown as Octokit; + +const options = { historyLimit: 9001, username: "abc123" }; + +describe("getAccountEmail", () => { + it("returns no email when the GitHub api returns no email", async () => { + const request = vi.fn().mockResolvedValue({ data: {} }); + + const actual = await getAccountEmail(createMockOctokit(request), options); + + expect(actual).toBeUndefined(); + }); + + it("returns an email when the GitHub api returns an email", async () => { + const email = "abc123@test.com"; + const request = vi.fn().mockResolvedValue({ data: { email } }); + + const actual = await getAccountEmail(createMockOctokit(request), options); + + expect(actual).toBe(email); + }); +}); diff --git a/src/getAccountEmail.ts b/src/getAccountEmail.ts new file mode 100644 index 00000000..4a23d259 --- /dev/null +++ b/src/getAccountEmail.ts @@ -0,0 +1,17 @@ +import { Octokit } from "octokit"; + +import { FilledOutOptions } from "./options.js"; + +export async function getAccountEmail( + octokit: Octokit, + { username }: Pick, +) { + const { data } = await octokit.request("GET /users/{username}", { + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + username, + }); + + return data.email ?? undefined; +} diff --git a/src/getEventsEmails.test.ts b/src/getEventsEmails.test.ts new file mode 100644 index 00000000..849f924a --- /dev/null +++ b/src/getEventsEmails.test.ts @@ -0,0 +1,90 @@ +import { Octokit } from "octokit"; +import { MockInstance, describe, expect, it, vi } from "vitest"; + +import { getEventsEmails } from "./getEventsEmails.js"; + +const createMockOctokit = (request: MockInstance) => + ({ request }) as unknown as Octokit; + +const options = { historyLimit: 9001, username: "abc123" }; + +describe("getEventsEmails", () => { + it("returns no emails when the GitHub api returns no events", async () => { + const request = vi.fn().mockResolvedValue({ data: [] }); + + const actual = await getEventsEmails(createMockOctokit(request), options); + + expect(actual).toEqual({}); + }); + + it("returns no emails when the GitHub api returns no events with emails", async () => { + const request = vi.fn().mockResolvedValue({ + data: [ + { + payload: { + commits: [{ author: {} }], + }, + }, + ], + }); + + const actual = await getEventsEmails(createMockOctokit(request), options); + + expect(actual).toEqual({}); + }); + + it("returns deduplicated emails when the GitHub api returns events with emails", async () => { + const email1 = "email-1@test.com"; + const email2 = "email-2@test.com"; + const name = "Abc 123"; + + const request = vi.fn().mockResolvedValue({ + data: [ + { + payload: { + commits: [{ author: { email: email1, name } }], + }, + }, + { + payload: { + commits: [ + { author: { email: email2, name } }, + { author: { email: email1, name } }, + ], + }, + }, + ], + }); + + const actual = await getEventsEmails(createMockOctokit(request), options); + + expect(actual).toEqual({ [email1]: [name], [email2]: [name] }); + }); + + it("ignores an email when it's an auto-generated noreply email", async () => { + const email = "email@test.com"; + const name = "Abc 123"; + + const request = vi.fn().mockResolvedValue({ + data: [ + { + payload: { + commits: [{ author: { email, name } }], + }, + }, + { + payload: { + commits: [ + { author: { email: "test@users.noreply.github.com", name } }, + { author: { email, name } }, + ], + }, + }, + ], + }); + + const actual = await getEventsEmails(createMockOctokit(request), options); + + expect(actual).toEqual({ [email]: [name] }); + }); +}); diff --git a/src/getEventsEmails.ts b/src/getEventsEmails.ts new file mode 100644 index 00000000..f543c33a --- /dev/null +++ b/src/getEventsEmails.ts @@ -0,0 +1,57 @@ +import { Octokit } from "octokit"; + +import { EmailNamesStore } from "./EmailNamesStore.js"; +import { isEventWithCommits } from "./isEventWithCommits.js"; +import { FilledOutOptions } from "./options.js"; + +export async function getEventsEmails( + octokit: Octokit, + { + historyLimit, + username, + }: Pick, +) { + const emailNames = new EmailNamesStore(); + + // GitHub defaults to 30 results per page, and caps it at 100 + const resultsPerPage = Math.min(historyLimit, 100); + let totalResults = 0; + let page = 0; + + // TODO: Instead of paginating in series, look into using a parallel queue? + while (totalResults < historyLimit) { + const { data: events } = await octokit.request( + "GET /users/{username}/events/public", + { + headers: { + "X-GitHub-Api-Version": "2022-11-28", + }, + page, + per_page: resultsPerPage, + username, + }, + ); + + for (const event of events) { + if (isEventWithCommits(event)) { + for (const commit of event.payload.commits) { + if ( + commit.author.email && + !commit.author.email.endsWith("@users.noreply.github.com") + ) { + emailNames.add(commit.author.email, commit.author.name); + } + } + } + } + + if (events.length < resultsPerPage) { + break; + } + + totalResults += events.length; + page += 1; + } + + return emailNames.toEntries(); +} diff --git a/src/getGitHubUsernameEmails.ts b/src/getGitHubUsernameEmails.ts new file mode 100644 index 00000000..3dbe593b --- /dev/null +++ b/src/getGitHubUsernameEmails.ts @@ -0,0 +1,33 @@ +import { Octokit } from "octokit"; + +import { getAccountEmail } from "./getAccountEmail.js"; +import { getEventsEmails } from "./getEventsEmails.js"; +import { GitHubUsernameEmailsOptions, defaultOptions } from "./options.js"; + +/** + * For any number of emails, the names found under their commits. + */ +export type EmailsToNames = Record; + +export interface GitHubUsernameEmails { + account: string | undefined; + events: EmailsToNames; +} + +export async function getGitHubUsernameEmails({ + auth, + ...rawOptions +}: GitHubUsernameEmailsOptions): Promise { + auth ??= process.env.GH_TOKEN; + if (!auth) { + throw new Error(`Please provide an auth token (process.env.GH_TOKEN).`); + } + + const octokit = new Octokit({ auth }); + const options = { ...defaultOptions, ...rawOptions }; + + return { + account: await getAccountEmail(octokit, options), + events: await getEventsEmails(octokit, options), + }; +} diff --git a/src/greet.test.ts b/src/greet.test.ts deleted file mode 100644 index f729115f..00000000 --- a/src/greet.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { describe, expect, it, vi } from "vitest"; - -import { greet } from "./greet.js"; - -const message = "Yay, testing!"; - -describe("greet", () => { - it("logs to the console once when message is provided as a string", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet(message); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs to the console once when message is provided as an object", () => { - const logger = vi.spyOn(console, "log").mockImplementation(() => undefined); - - greet({ message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs once when times is not provided in an object", () => { - const logger = vi.fn(); - - greet({ logger, message }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(1); - }); - - it("logs a specified number of times when times is provided", () => { - const logger = vi.fn(); - const times = 7; - - greet({ logger, message, times }); - - expect(logger).toHaveBeenCalledWith(message); - expect(logger).toHaveBeenCalledTimes(7); - }); -}); diff --git a/src/greet.ts b/src/greet.ts deleted file mode 100644 index a0d3b4c6..00000000 --- a/src/greet.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { GreetOptions } from "./types.js"; - -export function greet(options: GreetOptions | string) { - const { - logger = console.log.bind(console), - message, - times = 1, - } = typeof options === "string" ? { message: options } : options; - - for (let i = 0; i < times; i += 1) { - logger(message); - } -} diff --git a/src/index.ts b/src/index.ts index a39b40fa..082b14d0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ -export * from "./greet.js"; -export * from "./types.js"; +export * from "./options.js"; +export * from "./getGitHubUsernameEmails.js"; diff --git a/src/isEventWithCommits.test.ts b/src/isEventWithCommits.test.ts new file mode 100644 index 00000000..3a94c27a --- /dev/null +++ b/src/isEventWithCommits.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, test } from "vitest"; + +import { isEventWithCommits } from "./isEventWithCommits.js"; + +describe("isEventWithCommits", () => { + test.each([ + [{ payload: {} }, false], + [{ payload: { commits: {} } }, false], + [{ payload: { commits: [] } }, true], + ] as const)("%s", (input, expected) => { + expect(isEventWithCommits(input)).toBe(expected); + }); +}); diff --git a/src/isEventWithCommits.ts b/src/isEventWithCommits.ts new file mode 100644 index 00000000..64931bee --- /dev/null +++ b/src/isEventWithCommits.ts @@ -0,0 +1,26 @@ +export interface CommitAuthor { + email?: string; + name?: string; +} + +export interface EventCommit { + author: CommitAuthor; +} + +export interface PayloadWithCommits { + commits: EventCommit[]; +} + +export interface EventWithCommits { + payload: PayloadWithCommits; +} + +export function isEventWithCommits(event: { + payload: object; +}): event is EventWithCommits { + return ( + "payload" in event && + "commits" in event.payload && + Array.isArray(event.payload.commits) + ); +} diff --git a/src/options.ts b/src/options.ts new file mode 100644 index 00000000..0786fd08 --- /dev/null +++ b/src/options.ts @@ -0,0 +1,13 @@ +export interface GitHubUsernameEmailsOptions { + auth?: string; + historyLimit?: number; + username: string; +} + +export type FilledOutOptions = Required< + Omit +>; + +export const defaultOptions = { + historyLimit: 500, +}; diff --git a/src/types.ts b/src/types.ts deleted file mode 100644 index 4f16ae39..00000000 --- a/src/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export interface GreetOptions { - logger?: (message: string) => void; - message: string; - times?: number; -}