├── .prettierrc ├── media └── github-stars-to-pinboard.gif ├── babel.config.js ├── src ├── lib │ ├── pinboard │ │ ├── __snapshots__ │ │ │ └── index.test.ts.snap │ │ ├── types.ts │ │ ├── index.test.ts │ │ └── index.ts │ └── github │ │ ├── index.test.ts │ │ ├── types.ts │ │ ├── index.ts │ │ └── __snapshots__ │ │ └── index.test.ts.snap ├── utils │ ├── rate-limit.ts │ └── config.ts ├── cli │ ├── index.test.ts │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.ts ├── __playbacks__ │ ├── api.pinboard.in │ │ ├── get+v-1-posts-add+0719a7b529.nock.json │ │ ├── get+v-1-posts-add+ae80b9ca8e.nock.json │ │ ├── get+v-1-posts-add+c1848140f6.nock.json │ │ └── get+v-1-posts-add+5cb4e8d4c8.nock.json │ └── api.github.com │ │ ├── get+user-starred+1a5f3e9b85.nock.json │ │ └── get+user-starred+466d1c68d3.nock.json └── index.ts ├── .github └── workflows │ └── test.yml ├── jest.setup.js ├── .eslintrc.json ├── LICENSE ├── scripts └── scrub.js ├── .gitignore ├── package.json ├── README.md ├── tsconfig.json └── jest.config.js /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "es5" 4 | } 5 | -------------------------------------------------------------------------------- /media/github-stars-to-pinboard.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aortbals/github-stars-to-pinboard/HEAD/media/github-stars-to-pinboard.gif -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 'current', 8 | }, 9 | }, 10 | ], 11 | '@babel/preset-typescript', 12 | ], 13 | }; 14 | -------------------------------------------------------------------------------- /src/lib/pinboard/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`PinboardAPIClient #listReposStarredByAuthenticatedUser can request a single page with page size 1`] = ` 4 | Object { 5 | "result_code": "done", 6 | } 7 | `; 8 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | test: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v1 10 | - uses: actions/setup-node@v1 11 | with: 12 | node-version: '12.x' 13 | - name: yarn install, and test 14 | run: | 15 | yarn 16 | yarn test 17 | env: 18 | CI: true 19 | -------------------------------------------------------------------------------- /src/utils/rate-limit.ts: -------------------------------------------------------------------------------- 1 | const PLAYBACK = 2 | process.env.NODE_ENV === 'test' && process.env.JEST_PLAYBACK_MODE === 'play'; 3 | 4 | /** 5 | * The `async-sema` rate limit value in seconds for outbound API requests. 6 | */ 7 | export const RATE_LIMIT_SECONDS = (function rateLimitSeconds() { 8 | if (PLAYBACK) { 9 | // up to 100 per second 10 | return 100; 11 | } 12 | 13 | // Dev default of 1 every 10 seconds 14 | return 0.25; 15 | })(); 16 | -------------------------------------------------------------------------------- /jest.setup.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | require('dotenv').config(); 4 | 5 | process.env.JEST_PLAYBACK_MODE = process.env.JEST_PLAYBACK_MODE || 'play'; 6 | require('jest-playback').setup(path.join(__dirname, 'src')); 7 | 8 | if (process.env.JEST_PLAYBACK_MODE !== 'record') { 9 | process.env.GITHUB_ACCESS_TOKEN = 'REDACTED'; 10 | process.env.PINBOARD_API_TOKEN = 'REDACTED'; 11 | } 12 | 13 | if (process.env.JEST_PLAYBACK_MODE === 'record') { 14 | // eslint-disable-next-line 15 | jest.setTimeout(60000); 16 | } 17 | -------------------------------------------------------------------------------- /src/cli/index.test.ts: -------------------------------------------------------------------------------- 1 | import { main } from '.'; 2 | 3 | describe('CLI', () => { 4 | it('processes 4 total items from 2 pages with a page size of 2', async () => { 5 | const result = await main({ 6 | githubAccessToken: process.env.GITHUB_ACCESS_TOKEN as string, 7 | pinboardAPIToken: process.env.PINBOARD_API_TOKEN as string, 8 | paginationPageLimit: 2, 9 | perPage: 2, 10 | disableConfig: true, 11 | }); 12 | expect(result).toMatchSnapshot(); 13 | expect(result.length).toEqual(4); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/pinboard/types.ts: -------------------------------------------------------------------------------- 1 | export type YesNo = 'yes' | 'no'; 2 | 3 | /** 4 | * https://pinboard.in/api#posts_add 5 | */ 6 | export interface PinboardPostsAddParams { 7 | url: string; 8 | auth_token?: string; 9 | /** Title */ 10 | description?: string; 11 | /** Actual description */ 12 | extended?: string; 13 | tags?: string; 14 | /** Created at in UTC ISO-8601 */ 15 | dt?: string; 16 | replace?: YesNo; 17 | shared?: YesNo; 18 | toread?: YesNo; 19 | format?: 'json'; 20 | /** Index signature for query param encoding */ 21 | [key: string]: any; 22 | } 23 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": [ 7 | "plugin:@typescript-eslint/eslint-recommended", 8 | "prettier", 9 | "prettier/@typescript-eslint" 10 | ], 11 | "globals": { 12 | "Atomics": "readonly", 13 | "SharedArrayBuffer": "readonly" 14 | }, 15 | "parser": "@typescript-eslint/parser", 16 | "parserOptions": { 17 | "ecmaVersion": 2019, 18 | "sourceType": "module" 19 | }, 20 | "plugins": [ 21 | "@typescript-eslint", 22 | "prettier" 23 | ], 24 | "rules": {} 25 | } 26 | -------------------------------------------------------------------------------- /src/__playbacks__/api.pinboard.in/get+v-1-posts-add+0719a7b529.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.pinboard.in:443", 3 | "method": "GET", 4 | "path": "/v1/posts/add?auth_token=REDACTED&format=json&url=https%3A%2F%2Fgithub.com%2Fzeit%2Fasync-sema&description=async-sema&extended=Semaphore%20using%20%60async%60%20and%20%60await%60&dt=2019-09-21T16%3A47%3A07Z&tags=github-starred%2Casync%2Casynchronous%2Cawait%2Cpackage%2Csemaphore", 5 | "body": "", 6 | "status": 200, 7 | "response": { 8 | "result_code": "done" 9 | }, 10 | "rawHeaders": [ 11 | "Date", 12 | "Tue, 24 Sep 2019 20:39:34 GMT", 13 | "Server", 14 | "Apache/2.4.7 (Ubuntu)", 15 | "Content-Length", 16 | "23", 17 | "Connection", 18 | "close", 19 | "Content-Type", 20 | "text/plain; charset=utf-8" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/__playbacks__/api.pinboard.in/get+v-1-posts-add+ae80b9ca8e.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.pinboard.in:443", 3 | "method": "GET", 4 | "path": "/v1/posts/add?auth_token=REDACTED&format=json&url=https%3A%2F%2Fgithub.com%2Fsindresorhus%2Femittery&description=emittery&extended=Simple%20and%20modern%20async%20event%20emitter&dt=2019-09-24T00%3A57%3A46Z&tags=github-starred%2Casync%2Cemitter%2Cevent-emitter%2Cevent-listener%2Cjavascript%2Cnodejs%2Cnpm-package%2Cpromise", 5 | "body": "", 6 | "status": 200, 7 | "response": { 8 | "result_code": "done" 9 | }, 10 | "rawHeaders": [ 11 | "Date", 12 | "Tue, 24 Sep 2019 20:39:30 GMT", 13 | "Server", 14 | "Apache/2.4.7 (Ubuntu)", 15 | "Content-Length", 16 | "23", 17 | "Connection", 18 | "close", 19 | "Content-Type", 20 | "text/plain; charset=utf-8" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/__playbacks__/api.pinboard.in/get+v-1-posts-add+c1848140f6.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.pinboard.in:443", 3 | "method": "GET", 4 | "path": "/v1/posts/add?auth_token=REDACTED&format=json&url=https%3A%2F%2Fgithub.com%2Fdylanaraps%2Fpure-bash-bible&description=pure-bash-bible&extended=%F0%9F%93%96%20A%20collection%20of%20pure%20bash%20alternatives%20to%20external%20processes.&dt=2019-09-19T14%3A17%3A13Z&tags=github-starred%2Cbash%2Cbible%2Cbook%2Cguide%2Chandbook%2Chow-to%2Clearning%2Clist%2Creference%2Cscript%2Cshell%2Cshell-scripts", 5 | "body": "", 6 | "status": 200, 7 | "response": { 8 | "result_code": "done" 9 | }, 10 | "rawHeaders": [ 11 | "Date", 12 | "Tue, 24 Sep 2019 20:39:38 GMT", 13 | "Server", 14 | "Apache/2.4.7 (Ubuntu)", 15 | "Content-Length", 16 | "23", 17 | "Connection", 18 | "close", 19 | "Content-Type", 20 | "text/plain; charset=utf-8" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/__playbacks__/api.pinboard.in/get+v-1-posts-add+5cb4e8d4c8.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.pinboard.in:443", 3 | "method": "GET", 4 | "path": "/v1/posts/add?auth_token=REDACTED&format=json&url=https%3A%2F%2Fgithub.com%2Fchakra-ui%2Fchakra-ui&description=chakra-ui&extended=%E2%9A%A1%EF%B8%8FSimple%2C%20Modular%20%26%20Accessible%20UI%20Components%20for%20your%20React%20Applications&dt=2019-09-05T21%3A49%3A10Z&tags=github-starred%2Ca11y%2Caccessible%2Cchakra-ui%2Cdark-mode%2Creact%2Creact-components%2Creactjs%2Cui-components%2Cui-library%2Cuikit%2Cwai-aria", 5 | "body": "", 6 | "status": 200, 7 | "response": { 8 | "result_code": "done" 9 | }, 10 | "rawHeaders": [ 11 | "Date", 12 | "Tue, 24 Sep 2019 20:39:42 GMT", 13 | "Server", 14 | "Apache/2.4.7 (Ubuntu)", 15 | "Content-Length", 16 | "23", 17 | "Connection", 18 | "close", 19 | "Content-Type", 20 | "text/plain; charset=utf-8" 21 | ] 22 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | if (!module.parent) { 6 | process.env.DEBUG = process.env.DEBUG || 'github,pinboard'; 7 | } 8 | 9 | if (!process.env.GITHUB_ACCESS_TOKEN) { 10 | console.log("'GITHUB_ACCESS_TOKEN' environment variable is required."); 11 | process.exit(1); 12 | } 13 | 14 | if (!process.env.PINBOARD_API_TOKEN) { 15 | console.log("'PINBOARD_API_TOKEN' environment variable is required."); 16 | process.exit(1); 17 | } 18 | 19 | import { main } from './cli'; 20 | import chalk from 'chalk'; 21 | 22 | async function run() { 23 | const processed = await main({ 24 | githubAccessToken: process.env.GITHUB_ACCESS_TOKEN as string, 25 | pinboardAPIToken: process.env.PINBOARD_API_TOKEN as string, 26 | }); 27 | 28 | console.log( 29 | `\n${chalk.green('FINISHED')} processed ${processed.length} items.` 30 | ); 31 | process.exit(); 32 | } 33 | 34 | if (!module.parent) { 35 | run(); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/pinboard/index.test.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | import { PinboardAPIClient } from '.'; 6 | 7 | describe('PinboardAPIClient', () => { 8 | let client: PinboardAPIClient; 9 | 10 | beforeAll(() => { 11 | client = new PinboardAPIClient({ 12 | authToken: process.env.PINBOARD_API_TOKEN as string, 13 | }); 14 | }); 15 | 16 | describe('#listReposStarredByAuthenticatedUser', () => { 17 | it('can request a single page with page size', async () => { 18 | expect( 19 | await client.add({ 20 | url: 'https://github.com/zeit/async-sema', 21 | description: 'async-sema', 22 | extended: 'Semaphore using `async` and `await`', 23 | dt: '2019-09-21T16:47:07Z', 24 | tags: [ 25 | 'github-starred', 26 | 'async', 27 | 'asynchronous', 28 | 'await', 29 | 'package', 30 | 'semaphore', 31 | ].join(','), 32 | }) 33 | ).toMatchSnapshot(); 34 | }); 35 | }); 36 | }); 37 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Aaron Ortbals 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 | -------------------------------------------------------------------------------- /scripts/scrub.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const promisify = require('util').promisify; 6 | 7 | const readFile = promisify(fs.readFile); 8 | const writeFile = promisify(fs.writeFile); 9 | 10 | const RULES = [[/auth_token=[^?&]+/g, 'auth_token=REDACTED']]; 11 | 12 | function walk(dir, callback) { 13 | fs.readdir(dir, function(err, files) { 14 | if (err) throw err; 15 | files.forEach(function(file) { 16 | var filepath = path.join(dir, file); 17 | fs.stat(filepath, function(err, stats) { 18 | if (stats.isDirectory()) { 19 | walk(filepath, callback); 20 | } else if (stats.isFile()) { 21 | callback(filepath, stats); 22 | } 23 | }); 24 | }); 25 | }); 26 | } 27 | 28 | function main() { 29 | walk(path.join(__dirname, '..', 'src', '__playbacks__'), async path => { 30 | const buffer = await readFile(path, { 31 | encoding: 'utf8', 32 | }); 33 | let output = RULES.reduce( 34 | (out, rule) => out.replace(...rule), 35 | buffer.toString() 36 | ); 37 | 38 | writeFile(path, output, { 39 | encoding: 'utf8', 40 | }); 41 | }); 42 | } 43 | 44 | main(); 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | dist 64 | pkg 65 | .gstp_config 66 | -------------------------------------------------------------------------------- /src/lib/github/index.test.ts: -------------------------------------------------------------------------------- 1 | if (process.env.NODE_ENV !== 'production') { 2 | require('dotenv').config(); 3 | } 4 | 5 | import { GithubAPIClient } from '.'; 6 | 7 | describe('GithubAPIClient', () => { 8 | let client: GithubAPIClient; 9 | 10 | beforeAll(() => { 11 | client = new GithubAPIClient({ 12 | authToken: process.env.GITHUB_ACCESS_TOKEN as string, 13 | }); 14 | }); 15 | 16 | describe('#listReposStarredByAuthenticatedUser', () => { 17 | it('can request a single page with page size', async () => { 18 | for await (const stars of client.listReposStarredByAuthenticatedUser({ 19 | perPage: 2, 20 | paginationPageLimit: 1, 21 | })) { 22 | expect(stars).toMatchSnapshot(); 23 | } 24 | }); 25 | 26 | it('can request multiple pages with a limit', async () => { 27 | for await (const stars of client.listReposStarredByAuthenticatedUser({ 28 | perPage: 2, 29 | paginationPageLimit: 2, 30 | })) { 31 | expect(stars).toMatchSnapshot(); 32 | } 33 | }); 34 | 35 | it('can limit by a starred_at date', async () => { 36 | for await (const stars of client.listReposStarredByAuthenticatedUser({ 37 | perPage: 2, 38 | sinceStarredAt: '2019-09-21T16:47:07Z', 39 | })) { 40 | expect(stars.length).toEqual(1); 41 | } 42 | }); 43 | }); 44 | }); 45 | -------------------------------------------------------------------------------- /src/utils/config.ts: -------------------------------------------------------------------------------- 1 | import fs from 'fs'; 2 | import path from 'path'; 3 | import os from 'os'; 4 | import { promisify } from 'util'; 5 | 6 | const readFile = promisify(fs.readFile); 7 | const writeFile = promisify(fs.writeFile); 8 | const access = promisify(fs.access); 9 | 10 | const CONFIG_FILE = path.join(os.homedir(), '.gstp-config'); 11 | 12 | export interface Config { 13 | most_recent_starred_at?: string; // ISO-8601 14 | } 15 | 16 | export async function loadConfig(): Promise { 17 | try { 18 | await access(CONFIG_FILE, fs.constants.F_OK | fs.constants.W_OK); 19 | const file = await readFile(CONFIG_FILE, { 20 | encoding: 'utf8', 21 | }); 22 | return JSON.parse(file.toString()); 23 | } catch (e) { 24 | return {}; 25 | } 26 | } 27 | 28 | export async function writeConfig(config: Config) { 29 | try { 30 | return writeFile(CONFIG_FILE, JSON.stringify(config, null, 2), { 31 | encoding: 'utf8', 32 | }); 33 | } catch (e) { 34 | console.error(e); 35 | } 36 | } 37 | 38 | export async function updateConfig( 39 | config: Config, 40 | nextMostRecentStarredAt: string 41 | ) { 42 | const next = new Date(nextMostRecentStarredAt); 43 | 44 | if (config.most_recent_starred_at) { 45 | const current = new Date(config.most_recent_starred_at); 46 | if (next > current) { 47 | config.most_recent_starred_at = nextMostRecentStarredAt; 48 | } 49 | } else { 50 | config.most_recent_starred_at = nextMostRecentStarredAt; 51 | } 52 | 53 | await writeConfig(config); 54 | return config; 55 | } 56 | -------------------------------------------------------------------------------- /src/cli/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CLI processes 4 total items from 2 pages with a page size of 2 1`] = ` 4 | Array [ 5 | Object { 6 | "description": "emittery", 7 | "dt": "2019-09-24T00:57:46Z", 8 | "extended": "Simple and modern async event emitter", 9 | "tags": "github-starred,async,emitter,event-emitter,event-listener,javascript,nodejs,npm-package,promise", 10 | "url": "https://github.com/sindresorhus/emittery", 11 | }, 12 | Object { 13 | "description": "async-sema", 14 | "dt": "2019-09-21T16:47:07Z", 15 | "extended": "Semaphore using \`async\` and \`await\`", 16 | "tags": "github-starred,async,asynchronous,await,package,semaphore", 17 | "url": "https://github.com/zeit/async-sema", 18 | }, 19 | Object { 20 | "description": "pure-bash-bible", 21 | "dt": "2019-09-19T14:17:13Z", 22 | "extended": "📖 A collection of pure bash alternatives to external processes.", 23 | "tags": "github-starred,bash,bible,book,guide,handbook,how-to,learning,list,reference,script,shell,shell-scripts", 24 | "url": "https://github.com/dylanaraps/pure-bash-bible", 25 | }, 26 | Object { 27 | "description": "chakra-ui", 28 | "dt": "2019-09-05T21:49:10Z", 29 | "extended": "⚡️Simple, Modular & Accessible UI Components for your React Applications", 30 | "tags": "github-starred,a11y,accessible,chakra-ui,dark-mode,react,react-components,reactjs,ui-components,ui-library,uikit,wai-aria", 31 | "url": "https://github.com/chakra-ui/chakra-ui", 32 | }, 33 | ] 34 | `; 35 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "github-stars-to-pinboard", 3 | "version": "0.1.0", 4 | "description": "Sync Github Stars to Pinboard", 5 | "main": "src/index.ts", 6 | "repository": "git@github.com:aortbals/github-stars-to-pinboard.git", 7 | "author": "Aaron Ortbals (https://github.com/aortbals)", 8 | "license": "MIT", 9 | "private": false, 10 | "scripts": { 11 | "start": "node dist/index.js", 12 | "dev": "yarn build && yarn start", 13 | "build": "tsc", 14 | "pkg": "yarn build && yarn pkg:macos && yarn pkg:linux", 15 | "pkg:macos": "pkg -t macos -o pkg/macos/github-stars-to-pinboard dist/index.js", 16 | "pkg:linux": "pkg -t linux -o pkg/linux/github-stars-to-pinboard dist/index.js", 17 | "test": "jest", 18 | "scrub": "node scripts/scrub.js", 19 | "lint": "eslint src/**/*.ts" 20 | }, 21 | "devDependencies": { 22 | "@babel/core": "^7.6.0", 23 | "@babel/preset-env": "^7.6.0", 24 | "@babel/preset-typescript": "^7.6.0", 25 | "@types/debug": "^4.1.5", 26 | "@types/jest": "^24.0.18", 27 | "@types/node": "^12.7.5", 28 | "@types/node-fetch": "^2.5.2", 29 | "@typescript-eslint/eslint-plugin": "^2.3.1", 30 | "@typescript-eslint/parser": "^2.3.1", 31 | "dotenv": "^8.1.0", 32 | "eslint": "^6.4.0", 33 | "eslint-config-prettier": "^6.3.0", 34 | "eslint-plugin-prettier": "^3.1.1", 35 | "husky": "^3.0.5", 36 | "jest": "^24.9.0", 37 | "jest-playback": "^2.0.2", 38 | "lint-staged": "^9.3.0", 39 | "pkg": "^4.4.0", 40 | "prettier": "^1.18.2", 41 | "typescript": "^3.6.3" 42 | }, 43 | "dependencies": { 44 | "async-sema": "^3.0.1", 45 | "chalk": "^2.4.2", 46 | "debug": "^4.1.1", 47 | "node-fetch": "^2.6.0" 48 | }, 49 | "husky": { 50 | "hooks": { 51 | "pre-commit": "yarn scrub && lint-staged" 52 | } 53 | }, 54 | "lint-staged": { 55 | "src/**/*.{js,jsx,ts,tsx,md}": [ 56 | "yarn lint", 57 | "prettier --single-quote --write", 58 | "git add" 59 | ] 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/lib/pinboard/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import querystring from 'querystring'; 3 | import { RateLimit } from 'async-sema'; 4 | import chalk from 'chalk'; 5 | import Debug from 'debug'; 6 | 7 | import { RATE_LIMIT_SECONDS } from '../../utils/rate-limit'; 8 | import { PinboardPostsAddParams } from './types'; 9 | 10 | const scrub = (fn: (text: string) => void) => (arg: any) => 11 | fn( 12 | arg.replace ? arg.replace(/auth_token=[^?&]+/g, 'auth_token=REDACTED') : arg 13 | ); 14 | 15 | const log = scrub(Debug('pinboard')); 16 | const debug = scrub(Debug('pinboard:debug')); 17 | 18 | export interface PinboardAPIClientParams { 19 | authToken: string; 20 | } 21 | 22 | export class PinboardAPIClient { 23 | private authToken: string; 24 | private baseUrl = 'https://api.pinboard.in/v1'; 25 | private rateLimit = RateLimit(RATE_LIMIT_SECONDS, { 26 | uniformDistribution: true, 27 | }); 28 | 29 | constructor({ authToken }: PinboardAPIClientParams) { 30 | this.authToken = authToken; 31 | } 32 | 33 | public add = async (params: PinboardPostsAddParams) => { 34 | await this.rateLimit(); 35 | 36 | try { 37 | const response = await this.sendRequest(params); 38 | 39 | if (!response.ok) { 40 | log( 41 | `${chalk.red('ERROR')} HTTP ${response.status} ${ 42 | response.statusText 43 | } ${params.url}` 44 | ); 45 | return; 46 | } 47 | 48 | const json = await response.json(); 49 | log(`${chalk.green('OK')} ${params.url} ${json.result_code}`); 50 | debug(json); 51 | return json; 52 | } catch (e) { 53 | log(`${chalk.red('ERROR')} ${params.url} ${e.message}`); 54 | } 55 | }; 56 | 57 | private sendRequest = async (params: PinboardPostsAddParams) => { 58 | const requestParams: PinboardPostsAddParams = { 59 | auth_token: this.authToken, 60 | format: 'json', 61 | ...params, 62 | }; 63 | const url = `${this.baseUrl}/posts/add?${querystring.encode( 64 | requestParams 65 | )}`; 66 | 67 | log(`${chalk.blue('GET')} ${url}`); 68 | 69 | return fetch(url); 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Github Stars to Pinboard ![Tests Status](https://github.com/aortbals/github-stars-to-pinboard/workflows/Tests/badge.svg) 2 | 3 | `github-stars-to-pinboard` is a command line utility for syncing GitHub starred repos to Pinboard. 4 | 5 | ![github-stars-to-pinboard CLI Usage](media/github-stars-to-pinboard.gif) 6 | 7 | ## Features 8 | 9 | - Mirrors as much metadata as possible from GitHub to Pinboard 10 | - Repo Name 11 | - Repo Description 12 | - The Pinboard bookmark time reflects the same time the user starred the repo 13 | - Mirrors repo topics to Pinboard tags and includes a default tag of `github-starred` 14 | - Pagination support for iteration through starred repos 15 | - Smart rate limits to prevent 429s 16 | - Tracks last seen starred repos for fast subsequent runs (designed for cron, or similar) 17 | 18 | ## Usage 19 | 20 | To use `github-stars-to-pinboard`, download a [release](https://github.com/aortbals/github-stars-to-pinboard/releases) build for your OS distribution. These are standalone binaries that do not require Node.js or NPM. 21 | 22 | `github-stars-to-pinboard` requires two environment variables. Ensure these are available in the environment you plan on running the script. [`dotenv`](https://github.com/motdotla/dotenv) is also supported. Create a GitHub [personal access token](https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line) with `read:user` permissions. Your Pinboard API token is available [here](https://pinboard.in/settings/password). 23 | 24 | ``` 25 | GITHUB_ACCESS_TOKEN=xxxxxx 26 | PINBOARD_API_TOKEN=xxxxxx 27 | ``` 28 | 29 | Run it: 30 | 31 | ```shell 32 | ./github-stars-to-pinboard 33 | ``` 34 | 35 | Run it daily as a cronjob, outputting the logs to a file: 36 | 37 | ```shell 38 | @daily bin/github-stars-to-pinboard >> logs/github-stars-to-pinboard.log 2>&1 39 | ``` 40 | 41 | ### Config file 42 | 43 | `github-stars-to-pinboard` writes a `.gstp-config` file to your home directory. This file tracks the last starred repos that we're processed. 44 | 45 | ## Development 46 | 47 | Install the dependencies: 48 | 49 | ```shell 50 | yarn 51 | ``` 52 | 53 | Run the tests: 54 | 55 | ```shell 56 | yarn test 57 | ``` 58 | 59 | Run the CLI locally: 60 | 61 | ```shell 62 | yarn dev 63 | ``` 64 | -------------------------------------------------------------------------------- /src/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { PinboardAPIClient } from '../lib/pinboard'; 2 | import { GithubAPIClient } from '../lib/github'; 3 | import { loadConfig, updateConfig } from '../utils/config'; 4 | import { PinboardPostsAddParams } from '../lib/pinboard/types'; 5 | 6 | const DEFAULT_TAG = 'github-starred'; 7 | 8 | export interface MainParams { 9 | /** 10 | * Requests per page 11 | */ 12 | perPage?: number; 13 | 14 | /** 15 | * Limit pagination requests 16 | */ 17 | paginationPageLimit?: number; 18 | 19 | /** 20 | * Limit pagination to stars more recent than this date 21 | */ 22 | sinceStarredAt?: string; 23 | /** 24 | * Disable config file starred_at tracking functionality 25 | */ 26 | disableConfig?: boolean; 27 | /** 28 | * Pinboard API/Auth Token 29 | */ 30 | pinboardAPIToken: string; 31 | /** 32 | * GitHub Access Token 33 | */ 34 | githubAccessToken: string; 35 | } 36 | 37 | export async function main(params: MainParams) { 38 | const pinboardAPIClient = new PinboardAPIClient({ 39 | authToken: params.pinboardAPIToken, 40 | }); 41 | const githubAPIClient = new GithubAPIClient({ 42 | authToken: params.githubAccessToken, 43 | }); 44 | let config = params.disableConfig ? undefined : await loadConfig(); 45 | const processed: PinboardPostsAddParams[] = []; 46 | 47 | if (config && config.most_recent_starred_at) { 48 | console.log( 49 | `Checking for GitHub starred repos since ${new Date( 50 | config.most_recent_starred_at 51 | ).toLocaleString()}…\n` 52 | ); 53 | } 54 | 55 | for await (const stars of githubAPIClient.listReposStarredByAuthenticatedUser( 56 | { 57 | sinceStarredAt: config ? config.most_recent_starred_at : undefined, 58 | paginationPageLimit: params.paginationPageLimit, 59 | perPage: params.perPage, 60 | } 61 | )) { 62 | for (let star of stars) { 63 | try { 64 | const bookmark = { 65 | url: star.repo.html_url, 66 | description: star.repo.name || undefined, 67 | extended: star.repo.description, 68 | dt: star.starred_at, 69 | tags: star.repo.topics 70 | ? `${DEFAULT_TAG},${star.repo.topics.join(',')}` 71 | : DEFAULT_TAG, 72 | }; 73 | await pinboardAPIClient.add(bookmark); 74 | processed.push(bookmark); 75 | if (config) { 76 | config = await updateConfig(config, star.starred_at); 77 | } 78 | } catch (e) { 79 | console.error(e); 80 | } 81 | } 82 | } 83 | 84 | return processed; 85 | } 86 | -------------------------------------------------------------------------------- /src/lib/github/types.ts: -------------------------------------------------------------------------------- 1 | export type UserStarredResponse = UserStarredRepo[]; 2 | 3 | export interface UserStarredRepo { 4 | starred_at: string; 5 | repo: Repo; 6 | } 7 | 8 | export interface Repo { 9 | id: number; 10 | node_id: string; 11 | name: string; 12 | full_name: string; 13 | private: boolean; 14 | owner: Owner; 15 | html_url: string; 16 | description: string; 17 | fork: boolean; 18 | url: string; 19 | forks_url: string; 20 | keys_url: string; 21 | collaborators_url: string; 22 | teams_url: string; 23 | hooks_url: string; 24 | issue_events_url: string; 25 | events_url: string; 26 | assignees_url: string; 27 | branches_url: string; 28 | tags_url: string; 29 | blobs_url: string; 30 | git_tags_url: string; 31 | git_refs_url: string; 32 | trees_url: string; 33 | statuses_url: string; 34 | languages_url: string; 35 | stargazers_url: string; 36 | contributors_url: string; 37 | subscribers_url: string; 38 | subscription_url: string; 39 | commits_url: string; 40 | git_commits_url: string; 41 | comments_url: string; 42 | issue_comment_url: string; 43 | contents_url: string; 44 | compare_url: string; 45 | merges_url: string; 46 | archive_url: string; 47 | downloads_url: string; 48 | issues_url: string; 49 | pulls_url: string; 50 | milestones_url: string; 51 | notifications_url: string; 52 | labels_url: string; 53 | releases_url: string; 54 | deployments_url: string; 55 | created_at: string; 56 | updated_at: string; 57 | pushed_at: string; 58 | git_url: string; 59 | ssh_url: string; 60 | clone_url: string; 61 | svn_url: string; 62 | homepage: string; 63 | size: number; 64 | stargazers_count: number; 65 | watchers_count: number; 66 | language: string; 67 | has_issues: boolean; 68 | has_projects: boolean; 69 | has_downloads: boolean; 70 | has_wiki: boolean; 71 | has_pages: boolean; 72 | forks_count: number; 73 | mirror_url: null; 74 | archived: boolean; 75 | disabled: boolean; 76 | open_issues_count: number; 77 | license: License; 78 | topics: string[]; 79 | forks: number; 80 | open_issues: number; 81 | watchers: number; 82 | default_branch: string; 83 | permissions: Permissions; 84 | } 85 | 86 | export interface License { 87 | key: string; 88 | name: string; 89 | spdx_id: string; 90 | url: string; 91 | node_id: string; 92 | } 93 | 94 | export interface Owner { 95 | login: string; 96 | id: number; 97 | node_id: string; 98 | avatar_url: string; 99 | gravatar_id: string; 100 | url: string; 101 | html_url: string; 102 | followers_url: string; 103 | following_url: string; 104 | gists_url: string; 105 | starred_url: string; 106 | subscriptions_url: string; 107 | organizations_url: string; 108 | repos_url: string; 109 | events_url: string; 110 | received_events_url: string; 111 | type: string; 112 | site_admin: boolean; 113 | } 114 | 115 | export interface Permissions { 116 | admin: boolean; 117 | push: boolean; 118 | pull: boolean; 119 | } 120 | -------------------------------------------------------------------------------- /src/lib/github/index.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'node-fetch'; 2 | import { RateLimit } from 'async-sema'; 3 | import chalk from 'chalk'; 4 | import Debug from 'debug'; 5 | 6 | import { RATE_LIMIT_SECONDS } from '../../utils/rate-limit'; 7 | import { UserStarredResponse } from './types'; 8 | import { URL } from 'url'; 9 | 10 | const log = Debug('github'); 11 | const debug = Debug('github:debug'); 12 | 13 | export interface GithubAPIClientParams { 14 | authToken: string; 15 | } 16 | 17 | interface ListReposStarredByAuthenticatedUserParams { 18 | /** 19 | * Requests per page 20 | */ 21 | perPage?: number; 22 | /** 23 | * Limit pagination requests 24 | */ 25 | paginationPageLimit?: number; 26 | /** 27 | * Limit pagination to stars more recent than this date 28 | */ 29 | sinceStarredAt?: string; 30 | } 31 | 32 | export class GithubAPIClient { 33 | private authToken: string; 34 | private baseUrl = 'https://api.github.com'; 35 | private rateLimit = RateLimit(RATE_LIMIT_SECONDS, { 36 | uniformDistribution: true, 37 | }); 38 | 39 | constructor({ authToken }: GithubAPIClientParams) { 40 | this.authToken = authToken; 41 | } 42 | 43 | /** 44 | * An async generator for listing an authenticated user's GitHub 45 | * stars that supports pagination and the ability to limit the 46 | * crawl since a particular `starred_at` date. 47 | */ 48 | public async *listReposStarredByAuthenticatedUser( 49 | params: ListReposStarredByAuthenticatedUserParams = {} 50 | ) { 51 | let urlObject = new URL(`${this.baseUrl}/user/starred`); 52 | if (params.perPage) { 53 | urlObject.searchParams.append('per_page', params.perPage.toString()); 54 | } 55 | let url = urlObject.toString(); 56 | let requestCount = 0; 57 | 58 | while ( 59 | url && 60 | (params.paginationPageLimit 61 | ? requestCount < params.paginationPageLimit 62 | : true) 63 | ) { 64 | try { 65 | requestCount++; 66 | await this.rateLimit(); 67 | const response = await this.sendRequest(url); 68 | 69 | if (!response.ok) { 70 | log( 71 | `${chalk.red('ERROR')} HTTP ${response.status} ${ 72 | response.statusText 73 | } ${response.url}` 74 | ); 75 | return; 76 | } 77 | 78 | const json: UserStarredResponse = await response.json(); 79 | log(`${chalk.green('OK')} ${response.url}`); 80 | debug(json); 81 | 82 | if (params.sinceStarredAt) { 83 | const filteredItems = json.filter( 84 | (i: any) => 85 | new Date(i.starred_at) > new Date(params.sinceStarredAt as string) 86 | ); 87 | 88 | yield filteredItems; 89 | 90 | if (filteredItems.length < json.length) { 91 | log( 92 | `${chalk.blue('DONE')} all starred repos seen since ${new Date( 93 | params.sinceStarredAt 94 | ).toLocaleString()}` 95 | ); 96 | return; 97 | } 98 | } else { 99 | yield json; 100 | } 101 | 102 | const next = this.parseNextLink( 103 | response.headers.get('link') || undefined 104 | ); 105 | if (next) url = next; 106 | } catch (e) { 107 | log(`${chalk.red('ERROR')} ${e.message}`); 108 | 109 | return; 110 | } 111 | } 112 | } 113 | 114 | private sendRequest = async (url: string) => { 115 | log(`${chalk.blue('GET')} ${url}`); 116 | return fetch(url, { 117 | headers: { 118 | // Included starred_at date, include topics 119 | accept: 120 | 'application/vnd.github.v3.star+json, application/vnd.github.mercy-preview+json', 121 | authorization: `token ${this.authToken}`, 122 | }, 123 | }); 124 | }; 125 | 126 | private parseNextLink = (link?: string) => { 127 | return ((link || '').match(/<([^>]+)>;\s*rel="next"/) || [])[1]; 128 | }; 129 | } 130 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | // "incremental": true, /* Enable incremental compilation */ 5 | "target": "esnext", 6 | /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ 7 | "module": "commonjs", 8 | /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ 9 | "lib": ["esnext"], 10 | /* Specify library files to be included in the compilation. */ 11 | // "allowJs": true, /* Allow javascript files to be compiled. */ 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 14 | // "declaration": true, /* Generates corresponding '.d.ts' file. */ 15 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 16 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist", 19 | /* Redirect output structure to the directory. */ 20 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 21 | // "composite": true, /* Enable project compilation */ 22 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 23 | // "removeComments": true, /* Do not emit comments to output. */ 24 | // "noEmit": true, /* Do not emit outputs. */ 25 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 26 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 27 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 28 | 29 | /* Strict Type-Checking Options */ 30 | "strict": true, 31 | /* Enable all strict type-checking options. */ 32 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 33 | // "strictNullChecks": true, /* Enable strict null checks. */ 34 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 35 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 36 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 37 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 38 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 39 | 40 | /* Additional Checks */ 41 | "noUnusedLocals": true, 42 | /* Report errors on unused locals. */ 43 | "noUnusedParameters": true, 44 | /* Report errors on unused parameters. */ 45 | "noImplicitReturns": true, 46 | /* Report error when not all code paths in function return a value. */ 47 | "noFallthroughCasesInSwitch": true, 48 | /* Report errors for fallthrough cases in switch statement. */ 49 | 50 | /* Module Resolution Options */ 51 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 52 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 53 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 54 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 55 | // "typeRoots": [], /* List of folders to include type definitions from. */ 56 | // "types": [], /* Type declaration files to be included in compilation. */ 57 | "allowSyntheticDefaultImports": true, 58 | /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 59 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 60 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 61 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 62 | 63 | /* Source Map Options */ 64 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 65 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 66 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 67 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 68 | 69 | /* Experimental Options */ 70 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 71 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/__playbacks__/api.github.com/get+user-starred+1a5f3e9b85.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.github.com:443", 3 | "method": "GET", 4 | "path": "/user/starred?per_page=2", 5 | "body": "", 6 | "status": 200, 7 | "response": [ 8 | "1f8b0800000000000003cd9a7f6fda461880bf0af2bf83f80710c052d54d6a56b55a1a6d655b95aa228739b0", 9 | "83edb3eece49c1ca77df7b6783ed2b01ec63dafe4982e3f7b9d7f7cbf73ec9d7cc601c518a1733c40dd7702c7bd2b3263d6730b52c77387207d7f746d7a03821869b19c1c2706ddb7106e3c964d23562b2c03371cdb87d77f37c177e0cbdf7932dfaf2c79317afbfdf4e3f6c3ebd5b0defa6eb37c0885184e14e1c059c63ba812bcb340c67c56516c40b8a19a17ecaccca3d090d9e1087c0250a19ee1ae439c654a41292551003af1a084c99e1c87246563dbddfaffffaf229f41e6ffab7ef3ef46f7f7923524280467496d210403ee709734d33bfc8ecab55c0fd749e324c3d12731cf32b8f44666ae6f8b74f6f068058d10222bb012e28b02428387930c098a9a4ecf3285472c89b9621cacd4b1286e419286ad6a71a32f791a2e7252588572d2910999984fb183a0f1ee9457444c078f3a46454668a6f30910467371d1bf6a359c4415a6286bc64a698b31298ce9947838407246e9e602d1a6884ae501c6c513b1a44338088d49aa722a3201a3fc15c6c1e9e8765a65c51de46740dc51e0e9e60edb7442af140e49b442cf23f615288ae0f389ea1452416aa5cbf2f5d5866e7cef6ea36b0c0fb3104fce7204a42dc41f1a213c11644e30e629bd8ebc8c7e814bb07b4bf2474bddf398e4e28d9b7b5b5566d5d704e74f81100ac3c088774d678a34111d199095f8b85e2c11a4673421127a7768363c9d5309959fd28a608c728d2485a8603c62744a70765386002c6527cd66c3df6c892026f19b98e32334ea379be879db30c8e81f378c8133116ac628c357a6e8fc8ccdd163ba728f67c1de88e9099f94f7284d14a234d110d907948e61a1478df99129199cc47f9eb84cff432134c41a821295e6aa629087b24a75a632c5314883d10de621c865b23c71dc1cc8a9e0c51bc4ad14a87b947c0488bf7ec0a6d4f9e408ead9392014071b8a2c13cd5ddc64a8ac8327fe9c3bad6e9ca125222e539e2f8b1e9e8a3574e23f2e123380f6be458006a935c1b2ae6a50a169f4f9f3b8e3dfa8e50dd71f30dbdf84dfb5e2d76f4432d146777ad2e96a77f66663f2588fb62778286124471fb840b8099cd119c8aaeaeae321f2379fe8d30d55aab793c8010f57c38dcb5cf31db11e0fc12212e8fd44b91e2028ed821410b8d3edd2300970f5efb3cf3f8eaac4aa0b0d4484e865779511062c649acb387968c2a39263c5806de39e5c4b1a555c3646fa166f4701785611766290fbc00e62d946c62ece0c88875fa268f8747806a3e2f21420c5358a3b729ce09999917800b9c8464a3b9d7542062b9520c0ea1541ca39e6df79cd1d4b15d6be20e874271a4c9a276cf4e8338966bf75ddb12f72429f37f3025fda93d708770973425b0751633197e029bf04a25bfaf2e841d8030c6fc32ece732c83de8458a202f8429a9ac9b73db7b52df61a70221499f4438819384e1c6b0424479b7859fc7e371ed54e09134069534e9f7bbc633e27060857770f5e2ee340185dc4790279f65712ef088cdf2b56cb89ca6207bc495849247ec71b6afe3c4c572fba8dcf91cac83da5d22d7322e2fe28a44fad7b0d3069492c2fee40f546c7860720adbb408189a87b8bc40121c1749ee9e69048b2af070cca02f3251e3c173c13b1d1ea8505bb71fa69ddf8a3ba0a792c5f7c2997d988a7957974f758d53809959002bbecdbb9ebe0f1fefff1e6eefa7375b032a6b4e92c083e7fd0ae50754c3c02e2b615999f4d4cf2168172c7c5ad7788491c84b6c9138b4f3288ef57112f512e4adc5a0c3f4a7240ae031bfe595353425bab1d225860b9db11bf3620a2cf012a5219fe50587e81b048d8a26134c01c78496113d57f504f952db0f83d890f339f1f2f2d27d4d59da53fbda1d8c5c6ba42acbb13db24713314f8f1acbc1edd4fbfe69bb1edce57ab0183fd99b3d86230449579de5", 10 | "16c339a9f6db93b6528400455acac1643cb41cc553de6ceed693cdbdf36b8abe24fee27df8347fbcb1eea62b1bbce533841e7396d6ebceb2684cc35a16a91ff737c54d8d2ca5ecc6d676b212ad63252546cb464ac2e52c648eabba4b18fcc6f651529a5a4719248f1bd0e4394e44080a66cab0cb58c622839a9d845c0abb785771b070b5b96594f8dac255fc22acf5c42714775278ffae3a0ff2de07291c1fd0330af883d80a1adac51f5b3ddb2b2aa18d8da21aafed1255e0a52ca2ca6dec0f55405b73a8722ee00c55643b5ba852343da18ad331842aaba91b54e38586686f050fd15afbc043b07626f010a9bd0354697af64fa5b5f77e2a49cbf8a9b0aa3114fb7f33d7a7d22ae125ec6ccbf70a4dc603ae818a5349aa801395a5064e2c2615b9f365e7b89743e9a97f3f696cf354e8853c9e8add39c07606ef479a947f5aee4e65b6b1762ae332be4ea5b633752a45c3d1a9284d3ba7e22ee4e554ecbf61e4d436745c9ccad2b4702aee947fb39c9eed4c6dcb0505d71fbfeedfe09e916b3bee70f29a7fb3c7537bec3a43d739e5df941c4f99b7c3b79f706e8783d831dba684c04ba3f46cfb7f4a0211f3c8e4ff03d5ca96dcc00dc0b455fe2c57282ec70239a30a3879b122e0a6f0ff1aff8980b321e70b09b8c1ff57c0c9c1f22989492a6a69593ac2f752a909ab242bcd8a54135d53936af080a55493237871a9f6ed1faafe3c0109280000" 11 | ], 12 | "rawHeaders": [ 13 | "Server", 14 | "GitHub.com", 15 | "Date", 16 | "Tue, 24 Sep 2019 20:39:30 GMT", 17 | "Content-Type", 18 | "application/json; charset=utf-8", 19 | "Transfer-Encoding", 20 | "chunked", 21 | "Connection", 22 | "close", 23 | "Status", 24 | "200 OK", 25 | "X-RateLimit-Limit", 26 | "5000", 27 | "X-RateLimit-Remaining", 28 | "4973", 29 | "X-RateLimit-Reset", 30 | "1569360501", 31 | "Cache-Control", 32 | "private, max-age=60, s-maxage=60", 33 | "Vary", 34 | "Accept, Authorization, Cookie, X-GitHub-OTP", 35 | "ETag", 36 | "W/\"f4e71ba9be53675efda015f51eb122da\"", 37 | "X-OAuth-Scopes", 38 | "read:user", 39 | "X-Accepted-OAuth-Scopes", 40 | "", 41 | "X-GitHub-Media-Type", 42 | "github.v3; param=star; format=json, github.mercy-preview; format=json", 43 | "Link", 44 | "; rel=\"next\", ; rel=\"last\"", 45 | "Access-Control-Expose-Headers", 46 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type", 47 | "Access-Control-Allow-Origin", 48 | "*", 49 | "Strict-Transport-Security", 50 | "max-age=31536000; includeSubdomains; preload", 51 | "X-Frame-Options", 52 | "deny", 53 | "X-Content-Type-Options", 54 | "nosniff", 55 | "X-XSS-Protection", 56 | "1; mode=block", 57 | "Referrer-Policy", 58 | "origin-when-cross-origin, strict-origin-when-cross-origin", 59 | "Content-Security-Policy", 60 | "default-src 'none'", 61 | "Content-Encoding", 62 | "gzip", 63 | "X-GitHub-Request-Id", 64 | "D6A9:3D1D:38C4C1:89B0D7:5D8A7F01" 65 | ] 66 | } -------------------------------------------------------------------------------- /src/__playbacks__/api.github.com/get+user-starred+466d1c68d3.nock.json: -------------------------------------------------------------------------------- 1 | { 2 | "scope": "https://api.github.com:443", 3 | "method": "GET", 4 | "path": "/user/starred?per_page=2&page=2", 5 | "body": "", 6 | "status": 200, 7 | "response": [ 8 | "1f8b0800000000000003cd9adf8ee2c815875fc5e222378136ff1a1aa4d16694998c6695eed6ee30c9ecac56adc22e70751bdb2adbdd03563f432e22e5223751de22cf9317481e21bf631bff2105068a48b9698c717d75383e55aefa9a9f9356183129b9fdc0a2d6b4d5eff6269deea4d39bcc7ac3696f3ced0dbeb6da2dc903bf354d5ac26ee1ccb8371c0f6e46ed96e7dbfc81ceb56edfbd7fb977bf77ad0f930dfbf2e3b3e53d7dbbdd58dfeede599bfb773fbd01c3632b8e2b8358f2ce9c854e672ee62ec7078bd8751ff24fedb5cb3c2659109aff7d6120c5338b00593037e4ed96ffe2714961b9fe52786097ad81a5b846e3c964381ad723fd61f4872f77aef5f8d3e07ef6d4bd7bb4283a0632930fb174c171a22808a7a6999d0c07574b1139f13c0eb9b47c2fe25e7465f92b333673fe77cf6f86602c654e495382133bb440e4a0ac3568a1590bd98956ee4e0859cf6983daa50bdf75fd171076433edc8959b4a3c4a70ce12dcf62a05d62fa91c391357c95574a8008a353034adb2426bda09488b22dc893b267e6ad101255c56b6252cda6b8781e5a520491f0bd5383abb505cb974be6890d3b8785b6211014d6a961a46dd0963fa3f44e6d9c354acc74f4586b4a89e41617cf18f36701775a8317ad031ada9f5106947011f10766af6848a623f5b58d11755c652b46bdcd8bdb873efefdb73fffc5786b58285d6ed16d30fc8541538541738ac1dc884b0ff7e7998746e41bfc5bfade3502e95b3c0c7978850817be7c2a669183659666be32f214f111ace19e3451302ac140604f7cad8b224462e26f3e9a28536cee4b16f94d9345639835566256df5259459cad74c34f196039beaf9dd594019608c3981f55eb8d194851a1b91d535ebc9a6753df3123a9919e4110310b43b1f438d7cd66c149cced1c3d97ccb31c6df2169398d9517affd952376042803477fdb92e0a4f4e33e52466e8b0ecf1143d5c20460213a6c6957c71898009537023a95f0169b0c429a87850462806dd68b71833c9b38b85db32664b6d70c1411dd0437dc9368dcb9cc691558240a5359c14f3f822336289a278b3b506e604edf496a4929bae610e2fd69a3351590ea5b958ad44d39aa2119a536a23e23264aadf5d3abd6f5e081d15346112b39cc6b34745de8166a6f367c536da6a37f92e42b748b61833f975c02287e638f41630c93543cf29668245157fbdbaba4a1cced2c5f98a4bfd219e414063d272b054d38c36d962b08c5ab1285dfb2f28581b7b01d767b66e9e0b0e98d95dd58c3883542b22c0265837cc945185ae84cbc3c8f7b4e7e41254c57b7e2416c23a6637d438186bace4bb5078166f33d76da39a236109d437369c7453b19ae5daa9ca20f8329012d98ec8e52875dd3b20798649cc6c2b6bf3c0f5d79798ac2a241ae592438494cee6a6d31d757a8359b7371d4ca683d4d9c4815dbb26f53afde1acdf85c299f6c7744d1087ceaefae9033320cc757a0926e0bcd8710427a21412bbdb22f21c681b864ed9f63765cbe97ecd93b7b45c54edce203ba9e7e7dde7e451ad11b3e3af7880654c667042b1c1d1e066505b8b587eecc196f5bba3c1a4dd7a611116d478e2d74f6fd731007d72b8eb129b850fd9d06f4d23194361d1196c4d1fb1970d8b1d299d2ca79cca952fe249d4aea240cb76d94e348fa277ddbbc6742da4f473ade5617a28a65c18aadca2d922641071e5093fe05e1e66f195807285c5bd10d948689b8a6f857503be532eee6e3fce8cdfe757205781fd2d17831f67548b75ad56", 9 | "77543938347360452a5aa3d907f7f1eb1faf375f67ef372d8884c80f8485affc738bf6fb406f2de21c9b3d2ad758d824151de6d9f929c77fe9443ece616c4a0fb3081d4237e105cf092e39a61a1c6796810ef2bb95be76b2d361eb974c1ba0eb2cb3952ca112909f6d19147561f3058bdde821db2051c258081b811e022e57a8032ce6804b5a5557928dc9e2e6d0949ed5caebeb6b7b8faced5ecffabde97032ed7577656dbfdbbf9974c7e39b0659bbbe7df771783f7b3bb8db2cdf20c2fcae5a0e7b92ac130b9caa6adae2bc59bda2d1cf562f26477a3decf7fac3fe6e70ebfba7c9fa6bff7731fb1238f607f779fef8b97bfbf87e7d872011c92157dbddeb6ab79d69c8da6afc878d562d71a7a8da32b3679bda5d848ea82d595a9eb6c45c4ed3569855c38bfa38d9d296a853256dd9325de1a0f3634410b998b01c3a854ed253b4d5586a7e1751e586f6bee2af69a63bd9d4967d1447e0d415ed3ffffaf77ffde34f9fc42a7079dbb8f5edd865d2f895f1d622034bfff4313e7f347e8b8d0a9ef1d87f19786e196b3f96c68f9c5991f13608f048c8569534ef9c286bd5211e6d6915cd4fd6b32a86b69755412f256455ec934dac0a72ae8255b12ee05e55d8f3a4ab8aa4695b55481dcdaae29dea57550c5232e78bd57dc4b38dea3ee0792a751fed7c87aa22eac95315f17c6baaa269e95215b0aa5ce9c1739a2755112b881278b4203d404c19409ee02f55b45d6949fb684d240dbc5decd62e1e23a4f685a92d4155e00bd94f155a4f7baa89a935d5f29d2aee39a253c5b98ce15491cf539b2a9286d354e13465a60a79218ba942ff2ff4a5aa1f1d6fa9e2690a4b15f2b0a98485bce9f4c6f4ebb2fe1802a0c954e2b2fe5e53399cf546f87c7a9d7a0f9a6133d184a3baa954c4d9a428f737697093fb1b8687a4a4a219be53c5466e7f94565c483f0a4b3771a9a21c4dba6a47d9572a4a3a5b3194dfc3a97c4a7f25459d3668ca525d1eb09415bfb95f52de0c2fa62807f83de2ff9ba264bdde1af964c5c697961515a56633f9d459c177e23c8cbe952949bc76e81f70d9ee78fbd123c98558d43fc17b57608f20a99b187698082f4c749814aca22c29cf356149d9aa0a4b2a878bfbca5ffe03184206aa5e2a0000" 10 | ], 11 | "rawHeaders": [ 12 | "Server", 13 | "GitHub.com", 14 | "Date", 15 | "Tue, 24 Sep 2019 20:39:34 GMT", 16 | "Content-Type", 17 | "application/json; charset=utf-8", 18 | "Transfer-Encoding", 19 | "chunked", 20 | "Connection", 21 | "close", 22 | "Status", 23 | "200 OK", 24 | "X-RateLimit-Limit", 25 | "5000", 26 | "X-RateLimit-Remaining", 27 | "4971", 28 | "X-RateLimit-Reset", 29 | "1569360501", 30 | "Cache-Control", 31 | "private, max-age=60, s-maxage=60", 32 | "Vary", 33 | "Accept, Authorization, Cookie, X-GitHub-OTP", 34 | "ETag", 35 | "W/\"a58a7d39343dce4da24ed1ecbdd7d57f\"", 36 | "X-OAuth-Scopes", 37 | "read:user", 38 | "X-Accepted-OAuth-Scopes", 39 | "", 40 | "X-GitHub-Media-Type", 41 | "github.v3; param=star; format=json, github.mercy-preview; format=json", 42 | "Link", 43 | "; rel=\"prev\", ; rel=\"next\", ; rel=\"last\", ; rel=\"first\"", 44 | "Access-Control-Expose-Headers", 45 | "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type", 46 | "Access-Control-Allow-Origin", 47 | "*", 48 | "Strict-Transport-Security", 49 | "max-age=31536000; includeSubdomains; preload", 50 | "X-Frame-Options", 51 | "deny", 52 | "X-Content-Type-Options", 53 | "nosniff", 54 | "X-XSS-Protection", 55 | "1; mode=block", 56 | "Referrer-Policy", 57 | "origin-when-cross-origin, strict-origin-when-cross-origin", 58 | "Content-Security-Policy", 59 | "default-src 'none'", 60 | "Content-Encoding", 61 | "gzip", 62 | "X-GitHub-Request-Id", 63 | "D6B2:0A02:8D84E3:111518B:5D8A7F06" 64 | ] 65 | } -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/d7/06x1_7q12fl0ggh0shgpnmdw0000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | clearMocks: true, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | coverageDirectory: 'coverage', 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | // preset: null, 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | // roots: [ 119 | // "" 120 | // ], 121 | 122 | // Allows you to use a custom runner instead of Jest's default test runner 123 | // runner: "jest-runner", 124 | 125 | // The paths to modules that run some code to configure or set up the testing environment before each test 126 | // setupFiles: [], 127 | 128 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 129 | setupFilesAfterEnv: ['./jest.setup.js'], 130 | 131 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 132 | // snapshotSerializers: [], 133 | 134 | // The test environment that will be used for testing 135 | testEnvironment: 'node', 136 | 137 | // Options that will be passed to the testEnvironment 138 | // testEnvironmentOptions: {}, 139 | 140 | // Adds a location field to test results 141 | // testLocationInResults: false, 142 | 143 | // The glob patterns Jest uses to detect test files 144 | // testMatch: [ 145 | // "**/__tests__/**/*.[jt]s?(x)", 146 | // "**/?(*.)+(spec|test).[tj]s?(x)" 147 | // ], 148 | 149 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 150 | testPathIgnorePatterns: ['/node_modules/', '/dist/'], 151 | 152 | // The regexp pattern or array of patterns that Jest uses to detect test files 153 | // testRegex: [], 154 | 155 | // This option allows the use of a custom results processor 156 | // testResultsProcessor: null, 157 | 158 | // This option allows use of a custom test runner 159 | // testRunner: "jasmine2", 160 | 161 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 162 | // testURL: "http://localhost", 163 | 164 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 165 | // timers: "real", 166 | 167 | // A map from regular expressions to paths to transformers 168 | // transform: null, 169 | 170 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 171 | // transformIgnorePatterns: [ 172 | // "/node_modules/" 173 | // ], 174 | 175 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 176 | // unmockedModulePathPatterns: undefined, 177 | 178 | // Indicates whether each individual test should be reported during the run 179 | // verbose: null, 180 | 181 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 182 | // watchPathIgnorePatterns: [], 183 | 184 | // Whether to use watchman for file crawling 185 | // watchman: true, 186 | }; 187 | -------------------------------------------------------------------------------- /src/lib/github/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`GithubAPIClient #listReposStarredByAuthenticatedUser can request a single page with page size 1`] = ` 4 | Array [ 5 | Object { 6 | "repo": Object { 7 | "archive_url": "https://api.github.com/repos/sindresorhus/emittery/{archive_format}{/ref}", 8 | "archived": false, 9 | "assignees_url": "https://api.github.com/repos/sindresorhus/emittery/assignees{/user}", 10 | "blobs_url": "https://api.github.com/repos/sindresorhus/emittery/git/blobs{/sha}", 11 | "branches_url": "https://api.github.com/repos/sindresorhus/emittery/branches{/branch}", 12 | "clone_url": "https://github.com/sindresorhus/emittery.git", 13 | "collaborators_url": "https://api.github.com/repos/sindresorhus/emittery/collaborators{/collaborator}", 14 | "comments_url": "https://api.github.com/repos/sindresorhus/emittery/comments{/number}", 15 | "commits_url": "https://api.github.com/repos/sindresorhus/emittery/commits{/sha}", 16 | "compare_url": "https://api.github.com/repos/sindresorhus/emittery/compare/{base}...{head}", 17 | "contents_url": "https://api.github.com/repos/sindresorhus/emittery/contents/{+path}", 18 | "contributors_url": "https://api.github.com/repos/sindresorhus/emittery/contributors", 19 | "created_at": "2017-11-27T21:09:55Z", 20 | "default_branch": "master", 21 | "deployments_url": "https://api.github.com/repos/sindresorhus/emittery/deployments", 22 | "description": "Simple and modern async event emitter", 23 | "disabled": false, 24 | "downloads_url": "https://api.github.com/repos/sindresorhus/emittery/downloads", 25 | "events_url": "https://api.github.com/repos/sindresorhus/emittery/events", 26 | "fork": false, 27 | "forks": 36, 28 | "forks_count": 36, 29 | "forks_url": "https://api.github.com/repos/sindresorhus/emittery/forks", 30 | "full_name": "sindresorhus/emittery", 31 | "git_commits_url": "https://api.github.com/repos/sindresorhus/emittery/git/commits{/sha}", 32 | "git_refs_url": "https://api.github.com/repos/sindresorhus/emittery/git/refs{/sha}", 33 | "git_tags_url": "https://api.github.com/repos/sindresorhus/emittery/git/tags{/sha}", 34 | "git_url": "git://github.com/sindresorhus/emittery.git", 35 | "has_downloads": true, 36 | "has_issues": true, 37 | "has_pages": false, 38 | "has_projects": false, 39 | "has_wiki": false, 40 | "homepage": null, 41 | "hooks_url": "https://api.github.com/repos/sindresorhus/emittery/hooks", 42 | "html_url": "https://github.com/sindresorhus/emittery", 43 | "id": 112248999, 44 | "issue_comment_url": "https://api.github.com/repos/sindresorhus/emittery/issues/comments{/number}", 45 | "issue_events_url": "https://api.github.com/repos/sindresorhus/emittery/issues/events{/number}", 46 | "issues_url": "https://api.github.com/repos/sindresorhus/emittery/issues{/number}", 47 | "keys_url": "https://api.github.com/repos/sindresorhus/emittery/keys{/key_id}", 48 | "labels_url": "https://api.github.com/repos/sindresorhus/emittery/labels{/name}", 49 | "language": "JavaScript", 50 | "languages_url": "https://api.github.com/repos/sindresorhus/emittery/languages", 51 | "license": Object { 52 | "key": "mit", 53 | "name": "MIT License", 54 | "node_id": "MDc6TGljZW5zZTEz", 55 | "spdx_id": "MIT", 56 | "url": "https://api.github.com/licenses/mit", 57 | }, 58 | "merges_url": "https://api.github.com/repos/sindresorhus/emittery/merges", 59 | "milestones_url": "https://api.github.com/repos/sindresorhus/emittery/milestones{/number}", 60 | "mirror_url": null, 61 | "name": "emittery", 62 | "node_id": "MDEwOlJlcG9zaXRvcnkxMTIyNDg5OTk=", 63 | "notifications_url": "https://api.github.com/repos/sindresorhus/emittery/notifications{?since,all,participating}", 64 | "open_issues": 7, 65 | "open_issues_count": 7, 66 | "owner": Object { 67 | "avatar_url": "https://avatars1.githubusercontent.com/u/170270?v=4", 68 | "events_url": "https://api.github.com/users/sindresorhus/events{/privacy}", 69 | "followers_url": "https://api.github.com/users/sindresorhus/followers", 70 | "following_url": "https://api.github.com/users/sindresorhus/following{/other_user}", 71 | "gists_url": "https://api.github.com/users/sindresorhus/gists{/gist_id}", 72 | "gravatar_id": "", 73 | "html_url": "https://github.com/sindresorhus", 74 | "id": 170270, 75 | "login": "sindresorhus", 76 | "node_id": "MDQ6VXNlcjE3MDI3MA==", 77 | "organizations_url": "https://api.github.com/users/sindresorhus/orgs", 78 | "received_events_url": "https://api.github.com/users/sindresorhus/received_events", 79 | "repos_url": "https://api.github.com/users/sindresorhus/repos", 80 | "site_admin": false, 81 | "starred_url": "https://api.github.com/users/sindresorhus/starred{/owner}{/repo}", 82 | "subscriptions_url": "https://api.github.com/users/sindresorhus/subscriptions", 83 | "type": "User", 84 | "url": "https://api.github.com/users/sindresorhus", 85 | }, 86 | "permissions": Object { 87 | "admin": false, 88 | "pull": true, 89 | "push": false, 90 | }, 91 | "private": false, 92 | "pulls_url": "https://api.github.com/repos/sindresorhus/emittery/pulls{/number}", 93 | "pushed_at": "2019-09-23T14:50:16Z", 94 | "releases_url": "https://api.github.com/repos/sindresorhus/emittery/releases{/id}", 95 | "size": 888, 96 | "ssh_url": "git@github.com:sindresorhus/emittery.git", 97 | "stargazers_count": 933, 98 | "stargazers_url": "https://api.github.com/repos/sindresorhus/emittery/stargazers", 99 | "statuses_url": "https://api.github.com/repos/sindresorhus/emittery/statuses/{sha}", 100 | "subscribers_url": "https://api.github.com/repos/sindresorhus/emittery/subscribers", 101 | "subscription_url": "https://api.github.com/repos/sindresorhus/emittery/subscription", 102 | "svn_url": "https://github.com/sindresorhus/emittery", 103 | "tags_url": "https://api.github.com/repos/sindresorhus/emittery/tags", 104 | "teams_url": "https://api.github.com/repos/sindresorhus/emittery/teams", 105 | "topics": Array [ 106 | "async", 107 | "emitter", 108 | "event-emitter", 109 | "event-listener", 110 | "javascript", 111 | "nodejs", 112 | "npm-package", 113 | "promise", 114 | ], 115 | "trees_url": "https://api.github.com/repos/sindresorhus/emittery/git/trees{/sha}", 116 | "updated_at": "2019-09-24T20:13:10Z", 117 | "url": "https://api.github.com/repos/sindresorhus/emittery", 118 | "watchers": 933, 119 | "watchers_count": 933, 120 | }, 121 | "starred_at": "2019-09-24T00:57:46Z", 122 | }, 123 | Object { 124 | "repo": Object { 125 | "archive_url": "https://api.github.com/repos/zeit/async-sema/{archive_format}{/ref}", 126 | "archived": false, 127 | "assignees_url": "https://api.github.com/repos/zeit/async-sema/assignees{/user}", 128 | "blobs_url": "https://api.github.com/repos/zeit/async-sema/git/blobs{/sha}", 129 | "branches_url": "https://api.github.com/repos/zeit/async-sema/branches{/branch}", 130 | "clone_url": "https://github.com/zeit/async-sema.git", 131 | "collaborators_url": "https://api.github.com/repos/zeit/async-sema/collaborators{/collaborator}", 132 | "comments_url": "https://api.github.com/repos/zeit/async-sema/comments{/number}", 133 | "commits_url": "https://api.github.com/repos/zeit/async-sema/commits{/sha}", 134 | "compare_url": "https://api.github.com/repos/zeit/async-sema/compare/{base}...{head}", 135 | "contents_url": "https://api.github.com/repos/zeit/async-sema/contents/{+path}", 136 | "contributors_url": "https://api.github.com/repos/zeit/async-sema/contributors", 137 | "created_at": "2017-02-12T10:21:38Z", 138 | "default_branch": "master", 139 | "deployments_url": "https://api.github.com/repos/zeit/async-sema/deployments", 140 | "description": "Semaphore using \`async\` and \`await\`", 141 | "disabled": false, 142 | "downloads_url": "https://api.github.com/repos/zeit/async-sema/downloads", 143 | "events_url": "https://api.github.com/repos/zeit/async-sema/events", 144 | "fork": false, 145 | "forks": 13, 146 | "forks_count": 13, 147 | "forks_url": "https://api.github.com/repos/zeit/async-sema/forks", 148 | "full_name": "zeit/async-sema", 149 | "git_commits_url": "https://api.github.com/repos/zeit/async-sema/git/commits{/sha}", 150 | "git_refs_url": "https://api.github.com/repos/zeit/async-sema/git/refs{/sha}", 151 | "git_tags_url": "https://api.github.com/repos/zeit/async-sema/git/tags{/sha}", 152 | "git_url": "git://github.com/zeit/async-sema.git", 153 | "has_downloads": true, 154 | "has_issues": true, 155 | "has_pages": false, 156 | "has_projects": false, 157 | "has_wiki": false, 158 | "homepage": "https://npmjs.com/async-sema", 159 | "hooks_url": "https://api.github.com/repos/zeit/async-sema/hooks", 160 | "html_url": "https://github.com/zeit/async-sema", 161 | "id": 81717988, 162 | "issue_comment_url": "https://api.github.com/repos/zeit/async-sema/issues/comments{/number}", 163 | "issue_events_url": "https://api.github.com/repos/zeit/async-sema/issues/events{/number}", 164 | "issues_url": "https://api.github.com/repos/zeit/async-sema/issues{/number}", 165 | "keys_url": "https://api.github.com/repos/zeit/async-sema/keys{/key_id}", 166 | "labels_url": "https://api.github.com/repos/zeit/async-sema/labels{/name}", 167 | "language": "TypeScript", 168 | "languages_url": "https://api.github.com/repos/zeit/async-sema/languages", 169 | "license": Object { 170 | "key": "mit", 171 | "name": "MIT License", 172 | "node_id": "MDc6TGljZW5zZTEz", 173 | "spdx_id": "MIT", 174 | "url": "https://api.github.com/licenses/mit", 175 | }, 176 | "merges_url": "https://api.github.com/repos/zeit/async-sema/merges", 177 | "milestones_url": "https://api.github.com/repos/zeit/async-sema/milestones{/number}", 178 | "mirror_url": null, 179 | "name": "async-sema", 180 | "node_id": "MDEwOlJlcG9zaXRvcnk4MTcxNzk4OA==", 181 | "notifications_url": "https://api.github.com/repos/zeit/async-sema/notifications{?since,all,participating}", 182 | "open_issues": 4, 183 | "open_issues_count": 4, 184 | "owner": Object { 185 | "avatar_url": "https://avatars0.githubusercontent.com/u/14985020?v=4", 186 | "events_url": "https://api.github.com/users/zeit/events{/privacy}", 187 | "followers_url": "https://api.github.com/users/zeit/followers", 188 | "following_url": "https://api.github.com/users/zeit/following{/other_user}", 189 | "gists_url": "https://api.github.com/users/zeit/gists{/gist_id}", 190 | "gravatar_id": "", 191 | "html_url": "https://github.com/zeit", 192 | "id": 14985020, 193 | "login": "zeit", 194 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0OTg1MDIw", 195 | "organizations_url": "https://api.github.com/users/zeit/orgs", 196 | "received_events_url": "https://api.github.com/users/zeit/received_events", 197 | "repos_url": "https://api.github.com/users/zeit/repos", 198 | "site_admin": false, 199 | "starred_url": "https://api.github.com/users/zeit/starred{/owner}{/repo}", 200 | "subscriptions_url": "https://api.github.com/users/zeit/subscriptions", 201 | "type": "Organization", 202 | "url": "https://api.github.com/users/zeit", 203 | }, 204 | "permissions": Object { 205 | "admin": false, 206 | "pull": true, 207 | "push": false, 208 | }, 209 | "private": false, 210 | "pulls_url": "https://api.github.com/repos/zeit/async-sema/pulls{/number}", 211 | "pushed_at": "2019-09-18T18:25:26Z", 212 | "releases_url": "https://api.github.com/repos/zeit/async-sema/releases{/id}", 213 | "size": 43, 214 | "ssh_url": "git@github.com:zeit/async-sema.git", 215 | "stargazers_count": 206, 216 | "stargazers_url": "https://api.github.com/repos/zeit/async-sema/stargazers", 217 | "statuses_url": "https://api.github.com/repos/zeit/async-sema/statuses/{sha}", 218 | "subscribers_url": "https://api.github.com/repos/zeit/async-sema/subscribers", 219 | "subscription_url": "https://api.github.com/repos/zeit/async-sema/subscription", 220 | "svn_url": "https://github.com/zeit/async-sema", 221 | "tags_url": "https://api.github.com/repos/zeit/async-sema/tags", 222 | "teams_url": "https://api.github.com/repos/zeit/async-sema/teams", 223 | "topics": Array [ 224 | "async", 225 | "asynchronous", 226 | "await", 227 | "package", 228 | "semaphore", 229 | ], 230 | "trees_url": "https://api.github.com/repos/zeit/async-sema/git/trees{/sha}", 231 | "updated_at": "2019-09-22T17:12:59Z", 232 | "url": "https://api.github.com/repos/zeit/async-sema", 233 | "watchers": 206, 234 | "watchers_count": 206, 235 | }, 236 | "starred_at": "2019-09-21T16:47:07Z", 237 | }, 238 | ] 239 | `; 240 | 241 | exports[`GithubAPIClient #listReposStarredByAuthenticatedUser can request multiple pages with a limit 1`] = ` 242 | Array [ 243 | Object { 244 | "repo": Object { 245 | "archive_url": "https://api.github.com/repos/sindresorhus/emittery/{archive_format}{/ref}", 246 | "archived": false, 247 | "assignees_url": "https://api.github.com/repos/sindresorhus/emittery/assignees{/user}", 248 | "blobs_url": "https://api.github.com/repos/sindresorhus/emittery/git/blobs{/sha}", 249 | "branches_url": "https://api.github.com/repos/sindresorhus/emittery/branches{/branch}", 250 | "clone_url": "https://github.com/sindresorhus/emittery.git", 251 | "collaborators_url": "https://api.github.com/repos/sindresorhus/emittery/collaborators{/collaborator}", 252 | "comments_url": "https://api.github.com/repos/sindresorhus/emittery/comments{/number}", 253 | "commits_url": "https://api.github.com/repos/sindresorhus/emittery/commits{/sha}", 254 | "compare_url": "https://api.github.com/repos/sindresorhus/emittery/compare/{base}...{head}", 255 | "contents_url": "https://api.github.com/repos/sindresorhus/emittery/contents/{+path}", 256 | "contributors_url": "https://api.github.com/repos/sindresorhus/emittery/contributors", 257 | "created_at": "2017-11-27T21:09:55Z", 258 | "default_branch": "master", 259 | "deployments_url": "https://api.github.com/repos/sindresorhus/emittery/deployments", 260 | "description": "Simple and modern async event emitter", 261 | "disabled": false, 262 | "downloads_url": "https://api.github.com/repos/sindresorhus/emittery/downloads", 263 | "events_url": "https://api.github.com/repos/sindresorhus/emittery/events", 264 | "fork": false, 265 | "forks": 36, 266 | "forks_count": 36, 267 | "forks_url": "https://api.github.com/repos/sindresorhus/emittery/forks", 268 | "full_name": "sindresorhus/emittery", 269 | "git_commits_url": "https://api.github.com/repos/sindresorhus/emittery/git/commits{/sha}", 270 | "git_refs_url": "https://api.github.com/repos/sindresorhus/emittery/git/refs{/sha}", 271 | "git_tags_url": "https://api.github.com/repos/sindresorhus/emittery/git/tags{/sha}", 272 | "git_url": "git://github.com/sindresorhus/emittery.git", 273 | "has_downloads": true, 274 | "has_issues": true, 275 | "has_pages": false, 276 | "has_projects": false, 277 | "has_wiki": false, 278 | "homepage": null, 279 | "hooks_url": "https://api.github.com/repos/sindresorhus/emittery/hooks", 280 | "html_url": "https://github.com/sindresorhus/emittery", 281 | "id": 112248999, 282 | "issue_comment_url": "https://api.github.com/repos/sindresorhus/emittery/issues/comments{/number}", 283 | "issue_events_url": "https://api.github.com/repos/sindresorhus/emittery/issues/events{/number}", 284 | "issues_url": "https://api.github.com/repos/sindresorhus/emittery/issues{/number}", 285 | "keys_url": "https://api.github.com/repos/sindresorhus/emittery/keys{/key_id}", 286 | "labels_url": "https://api.github.com/repos/sindresorhus/emittery/labels{/name}", 287 | "language": "JavaScript", 288 | "languages_url": "https://api.github.com/repos/sindresorhus/emittery/languages", 289 | "license": Object { 290 | "key": "mit", 291 | "name": "MIT License", 292 | "node_id": "MDc6TGljZW5zZTEz", 293 | "spdx_id": "MIT", 294 | "url": "https://api.github.com/licenses/mit", 295 | }, 296 | "merges_url": "https://api.github.com/repos/sindresorhus/emittery/merges", 297 | "milestones_url": "https://api.github.com/repos/sindresorhus/emittery/milestones{/number}", 298 | "mirror_url": null, 299 | "name": "emittery", 300 | "node_id": "MDEwOlJlcG9zaXRvcnkxMTIyNDg5OTk=", 301 | "notifications_url": "https://api.github.com/repos/sindresorhus/emittery/notifications{?since,all,participating}", 302 | "open_issues": 7, 303 | "open_issues_count": 7, 304 | "owner": Object { 305 | "avatar_url": "https://avatars1.githubusercontent.com/u/170270?v=4", 306 | "events_url": "https://api.github.com/users/sindresorhus/events{/privacy}", 307 | "followers_url": "https://api.github.com/users/sindresorhus/followers", 308 | "following_url": "https://api.github.com/users/sindresorhus/following{/other_user}", 309 | "gists_url": "https://api.github.com/users/sindresorhus/gists{/gist_id}", 310 | "gravatar_id": "", 311 | "html_url": "https://github.com/sindresorhus", 312 | "id": 170270, 313 | "login": "sindresorhus", 314 | "node_id": "MDQ6VXNlcjE3MDI3MA==", 315 | "organizations_url": "https://api.github.com/users/sindresorhus/orgs", 316 | "received_events_url": "https://api.github.com/users/sindresorhus/received_events", 317 | "repos_url": "https://api.github.com/users/sindresorhus/repos", 318 | "site_admin": false, 319 | "starred_url": "https://api.github.com/users/sindresorhus/starred{/owner}{/repo}", 320 | "subscriptions_url": "https://api.github.com/users/sindresorhus/subscriptions", 321 | "type": "User", 322 | "url": "https://api.github.com/users/sindresorhus", 323 | }, 324 | "permissions": Object { 325 | "admin": false, 326 | "pull": true, 327 | "push": false, 328 | }, 329 | "private": false, 330 | "pulls_url": "https://api.github.com/repos/sindresorhus/emittery/pulls{/number}", 331 | "pushed_at": "2019-09-23T14:50:16Z", 332 | "releases_url": "https://api.github.com/repos/sindresorhus/emittery/releases{/id}", 333 | "size": 888, 334 | "ssh_url": "git@github.com:sindresorhus/emittery.git", 335 | "stargazers_count": 933, 336 | "stargazers_url": "https://api.github.com/repos/sindresorhus/emittery/stargazers", 337 | "statuses_url": "https://api.github.com/repos/sindresorhus/emittery/statuses/{sha}", 338 | "subscribers_url": "https://api.github.com/repos/sindresorhus/emittery/subscribers", 339 | "subscription_url": "https://api.github.com/repos/sindresorhus/emittery/subscription", 340 | "svn_url": "https://github.com/sindresorhus/emittery", 341 | "tags_url": "https://api.github.com/repos/sindresorhus/emittery/tags", 342 | "teams_url": "https://api.github.com/repos/sindresorhus/emittery/teams", 343 | "topics": Array [ 344 | "async", 345 | "emitter", 346 | "event-emitter", 347 | "event-listener", 348 | "javascript", 349 | "nodejs", 350 | "npm-package", 351 | "promise", 352 | ], 353 | "trees_url": "https://api.github.com/repos/sindresorhus/emittery/git/trees{/sha}", 354 | "updated_at": "2019-09-24T20:13:10Z", 355 | "url": "https://api.github.com/repos/sindresorhus/emittery", 356 | "watchers": 933, 357 | "watchers_count": 933, 358 | }, 359 | "starred_at": "2019-09-24T00:57:46Z", 360 | }, 361 | Object { 362 | "repo": Object { 363 | "archive_url": "https://api.github.com/repos/zeit/async-sema/{archive_format}{/ref}", 364 | "archived": false, 365 | "assignees_url": "https://api.github.com/repos/zeit/async-sema/assignees{/user}", 366 | "blobs_url": "https://api.github.com/repos/zeit/async-sema/git/blobs{/sha}", 367 | "branches_url": "https://api.github.com/repos/zeit/async-sema/branches{/branch}", 368 | "clone_url": "https://github.com/zeit/async-sema.git", 369 | "collaborators_url": "https://api.github.com/repos/zeit/async-sema/collaborators{/collaborator}", 370 | "comments_url": "https://api.github.com/repos/zeit/async-sema/comments{/number}", 371 | "commits_url": "https://api.github.com/repos/zeit/async-sema/commits{/sha}", 372 | "compare_url": "https://api.github.com/repos/zeit/async-sema/compare/{base}...{head}", 373 | "contents_url": "https://api.github.com/repos/zeit/async-sema/contents/{+path}", 374 | "contributors_url": "https://api.github.com/repos/zeit/async-sema/contributors", 375 | "created_at": "2017-02-12T10:21:38Z", 376 | "default_branch": "master", 377 | "deployments_url": "https://api.github.com/repos/zeit/async-sema/deployments", 378 | "description": "Semaphore using \`async\` and \`await\`", 379 | "disabled": false, 380 | "downloads_url": "https://api.github.com/repos/zeit/async-sema/downloads", 381 | "events_url": "https://api.github.com/repos/zeit/async-sema/events", 382 | "fork": false, 383 | "forks": 13, 384 | "forks_count": 13, 385 | "forks_url": "https://api.github.com/repos/zeit/async-sema/forks", 386 | "full_name": "zeit/async-sema", 387 | "git_commits_url": "https://api.github.com/repos/zeit/async-sema/git/commits{/sha}", 388 | "git_refs_url": "https://api.github.com/repos/zeit/async-sema/git/refs{/sha}", 389 | "git_tags_url": "https://api.github.com/repos/zeit/async-sema/git/tags{/sha}", 390 | "git_url": "git://github.com/zeit/async-sema.git", 391 | "has_downloads": true, 392 | "has_issues": true, 393 | "has_pages": false, 394 | "has_projects": false, 395 | "has_wiki": false, 396 | "homepage": "https://npmjs.com/async-sema", 397 | "hooks_url": "https://api.github.com/repos/zeit/async-sema/hooks", 398 | "html_url": "https://github.com/zeit/async-sema", 399 | "id": 81717988, 400 | "issue_comment_url": "https://api.github.com/repos/zeit/async-sema/issues/comments{/number}", 401 | "issue_events_url": "https://api.github.com/repos/zeit/async-sema/issues/events{/number}", 402 | "issues_url": "https://api.github.com/repos/zeit/async-sema/issues{/number}", 403 | "keys_url": "https://api.github.com/repos/zeit/async-sema/keys{/key_id}", 404 | "labels_url": "https://api.github.com/repos/zeit/async-sema/labels{/name}", 405 | "language": "TypeScript", 406 | "languages_url": "https://api.github.com/repos/zeit/async-sema/languages", 407 | "license": Object { 408 | "key": "mit", 409 | "name": "MIT License", 410 | "node_id": "MDc6TGljZW5zZTEz", 411 | "spdx_id": "MIT", 412 | "url": "https://api.github.com/licenses/mit", 413 | }, 414 | "merges_url": "https://api.github.com/repos/zeit/async-sema/merges", 415 | "milestones_url": "https://api.github.com/repos/zeit/async-sema/milestones{/number}", 416 | "mirror_url": null, 417 | "name": "async-sema", 418 | "node_id": "MDEwOlJlcG9zaXRvcnk4MTcxNzk4OA==", 419 | "notifications_url": "https://api.github.com/repos/zeit/async-sema/notifications{?since,all,participating}", 420 | "open_issues": 4, 421 | "open_issues_count": 4, 422 | "owner": Object { 423 | "avatar_url": "https://avatars0.githubusercontent.com/u/14985020?v=4", 424 | "events_url": "https://api.github.com/users/zeit/events{/privacy}", 425 | "followers_url": "https://api.github.com/users/zeit/followers", 426 | "following_url": "https://api.github.com/users/zeit/following{/other_user}", 427 | "gists_url": "https://api.github.com/users/zeit/gists{/gist_id}", 428 | "gravatar_id": "", 429 | "html_url": "https://github.com/zeit", 430 | "id": 14985020, 431 | "login": "zeit", 432 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjE0OTg1MDIw", 433 | "organizations_url": "https://api.github.com/users/zeit/orgs", 434 | "received_events_url": "https://api.github.com/users/zeit/received_events", 435 | "repos_url": "https://api.github.com/users/zeit/repos", 436 | "site_admin": false, 437 | "starred_url": "https://api.github.com/users/zeit/starred{/owner}{/repo}", 438 | "subscriptions_url": "https://api.github.com/users/zeit/subscriptions", 439 | "type": "Organization", 440 | "url": "https://api.github.com/users/zeit", 441 | }, 442 | "permissions": Object { 443 | "admin": false, 444 | "pull": true, 445 | "push": false, 446 | }, 447 | "private": false, 448 | "pulls_url": "https://api.github.com/repos/zeit/async-sema/pulls{/number}", 449 | "pushed_at": "2019-09-18T18:25:26Z", 450 | "releases_url": "https://api.github.com/repos/zeit/async-sema/releases{/id}", 451 | "size": 43, 452 | "ssh_url": "git@github.com:zeit/async-sema.git", 453 | "stargazers_count": 206, 454 | "stargazers_url": "https://api.github.com/repos/zeit/async-sema/stargazers", 455 | "statuses_url": "https://api.github.com/repos/zeit/async-sema/statuses/{sha}", 456 | "subscribers_url": "https://api.github.com/repos/zeit/async-sema/subscribers", 457 | "subscription_url": "https://api.github.com/repos/zeit/async-sema/subscription", 458 | "svn_url": "https://github.com/zeit/async-sema", 459 | "tags_url": "https://api.github.com/repos/zeit/async-sema/tags", 460 | "teams_url": "https://api.github.com/repos/zeit/async-sema/teams", 461 | "topics": Array [ 462 | "async", 463 | "asynchronous", 464 | "await", 465 | "package", 466 | "semaphore", 467 | ], 468 | "trees_url": "https://api.github.com/repos/zeit/async-sema/git/trees{/sha}", 469 | "updated_at": "2019-09-22T17:12:59Z", 470 | "url": "https://api.github.com/repos/zeit/async-sema", 471 | "watchers": 206, 472 | "watchers_count": 206, 473 | }, 474 | "starred_at": "2019-09-21T16:47:07Z", 475 | }, 476 | ] 477 | `; 478 | 479 | exports[`GithubAPIClient #listReposStarredByAuthenticatedUser can request multiple pages with a limit 2`] = ` 480 | Array [ 481 | Object { 482 | "repo": Object { 483 | "archive_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/{archive_format}{/ref}", 484 | "archived": false, 485 | "assignees_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/assignees{/user}", 486 | "blobs_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/git/blobs{/sha}", 487 | "branches_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/branches{/branch}", 488 | "clone_url": "https://github.com/dylanaraps/pure-bash-bible.git", 489 | "collaborators_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/collaborators{/collaborator}", 490 | "comments_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/comments{/number}", 491 | "commits_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/commits{/sha}", 492 | "compare_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/compare/{base}...{head}", 493 | "contents_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/contents/{+path}", 494 | "contributors_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/contributors", 495 | "created_at": "2018-06-13T01:39:33Z", 496 | "default_branch": "master", 497 | "deployments_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/deployments", 498 | "description": "📖 A collection of pure bash alternatives to external processes.", 499 | "disabled": false, 500 | "downloads_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/downloads", 501 | "events_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/events", 502 | "fork": false, 503 | "forks": 1515, 504 | "forks_count": 1515, 505 | "forks_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/forks", 506 | "full_name": "dylanaraps/pure-bash-bible", 507 | "git_commits_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/git/commits{/sha}", 508 | "git_refs_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/git/refs{/sha}", 509 | "git_tags_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/git/tags{/sha}", 510 | "git_url": "git://github.com/dylanaraps/pure-bash-bible.git", 511 | "has_downloads": true, 512 | "has_issues": true, 513 | "has_pages": false, 514 | "has_projects": false, 515 | "has_wiki": false, 516 | "homepage": "", 517 | "hooks_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/hooks", 518 | "html_url": "https://github.com/dylanaraps/pure-bash-bible", 519 | "id": 137147386, 520 | "issue_comment_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/issues/comments{/number}", 521 | "issue_events_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/issues/events{/number}", 522 | "issues_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/issues{/number}", 523 | "keys_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/keys{/key_id}", 524 | "labels_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/labels{/name}", 525 | "language": "Shell", 526 | "languages_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/languages", 527 | "license": Object { 528 | "key": "mit", 529 | "name": "MIT License", 530 | "node_id": "MDc6TGljZW5zZTEz", 531 | "spdx_id": "MIT", 532 | "url": "https://api.github.com/licenses/mit", 533 | }, 534 | "merges_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/merges", 535 | "milestones_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/milestones{/number}", 536 | "mirror_url": null, 537 | "name": "pure-bash-bible", 538 | "node_id": "MDEwOlJlcG9zaXRvcnkxMzcxNDczODY=", 539 | "notifications_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/notifications{?since,all,participating}", 540 | "open_issues": 25, 541 | "open_issues_count": 25, 542 | "owner": Object { 543 | "avatar_url": "https://avatars3.githubusercontent.com/u/6799467?v=4", 544 | "events_url": "https://api.github.com/users/dylanaraps/events{/privacy}", 545 | "followers_url": "https://api.github.com/users/dylanaraps/followers", 546 | "following_url": "https://api.github.com/users/dylanaraps/following{/other_user}", 547 | "gists_url": "https://api.github.com/users/dylanaraps/gists{/gist_id}", 548 | "gravatar_id": "", 549 | "html_url": "https://github.com/dylanaraps", 550 | "id": 6799467, 551 | "login": "dylanaraps", 552 | "node_id": "MDQ6VXNlcjY3OTk0Njc=", 553 | "organizations_url": "https://api.github.com/users/dylanaraps/orgs", 554 | "received_events_url": "https://api.github.com/users/dylanaraps/received_events", 555 | "repos_url": "https://api.github.com/users/dylanaraps/repos", 556 | "site_admin": false, 557 | "starred_url": "https://api.github.com/users/dylanaraps/starred{/owner}{/repo}", 558 | "subscriptions_url": "https://api.github.com/users/dylanaraps/subscriptions", 559 | "type": "User", 560 | "url": "https://api.github.com/users/dylanaraps", 561 | }, 562 | "permissions": Object { 563 | "admin": false, 564 | "pull": true, 565 | "push": false, 566 | }, 567 | "private": false, 568 | "pulls_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/pulls{/number}", 569 | "pushed_at": "2019-09-23T03:39:57Z", 570 | "releases_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/releases{/id}", 571 | "size": 383, 572 | "ssh_url": "git@github.com:dylanaraps/pure-bash-bible.git", 573 | "stargazers_count": 20639, 574 | "stargazers_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/stargazers", 575 | "statuses_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/statuses/{sha}", 576 | "subscribers_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/subscribers", 577 | "subscription_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/subscription", 578 | "svn_url": "https://github.com/dylanaraps/pure-bash-bible", 579 | "tags_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/tags", 580 | "teams_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/teams", 581 | "topics": Array [ 582 | "bash", 583 | "bible", 584 | "book", 585 | "guide", 586 | "handbook", 587 | "how-to", 588 | "learning", 589 | "list", 590 | "reference", 591 | "script", 592 | "shell", 593 | "shell-scripts", 594 | ], 595 | "trees_url": "https://api.github.com/repos/dylanaraps/pure-bash-bible/git/trees{/sha}", 596 | "updated_at": "2019-09-24T20:13:27Z", 597 | "url": "https://api.github.com/repos/dylanaraps/pure-bash-bible", 598 | "watchers": 20639, 599 | "watchers_count": 20639, 600 | }, 601 | "starred_at": "2019-09-19T14:17:13Z", 602 | }, 603 | Object { 604 | "repo": Object { 605 | "archive_url": "https://api.github.com/repos/chakra-ui/chakra-ui/{archive_format}{/ref}", 606 | "archived": false, 607 | "assignees_url": "https://api.github.com/repos/chakra-ui/chakra-ui/assignees{/user}", 608 | "blobs_url": "https://api.github.com/repos/chakra-ui/chakra-ui/git/blobs{/sha}", 609 | "branches_url": "https://api.github.com/repos/chakra-ui/chakra-ui/branches{/branch}", 610 | "clone_url": "https://github.com/chakra-ui/chakra-ui.git", 611 | "collaborators_url": "https://api.github.com/repos/chakra-ui/chakra-ui/collaborators{/collaborator}", 612 | "comments_url": "https://api.github.com/repos/chakra-ui/chakra-ui/comments{/number}", 613 | "commits_url": "https://api.github.com/repos/chakra-ui/chakra-ui/commits{/sha}", 614 | "compare_url": "https://api.github.com/repos/chakra-ui/chakra-ui/compare/{base}...{head}", 615 | "contents_url": "https://api.github.com/repos/chakra-ui/chakra-ui/contents/{+path}", 616 | "contributors_url": "https://api.github.com/repos/chakra-ui/chakra-ui/contributors", 617 | "created_at": "2019-08-17T14:27:54Z", 618 | "default_branch": "master", 619 | "deployments_url": "https://api.github.com/repos/chakra-ui/chakra-ui/deployments", 620 | "description": "⚡️Simple, Modular & Accessible UI Components for your React Applications", 621 | "disabled": false, 622 | "downloads_url": "https://api.github.com/repos/chakra-ui/chakra-ui/downloads", 623 | "events_url": "https://api.github.com/repos/chakra-ui/chakra-ui/events", 624 | "fork": false, 625 | "forks": 84, 626 | "forks_count": 84, 627 | "forks_url": "https://api.github.com/repos/chakra-ui/chakra-ui/forks", 628 | "full_name": "chakra-ui/chakra-ui", 629 | "git_commits_url": "https://api.github.com/repos/chakra-ui/chakra-ui/git/commits{/sha}", 630 | "git_refs_url": "https://api.github.com/repos/chakra-ui/chakra-ui/git/refs{/sha}", 631 | "git_tags_url": "https://api.github.com/repos/chakra-ui/chakra-ui/git/tags{/sha}", 632 | "git_url": "git://github.com/chakra-ui/chakra-ui.git", 633 | "has_downloads": true, 634 | "has_issues": true, 635 | "has_pages": false, 636 | "has_projects": true, 637 | "has_wiki": true, 638 | "homepage": "https://chakra-ui.com", 639 | "hooks_url": "https://api.github.com/repos/chakra-ui/chakra-ui/hooks", 640 | "html_url": "https://github.com/chakra-ui/chakra-ui", 641 | "id": 202890778, 642 | "issue_comment_url": "https://api.github.com/repos/chakra-ui/chakra-ui/issues/comments{/number}", 643 | "issue_events_url": "https://api.github.com/repos/chakra-ui/chakra-ui/issues/events{/number}", 644 | "issues_url": "https://api.github.com/repos/chakra-ui/chakra-ui/issues{/number}", 645 | "keys_url": "https://api.github.com/repos/chakra-ui/chakra-ui/keys{/key_id}", 646 | "labels_url": "https://api.github.com/repos/chakra-ui/chakra-ui/labels{/name}", 647 | "language": "JavaScript", 648 | "languages_url": "https://api.github.com/repos/chakra-ui/chakra-ui/languages", 649 | "license": Object { 650 | "key": "mit", 651 | "name": "MIT License", 652 | "node_id": "MDc6TGljZW5zZTEz", 653 | "spdx_id": "MIT", 654 | "url": "https://api.github.com/licenses/mit", 655 | }, 656 | "merges_url": "https://api.github.com/repos/chakra-ui/chakra-ui/merges", 657 | "milestones_url": "https://api.github.com/repos/chakra-ui/chakra-ui/milestones{/number}", 658 | "mirror_url": null, 659 | "name": "chakra-ui", 660 | "node_id": "MDEwOlJlcG9zaXRvcnkyMDI4OTA3Nzg=", 661 | "notifications_url": "https://api.github.com/repos/chakra-ui/chakra-ui/notifications{?since,all,participating}", 662 | "open_issues": 36, 663 | "open_issues_count": 36, 664 | "owner": Object { 665 | "avatar_url": "https://avatars0.githubusercontent.com/u/54212428?v=4", 666 | "events_url": "https://api.github.com/users/chakra-ui/events{/privacy}", 667 | "followers_url": "https://api.github.com/users/chakra-ui/followers", 668 | "following_url": "https://api.github.com/users/chakra-ui/following{/other_user}", 669 | "gists_url": "https://api.github.com/users/chakra-ui/gists{/gist_id}", 670 | "gravatar_id": "", 671 | "html_url": "https://github.com/chakra-ui", 672 | "id": 54212428, 673 | "login": "chakra-ui", 674 | "node_id": "MDEyOk9yZ2FuaXphdGlvbjU0MjEyNDI4", 675 | "organizations_url": "https://api.github.com/users/chakra-ui/orgs", 676 | "received_events_url": "https://api.github.com/users/chakra-ui/received_events", 677 | "repos_url": "https://api.github.com/users/chakra-ui/repos", 678 | "site_admin": false, 679 | "starred_url": "https://api.github.com/users/chakra-ui/starred{/owner}{/repo}", 680 | "subscriptions_url": "https://api.github.com/users/chakra-ui/subscriptions", 681 | "type": "Organization", 682 | "url": "https://api.github.com/users/chakra-ui", 683 | }, 684 | "permissions": Object { 685 | "admin": false, 686 | "pull": true, 687 | "push": false, 688 | }, 689 | "private": false, 690 | "pulls_url": "https://api.github.com/repos/chakra-ui/chakra-ui/pulls{/number}", 691 | "pushed_at": "2019-09-24T16:22:50Z", 692 | "releases_url": "https://api.github.com/repos/chakra-ui/chakra-ui/releases{/id}", 693 | "size": 3690, 694 | "ssh_url": "git@github.com:chakra-ui/chakra-ui.git", 695 | "stargazers_count": 2029, 696 | "stargazers_url": "https://api.github.com/repos/chakra-ui/chakra-ui/stargazers", 697 | "statuses_url": "https://api.github.com/repos/chakra-ui/chakra-ui/statuses/{sha}", 698 | "subscribers_url": "https://api.github.com/repos/chakra-ui/chakra-ui/subscribers", 699 | "subscription_url": "https://api.github.com/repos/chakra-ui/chakra-ui/subscription", 700 | "svn_url": "https://github.com/chakra-ui/chakra-ui", 701 | "tags_url": "https://api.github.com/repos/chakra-ui/chakra-ui/tags", 702 | "teams_url": "https://api.github.com/repos/chakra-ui/chakra-ui/teams", 703 | "topics": Array [ 704 | "a11y", 705 | "accessible", 706 | "chakra-ui", 707 | "dark-mode", 708 | "react", 709 | "react-components", 710 | "reactjs", 711 | "ui-components", 712 | "ui-library", 713 | "uikit", 714 | "wai-aria", 715 | ], 716 | "trees_url": "https://api.github.com/repos/chakra-ui/chakra-ui/git/trees{/sha}", 717 | "updated_at": "2019-09-24T20:14:22Z", 718 | "url": "https://api.github.com/repos/chakra-ui/chakra-ui", 719 | "watchers": 2029, 720 | "watchers_count": 2029, 721 | }, 722 | "starred_at": "2019-09-05T21:49:10Z", 723 | }, 724 | ] 725 | `; 726 | --------------------------------------------------------------------------------