forked from mirrors/probot
feat: v13 (#1874)
BREAKING CHANGE: Drop support for NodeJS < 18 BREAKING CHANGE: replace node-fetch with the Fetch API BREAKING CHANGE: default webhookPath is now /api/github/webhooks BREAKING CHANGE: probot receive now only supports payloads in JSON format, previously also (unintionally) allowed JS. closes #1643 --------- Co-authored-by: Aras Abbasi <aras.abbasi@googlemail.com> Co-authored-by: Alexander Fortin <shaftoe@users.noreply.github.com> Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Aaron Dewes <aaron.dewes@protonmail.com> Co-authored-by: Gregor Martynus <39992+gr2m@users.noreply.github.com> Co-authored-by: Aaron Dewes <aaron@runcitadel.space> Co-authored-by: prettier-toc-me[bot] <56236715+prettier-toc-me[bot]@users.noreply.github.com> Co-authored-by: Yaseen <9275716+ynx0@users.noreply.github.com>
This commit is contained in:
parent
02d81f886c
commit
948a1b7147
|
@ -0,0 +1,42 @@
|
|||
name: "CodeQL"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ "master", "*.x", "beta", "next" ]
|
||||
pull_request:
|
||||
# The branches below must be a subset of the branches above
|
||||
branches: [ "master", "beta" ]
|
||||
schedule:
|
||||
- cron: '24 7 * * 2'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }}
|
||||
timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }}
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'javascript-typescript' ]
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v2
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
queries: security-and-quality
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v2
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
|
@ -1,5 +1,5 @@
|
|||
name: Docs
|
||||
"on":
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
@ -8,14 +8,18 @@ jobs:
|
|||
name: docs
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
- run: git fetch --depth=20 origin +refs/tags/*:refs/tags/*
|
||||
- uses: actions/setup-node@v2
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 16
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: ./script/publish-docs
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build probot
|
||||
run: npm run build
|
||||
- name: Publish documentation
|
||||
run: ./script/publish-docs
|
||||
env:
|
||||
OCTOKITBOT_PAT: ${{ secrets.OCTOKITBOT_PAT }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: Release
|
||||
"on":
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
@ -11,14 +11,19 @@ jobs:
|
|||
name: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx semantic-release
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Release
|
||||
run: npx semantic-release
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
name: Test
|
||||
"on":
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
@ -7,43 +7,140 @@ name: Test
|
|||
types:
|
||||
- opened
|
||||
- synchronize
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test_matrix:
|
||||
dependency-review:
|
||||
name: Dependency Review
|
||||
if: github.event_name == 'pull_request'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Dependency review
|
||||
uses: actions/dependency-review-action@v3
|
||||
|
||||
license-check:
|
||||
name: Check Licenses
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Check Licenses
|
||||
run: npx license-checker --production --summary --onlyAllow="0BSD;Apache-2.0;Apache 2.0;Python-2.0;BSD-2-Clause;BSD-3-Clause;ISC;MIT"
|
||||
|
||||
lint:
|
||||
name: Lint
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Lint
|
||||
run: npm run lint
|
||||
|
||||
test-unit:
|
||||
name: Test on Node ${{ matrix.node-version }} and ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
node-version:
|
||||
- 10
|
||||
- 12
|
||||
- 14
|
||||
- 18
|
||||
- 20
|
||||
- 21
|
||||
os:
|
||||
- ubuntu-latest
|
||||
- macos-latest
|
||||
- windows-latest
|
||||
fail-fast: false
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v2
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
cache: npm
|
||||
- run: npm ci
|
||||
- run: npm run build
|
||||
- run: npx jest
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
run: npm run test
|
||||
|
||||
test-redis:
|
||||
name: Test on Node LTS and Redis 7
|
||||
runs-on: ubuntu-latest
|
||||
services:
|
||||
redis:
|
||||
image: 'redis:7'
|
||||
ports:
|
||||
- '6379:6379'
|
||||
options: '--entrypoint redis-server'
|
||||
steps:
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Test
|
||||
run: npm run test
|
||||
env:
|
||||
REDIS_HOST: 127.0.0.1
|
||||
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
needs: test_matrix
|
||||
needs:
|
||||
- test-unit
|
||||
- test-redis
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
- run: exit 1
|
||||
if: ${{ needs.test-unit.result != 'success' || needs.test-redis.result != 'success' }}
|
||||
- name: Check out repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Use Node.js LTS
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: lts/*
|
||||
cache: npm
|
||||
node-version: 16
|
||||
- run: npm ci
|
||||
- run: npm run lint
|
||||
- run: npm run build
|
||||
- run: npx jest
|
||||
- name: codecov
|
||||
- name: Install Dependencies
|
||||
run: npm ci
|
||||
- name: Build
|
||||
run: npm run build
|
||||
- name: Test
|
||||
run: npm run test:coverage
|
||||
- name: Update Codecov
|
||||
run: npx codecov
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
if: ${{ always() }}
|
||||
|
|
|
@ -44,7 +44,7 @@ Probot is built by people just like you! Most of the interesting things are buil
|
|||
|
||||
If you're interested in contributing to Probot itself, check out our [contributing docs](CONTRIBUTING.md) to get started.
|
||||
|
||||
Want to chat with Probot users and contributors? [Join us in Slack](https://probot-slackin.herokuapp.com/)!
|
||||
Want to discuss with Probot users and contributors? [Discuss on GitHub](https://github.com/probot/probot/discussions)!
|
||||
|
||||
## Ideas
|
||||
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
# Probot
|
||||
|
||||
[![npm version](https://img.shields.io/npm/v/probot.svg)](https://www.npmjs.com/package/probot) [![](https://img.shields.io/twitter/follow/ProbotTheRobot.svg?style=social&logo=twitter&label=Follow)](https://twitter.com/ProbotTheRobot)
|
||||
|
||||
> 🤖 Um framework para criar aplicativos do GitHub para automatizar e melhorar seu fluxo de trabalho
|
||||
|
||||
Se você já pensou, "não seria legal se o GitHub pudesse..."; Eu vou parar você aí mesmo. A maioria dos recursos pode realmente ser adicionada via [GitHub Apps](https://docs.github.com/apps/), que estende o GitHub e pode ser instalado diretamente em organizações e contas de usuários e com acesso a repositórios específicos. Eles vêm com permissões granulares e webhooks integrados. Os aplicativos são atores de primeira classe no GitHub.
|
||||
|
||||
## Como funciona
|
||||
|
||||
**Probot é um framework para construir [GitHub Apps](https://docs.github.com/apps) em [Node.js](https://nodejs.org/)**, escrito em [TypeScript](https://www.typescriptlang.org/). O GitHub Apps pode ouvir eventos de webhook enviados por um repositório ou organização. O Probot usa seu emissor de evento interno para executar ações com base nesses eventos. Um aplicativo Probot simples pode ter esta aparência:
|
||||
|
||||
```js
|
||||
module.exports = (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
const issueComment = context.issue({
|
||||
body: "Obrigado por abrir esta issue!",
|
||||
});
|
||||
return context.octokit.issues.createComment(issueComment);
|
||||
});
|
||||
};
|
||||
```
|
||||
|
||||
## Criando um app Probot
|
||||
|
||||
Se você acessou este repositório GitHub e está procurando começar a construir seu próprio aplicativo Probot, não precisa procurar mais do que [probot.github.io](https://probot.github.io/docs/)! O site Probot contém nossa extensa documentação inicial e o guiará pelo processo de configuração.
|
||||
|
||||
Este repositório hospeda o código do pacote npm Probot, que é o que todos os Apps Probot executam. A maioria das pessoas que vem neste repositório provavelmente estão querendo começar [construindo seu próprio aplicativo](https://probot.github.io/docs/).
|
||||
|
||||
## Contribuindo
|
||||
|
||||
Probot é construído por pessoas como você! A maioria das coisas interessantes são construídas com o Probot, então considere começar [escrevendo um novo aplicativo](https://probot.github.io/docs/) ou melhorando [um dos existentes](https://github.com/search?q=topic%3Aprobot-app&type=Repositories).
|
||||
|
||||
Se você estiver interessado em contribuir com o Probot, confira nossa [doc de contribuição](CONTRIBUTING.md) para começar.
|
||||
|
||||
Quer conversar com usuários Probot e colaboradores? [Junte-se a nós no Slack](https://probot-slackin.herokuapp.com/)!
|
||||
|
||||
## Ideias
|
||||
|
||||
Tem uma ideia para um novo app GitHub legal (feito com o Probot)? Isso é ótimo! Se você quer feedback, ajuda, ou apenas para compartilhá-lo com o mundo, você pode fazer isso [criando uma issue no repositório `probot/ideas`](https://github.com/probot/ideas/issues/new)!
|
|
@ -7,31 +7,31 @@ title: Configuration
|
|||
|
||||
When developing a Probot App, you will need to have several different fields in a `.env` file which specify environment variables. Here are some common use cases:
|
||||
|
||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
||||
| ----------------------------------- | ------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `APP_ID` | `new Probot({ appId })` | The App ID assigned to your GitHub App. **Required** <p>_(Example: `1234`)_</p> |
|
||||
| `PRIVATE_KEY` or `PRIVATE_KEY_PATH` | `new Probot({ privateKey })` | The contents of the private key for your GitHub App. If you're unable to use multiline environment variables, use base64 encoding to convert the key to a single line string. See the [Deployment](/docs/deployment) docs for provider specific usage. When using the `PRIVATE_KEY_PATH` environment variable, set it to the path of the `.pem` file that you downloaded from your GitHub App registration. <p>_(Example: `path/to/key.pem`)_</p> |
|
||||
| **Webhook options** | |
|
||||
| `WEBHOOK_PROXY_URL` | `new Server({ webhookProxy })` | Allows your local development environment to receive GitHub webhook events. Go to https://smee.io/new to get started. <p>_(Example: `https://smee.io/your-custom-url`)_</p> |
|
||||
| `WEBHOOK_SECRET` | `new Probot({ secret })` | The webhook secret used when creating a GitHub App. 'development' is used as a default, but the value in `.env` needs to match the value configured in your App settings on GitHub. Note: GitHub marks this value as optional, but for optimal security it's required for Probot apps. **Required** <p>_(Example: `development`)_</p> |
|
||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
||||
| ----------------------------------- | ----------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `APP_ID` | `new Probot({ appId })` | The App ID assigned to your GitHub App. **Required** <p>_(Example: `1234`)_</p> |
|
||||
| `PRIVATE_KEY` or `PRIVATE_KEY_PATH` | `new Probot({ privateKey })` | The contents of the private key for your GitHub App. If you're unable to use multiline environment variables, use base64 encoding to convert the key to a single line string. See the [Deployment](/docs/deployment) docs for provider specific usage. When using the `PRIVATE_KEY_PATH` environment variable, set it to the path of the `.pem` file that you downloaded from your GitHub App registration. <p>_(Example: `path/to/key.pem`)_</p> |
|
||||
| **Webhook options** | |
|
||||
| `WEBHOOK_PROXY_URL` | `new Server({ webhookProxy })` | Allows your local development environment to receive GitHub webhook events. Go to https://smee.io/new to get started. <p>_(Example: `https://smee.io/your-custom-url`)_</p> |
|
||||
| `WEBHOOK_SECRET` | `new Probot({ secret })` | The webhook secret used when creating a GitHub App. 'development' is used as a default, but the value in `.env` needs to match the value configured in your App settings on GitHub. Note: GitHub marks this value as optional, but for optimal security it's required for Probot apps. **Required** <p>_(Example: `development`)_</p> |
|
||||
|
||||
For more on the set up of these items, check out [Configuring a GitHub App](/docs/development/#configuring-a-github-app).
|
||||
|
||||
Some less common environment variables are:
|
||||
|
||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
||||
| --------------------- | ------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GHE_HOST` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The hostname of your GitHub Enterprise instance. <p>_(Example: `github.mycompany.com`)_</p> |
|
||||
| `GHE_PROTOCOL` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The protocol of your GitHub Enterprise instance. Defaults to HTTPS. Do not change unless you are certain. <p>_(Example: `https`)_</p> |
|
||||
| `GH_ORG` | - | The organization where you want to register the app in the app creation manifest flow. If set, the app is registered for an organization (https://github.com/organizations/ORGANIZATION/settings/apps/new), if not set, the GitHub app would be registered for the user account (https://github.com/settings/apps/new). |
|
||||
| `LOG_FORMAT` | - | By default, logs are formatted for readability in development. You can set this to `json` in order to disable the formatting |
|
||||
| `LOG_LEVEL` | `const log = require('pino')({ level })` | The verbosity of logs to show when running your app, which can be `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`. Default: `info` |
|
||||
| `LOG_LEVEL_IN_STRING` | - | By default, when using the `json` format, the level printed in the log records is an int (`10`, `20`, ..). This option tells the logger to print level as a string: `{"level": "info"}`. Default `false` |
|
||||
| `LOG_MESSAGE_KEY` | `const log = require('pino')({ messageKey: 'msg' })` | Only relevant when `LOG_FORMAT` is set to `json`. Sets the json key for the log message. Default: `msg` |
|
||||
| `SENTRY_DSN` | - | Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown by your app. <p>_(Example: `https://1234abcd@sentry.io/12345`)_</p> |
|
||||
| `PORT` | `new Server({ port })` | The port to start the local server on. Default: `3000` |
|
||||
| `HOST` | `new Server({ host })` | The host to start the local server on. |
|
||||
| `WEBHOOK_PATH` | `new Server({ webhookPath })` | The URL path which will receive webhooks. Default: `/` |
|
||||
| `REDIS_URL` | `new Probot({ redisConfig })` | Set to a `redis://` url as connection option for [ioredis](https://github.com/luin/ioredis#connect-to-redis) in order to enable [cluster support for request throttling](https://github.com/octokit/plugin-throttling.js#clustering). <p>_(Example: `redis://:secret@redis-123.redislabs.com:12345/0`)_</p> |
|
||||
| --------------------- | ----------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| `GHE_HOST` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The hostname of your GitHub Enterprise instance. <p>_(Example: `github.mycompany.com`)_</p> |
|
||||
| `GHE_PROTOCOL` | `new Probot({ Octokit: ProbotOctokit.defaults({ baseUrl }) })` | The protocol of your GitHub Enterprise instance. Defaults to HTTPS. Do not change unless you are certain. <p>_(Example: `https`)_</p> |
|
||||
| `GH_ORG` | - | The organization where you want to register the app in the app creation manifest flow. If set, the app is registered for an organization (https://github.com/organizations/ORGANIZATION/settings/apps/new), if not set, the GitHub app would be registered for the user account (https://github.com/settings/apps/new). |
|
||||
| `LOG_FORMAT` | - | By default, logs are formatted for readability in development. You can set this to `json` in order to disable the formatting |
|
||||
| `LOG_LEVEL` | `const log = require('pino')({ level })` | The verbosity of logs to show when running your app, which can be `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`. Default: `info` |
|
||||
| `LOG_LEVEL_IN_STRING` | - | By default, when using the `json` format, the level printed in the log records is an int (`10`, `20`, ..). This option tells the logger to print level as a string: `{"level": "info"}`. Default `false` |
|
||||
| `LOG_MESSAGE_KEY` | `const log = require('pino')({ messageKey: 'msg' })` | Only relevant when `LOG_FORMAT` is set to `json`. Sets the json key for the log message. Default: `msg` |
|
||||
| `SENTRY_DSN` | - | Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown by your app. <p>_(Example: `https://1234abcd@sentry.io/12345`)_</p> |
|
||||
| `PORT` | `new Server({ port })` | The port to start the local server on. Default: `3000` |
|
||||
| `HOST` | `new Server({ host })` | The host to start the local server on. |
|
||||
| `WEBHOOK_PATH` | `new Server({ webhookPath })` | The URL path which will receive webhooks. Default: `/api/github/webhooks` |
|
||||
| `REDIS_URL` | `new Probot({ redisConfig })` | Set to a `redis://` url as connection option for [ioredis](https://github.com/luin/ioredis#connect-to-redis) in order to enable [cluster support for request throttling](https://github.com/octokit/plugin-throttling.js#clustering). <p>_(Example: `redis://:secret@redis-123.redislabs.com:12345/0`)_</p> |
|
||||
|
||||
For more information on the formatting conventions and rules of `.env` files, check out [the npm `dotenv` module's documentation](https://www.npmjs.com/package/dotenv#rules).
|
||||
|
|
|
@ -5,7 +5,7 @@ title: Developing an app
|
|||
|
||||
# Developing an app
|
||||
|
||||
To develop a Probot app, you will first need a recent version of [Node.js](https://nodejs.org/) installed. Open a terminal and run `node -v` to verify that it is installed and is at least 10.0.0 or later. Otherwise, [install the latest version](https://nodejs.org/).
|
||||
To develop a Probot app, you will first need a recent version of [Node.js](https://nodejs.org/) installed. Open a terminal and run `node -v` to verify that it is installed and is at least 18.0.0 or later. Otherwise, [install the latest version](https://nodejs.org/).
|
||||
|
||||
**Contents:**
|
||||
|
||||
|
@ -224,7 +224,7 @@ const app = require("./index.js");
|
|||
module.exports = createNodeMiddleware(app, { probot: createProbot() });
|
||||
```
|
||||
|
||||
By default, `createNodeMiddleware()` uses `/` as the webhook endpoint. To customize this behaviour, you can use the `webhooksPath` option.
|
||||
By default, `createNodeMiddleware()` uses `/api/github/webhooks` as the webhook endpoint. To customize this behaviour, you can use the `webhooksPath` option.
|
||||
|
||||
```js
|
||||
module.exports = createNodeMiddleware(app, {
|
||||
|
|
|
@ -53,7 +53,7 @@ module.exports = (app) => {
|
|||
context.octokit.issues.createComment(
|
||||
context.issue({
|
||||
body: `There were ${edits} edits to issues in this thread.`,
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ module.exports = (app) => {
|
|||
response.data.issues.forEach((issue) => {
|
||||
context.log.info("Issue: %s", issue.title);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -33,7 +33,7 @@ module.exports = (app) => {
|
|||
const allIssues = await context.octokit.paginate(
|
||||
context.octokit.issues.list,
|
||||
context.repo(),
|
||||
(response) => response.data
|
||||
(response) => response.data,
|
||||
);
|
||||
console.log(allIssues);
|
||||
});
|
||||
|
@ -58,7 +58,7 @@ module.exports = (app) => {
|
|||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
};
|
||||
|
@ -66,14 +66,14 @@ module.exports = (app) => {
|
|||
|
||||
## Async iterators
|
||||
|
||||
If your runtime environment supports async iterators (such as Node 10+), you can iterate through each response
|
||||
Using async iterators you can iterate through each response
|
||||
|
||||
```js
|
||||
module.exports = (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
for await (const response of octokit.paginate.iterator(
|
||||
context.octokit.issues.list,
|
||||
context.repo()
|
||||
context.repo(),
|
||||
)) {
|
||||
for (const issue of response.data) {
|
||||
if (issue.body.includes("something")) {
|
||||
|
|
|
@ -15,10 +15,14 @@ To save a copy of the payload, go to the [settings](https://github.com/settings/
|
|||
|
||||
Next, simulate receiving this event being delivered by running:
|
||||
|
||||
$ node_modules/.bin/probot receive -e <event-name> -p <path-to-fixture> <path-to-app>
|
||||
```bash
|
||||
$ node_modules/.bin/probot receive -e <event-name> -p <path-to-fixture> <path-to-app>
|
||||
```
|
||||
|
||||
Note that `event-name` here is just the name of the event (like pull_request or issues) and not the action (like labeled). You can find it in the GitHub deliveries history under the `X-GitHub-Event` header.
|
||||
|
||||
For example, to simulate receiving the `pull_request.labeled` event, run:
|
||||
|
||||
$ node_modules/.bin/probot receive -e pull_request -p test/fixtures/pull_request.labeled.json ./index.js
|
||||
```bash
|
||||
$ node_modules/.bin/probot receive -e pull_request -p test/fixtures/pull_request.labeled.json ./index.js
|
||||
```
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
'use strict';
|
||||
|
||||
const { createServer } = require("http");
|
||||
const { createNodeMiddleware } = require('../lib/create-node-middleware');
|
||||
const { createProbot } = require('../lib/create-probot');
|
||||
const { sign } = require("@octokit/webhooks-methods");
|
||||
const WebhookExamples = require("@octokit/webhooks-examples");
|
||||
|
||||
process.env.APP_ID = "123";
|
||||
process.env.PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
process.env.WEBHOOK_SECRET = "secret";
|
||||
process.env.WEBHOOK_PATH = "/";
|
||||
|
||||
const pushEvent = JSON.stringify((
|
||||
WebhookExamples.filter(
|
||||
(event) => event.name === "push",
|
||||
)[0]
|
||||
).examples[0]);
|
||||
|
||||
const appFn = (app) => {
|
||||
app.on("issues.opened", async (context) => {
|
||||
const issueComment = context.issue({
|
||||
body: "Thanks for opening this issue!",
|
||||
});
|
||||
return context.octokit.issues.createComment(issueComment);
|
||||
});
|
||||
|
||||
app.onAny(async (context) => {
|
||||
context.log.info({ event: context.name, action: context.payload.action });
|
||||
});
|
||||
|
||||
app.onError(async (error) => {
|
||||
app.log.error(error);
|
||||
});
|
||||
};
|
||||
|
||||
const middleware = createNodeMiddleware(appFn, { probot: createProbot() });
|
||||
|
||||
const server = createServer(middleware);
|
||||
|
||||
server.listen(3000, async () => {
|
||||
console.log("Probot started http://localhost:3000/")
|
||||
console.log(`autocannon -m POST -b '${pushEvent}' -H content-type=application/json -H x-github-event=push -H x-github-delivery=1 -H x-hub-signature-256=${await sign("secret", pushEvent)} http://127.0.0.1:3000/`)
|
||||
});
|
File diff suppressed because it is too large
Load Diff
130
package.json
130
package.json
|
@ -12,9 +12,10 @@
|
|||
"build": "rimraf lib && tsc -p tsconfig.json",
|
||||
"lint": "prettier --check \"src/**/*.ts\" \"test/**/*.ts\" \"docs/*.md\" *.md package.json tsconfig.json --end-of-line auto",
|
||||
"lint:fix": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"docs/*.md\" *.md package.json tsconfig.json --end-of-line auto",
|
||||
"pretest": "tsc --noEmit -p test",
|
||||
"test": "jest",
|
||||
"posttest": "npm run lint",
|
||||
"pretest": "npm run build && tsc --noEmit -p test",
|
||||
"test": "vitest run",
|
||||
"test:coverage": "vitest run --coverage",
|
||||
"test:dev": "vitest --ui --coverage",
|
||||
"doc": "typedoc --options .typedoc.json"
|
||||
},
|
||||
"files": [
|
||||
|
@ -35,92 +36,65 @@
|
|||
"author": "Brandon Keepers",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@octokit/core": "^3.2.4",
|
||||
"@octokit/plugin-enterprise-compatibility": "^1.2.8",
|
||||
"@octokit/plugin-paginate-rest": "^2.6.2",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^5.0.1",
|
||||
"@octokit/plugin-retry": "^3.0.6",
|
||||
"@octokit/plugin-throttling": "^3.3.4",
|
||||
"@octokit/types": "^8.0.0",
|
||||
"@octokit/webhooks": "^9.26.3",
|
||||
"@probot/get-private-key": "^1.1.0",
|
||||
"@probot/octokit-plugin-config": "^1.0.0",
|
||||
"@probot/pino": "^2.2.0",
|
||||
"@types/express": "^4.17.9",
|
||||
"@types/ioredis": "^4.27.1",
|
||||
"@types/pino": "^6.3.4",
|
||||
"@types/pino-http": "^5.0.6",
|
||||
"commander": "^6.2.0",
|
||||
"deepmerge": "^4.2.2",
|
||||
"deprecation": "^2.3.1",
|
||||
"dotenv": "^8.2.0",
|
||||
"@octokit/core": "^5.0.2",
|
||||
"@octokit/plugin-enterprise-compatibility": "^4.0.1",
|
||||
"@octokit/plugin-paginate-rest": "^9.1.4",
|
||||
"@octokit/plugin-rest-endpoint-methods": "^10.1.5",
|
||||
"@octokit/plugin-retry": "^6.0.1",
|
||||
"@octokit/plugin-throttling": "^8.1.3",
|
||||
"@octokit/request": "^8.1.6",
|
||||
"@octokit/types": "^12.3.0",
|
||||
"@octokit/webhooks": "^12.0.10",
|
||||
"@probot/get-private-key": "^1.1.2",
|
||||
"@probot/octokit-plugin-config": "^2.0.1",
|
||||
"@probot/pino": "^2.3.5",
|
||||
"@types/express": "^4.17.21",
|
||||
"commander": "^11.1.0",
|
||||
"deepmerge": "^4.3.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eventsource": "^2.0.2",
|
||||
"express": "^4.17.1",
|
||||
"express-handlebars": "^6.0.3",
|
||||
"ioredis": "^4.27.8",
|
||||
"js-yaml": "^3.14.1",
|
||||
"lru-cache": "^6.0.0",
|
||||
"octokit-auth-probot": "^1.2.2",
|
||||
"pino": "^6.7.0",
|
||||
"pino-http": "^5.3.0",
|
||||
"express": "^4.18.2",
|
||||
"ioredis": "^5.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lru-cache": "^10.0.3",
|
||||
"octokit-auth-probot": "^2.0.0",
|
||||
"pino": "^8.16.1",
|
||||
"pino-http": "^8.5.1",
|
||||
"pkg-conf": "^3.1.0",
|
||||
"resolve": "^1.19.0",
|
||||
"semver": "^7.3.4",
|
||||
"update-dotenv": "^1.1.1",
|
||||
"uuid": "^8.3.2"
|
||||
"resolve": "^1.22.8",
|
||||
"update-dotenv": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@octokit/webhooks-examples": "^4.0.0",
|
||||
"@octokit/webhooks-methods": "^3.0.0",
|
||||
"@octokit/webhooks-types": "^4.0.0",
|
||||
"@tsconfig/node10": "^1.0.7",
|
||||
"@types/eventsource": "^1.1.5",
|
||||
"@types/jest": "^26.0.18",
|
||||
"@types/js-yaml": "^3.12.5",
|
||||
"@types/jsonwebtoken": "^8.5.0",
|
||||
"@types/node": "^16.0.0",
|
||||
"@types/readable-stream": "^2.3.9",
|
||||
"@types/resolve": "^1.17.1",
|
||||
"@types/semver": "^7.3.4",
|
||||
"@types/supertest": "^2.0.10",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"body-parser": "^1.19.0",
|
||||
"@octokit/tsconfig": "^2.0.0",
|
||||
"@octokit/webhooks-examples": "^7.3.1",
|
||||
"@octokit/webhooks-methods": "^4.0.0",
|
||||
"@octokit/webhooks-types": "^7.3.1",
|
||||
"@types/eventsource": "^1.1.15",
|
||||
"@types/js-yaml": "^4.0.9",
|
||||
"@types/node": "^18.18.1",
|
||||
"@types/resolve": "^1.20.5",
|
||||
"@types/supertest": "^2.0.16",
|
||||
"@vitest/coverage-v8": "^0.34.6",
|
||||
"@vitest/ui": "^0.34.6",
|
||||
"bottleneck": "^2.19.5",
|
||||
"connect-sse": "^1.2.0",
|
||||
"execa": "^5.0.0",
|
||||
"fetch-mock": "npm:@gr2m/fetch-mock@9.11.0-pull-request-644.1",
|
||||
"get-port": "^5.1.1",
|
||||
"got": "^11.8.0",
|
||||
"jest": "^26.6.3",
|
||||
"nock": "^13.0.5",
|
||||
"prettier": "^2.2.1",
|
||||
"rimraf": "^3.0.2",
|
||||
"semantic-release": "^19.0.3",
|
||||
"prettier": "^3.0.3",
|
||||
"rimraf": "^5.0.5",
|
||||
"semantic-release": "^22.0.7",
|
||||
"semantic-release-plugin-update-version-in-files": "^1.1.0",
|
||||
"smee-client": "^1.2.2",
|
||||
"supertest": "^6.0.1",
|
||||
"ts-jest": "^26.4.4",
|
||||
"tsd": "^0.23.0",
|
||||
"typedoc": "^0.22.10",
|
||||
"typescript": "^4.1.2"
|
||||
"smee-client": "^2.0.0",
|
||||
"sonic-boom": "^3.7.0",
|
||||
"supertest": "^6.3.3",
|
||||
"tsd": "^0.29.0",
|
||||
"typedoc": "^0.25.3",
|
||||
"typescript": "^5.2.2",
|
||||
"vitest": "^0.34.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.21"
|
||||
},
|
||||
"jest": {
|
||||
"testEnvironment": "node",
|
||||
"coveragePathIgnorePatterns": [
|
||||
"<rootDir>/lib/"
|
||||
],
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js",
|
||||
"json",
|
||||
"node"
|
||||
],
|
||||
"preset": "ts-jest"
|
||||
},
|
||||
"tsd": {
|
||||
"directory": "test/types"
|
||||
"node": ">=18"
|
||||
},
|
||||
"release": {
|
||||
"plugins": [
|
||||
|
|
|
@ -1,25 +1,28 @@
|
|||
import path from "path";
|
||||
import { ApplicationFunctionOptions, Probot } from "../index";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
import type { ApplicationFunctionOptions, Probot } from "../index.js";
|
||||
import { loadPackageJson } from "../helpers/load-package-json.js";
|
||||
import { probotView } from "../views/probot.js";
|
||||
|
||||
export function defaultApp(
|
||||
app: Probot,
|
||||
{ getRouter }: ApplicationFunctionOptions
|
||||
_app: Probot,
|
||||
{ getRouter, cwd = process.cwd() }: ApplicationFunctionOptions,
|
||||
) {
|
||||
if (!getRouter) {
|
||||
throw new Error("getRouter() is required for defaultApp");
|
||||
}
|
||||
|
||||
const pkg = loadPackageJson(resolve(cwd, "package.json"));
|
||||
const probotViewRendered = probotView({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
});
|
||||
const router = getRouter();
|
||||
|
||||
router.get("/probot", (req, res) => {
|
||||
let pkg;
|
||||
try {
|
||||
pkg = require(path.join(process.cwd(), "package.json"));
|
||||
} catch (e) {
|
||||
pkg = {};
|
||||
}
|
||||
|
||||
res.render("probot.handlebars", pkg);
|
||||
router.get("/probot", (_req, res) => {
|
||||
res.send(probotViewRendered);
|
||||
});
|
||||
router.get("/", (req, res, next) => res.redirect("/probot"));
|
||||
|
||||
router.get("/", (_req, res) => res.redirect("/probot"));
|
||||
}
|
||||
|
|
|
@ -1,23 +1,34 @@
|
|||
import bodyParser from "body-parser";
|
||||
import { exec } from "child_process";
|
||||
import { Request, Response } from "express";
|
||||
import { exec } from "node:child_process";
|
||||
|
||||
import type { IncomingMessage, ServerResponse } from "http";
|
||||
import { parse as parseQuery } from "querystring";
|
||||
import express from "express";
|
||||
import updateDotenv from "update-dotenv";
|
||||
|
||||
import { Probot } from "../probot";
|
||||
import { ManifestCreation } from "../manifest-creation";
|
||||
import { getLoggingMiddleware } from "../server/logging-middleware";
|
||||
import { ApplicationFunctionOptions } from "../types";
|
||||
import { isProduction } from "../helpers/is-production";
|
||||
import { Probot } from "../probot.js";
|
||||
import { ManifestCreation } from "../manifest-creation.js";
|
||||
import { getLoggingMiddleware } from "../server/logging-middleware.js";
|
||||
import type { ApplicationFunctionOptions } from "../types.js";
|
||||
import { isProduction } from "../helpers/is-production.js";
|
||||
|
||||
import { importView } from "../views/import.js";
|
||||
import { setupView } from "../views/setup.js";
|
||||
import { successView } from "../views/success.js";
|
||||
|
||||
export const setupAppFactory = (
|
||||
host: string | undefined,
|
||||
port: number | undefined
|
||||
port: number | undefined,
|
||||
) =>
|
||||
async function setupApp(
|
||||
app: Probot,
|
||||
{ getRouter }: ApplicationFunctionOptions
|
||||
{ getRouter }: ApplicationFunctionOptions,
|
||||
) {
|
||||
if (!getRouter) {
|
||||
throw new Error("getRouter is required to use the setup app");
|
||||
}
|
||||
|
||||
const setup: ManifestCreation = new ManifestCreation();
|
||||
const pkg = setup.pkg;
|
||||
|
||||
// If not on Glitch or Production, create a smee URL
|
||||
if (
|
||||
|
@ -31,75 +42,132 @@ export const setupAppFactory = (
|
|||
await setup.createWebhookChannel();
|
||||
}
|
||||
|
||||
if (!getRouter) {
|
||||
throw new Error("getRouter is required to use the setup app");
|
||||
}
|
||||
|
||||
const route = getRouter();
|
||||
|
||||
route.use(getLoggingMiddleware(app.log));
|
||||
|
||||
printWelcomeMessage(app, host, port);
|
||||
|
||||
route.get("/probot", async (req, res) => {
|
||||
route.get("/probot", async (req: IncomingMessage, res: ServerResponse) => {
|
||||
const baseUrl = getBaseUrl(req);
|
||||
const pkg = setup.pkg;
|
||||
const manifest = setup.getManifest(pkg, baseUrl);
|
||||
const createAppUrl = setup.createAppUrl;
|
||||
// Pass the manifest to be POST'd
|
||||
res.render("setup.handlebars", { pkg, createAppUrl, manifest });
|
||||
res.writeHead(200, { "content-type": "text/html" }).end(
|
||||
setupView({
|
||||
name: pkg.name,
|
||||
version: pkg.version,
|
||||
description: pkg.description,
|
||||
createAppUrl,
|
||||
manifest,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
route.get("/probot/setup", async (req: Request, res: Response) => {
|
||||
const { code } = req.query;
|
||||
const response = await setup.createAppFromCode(code);
|
||||
route.get(
|
||||
"/probot/setup",
|
||||
async (req: IncomingMessage, res: ServerResponse) => {
|
||||
// @ts-expect-error query could be set by a framework, e.g. express
|
||||
const { code } = req.query || parseQuery(req.url?.split("?")[1] || "");
|
||||
|
||||
// If using glitch, restart the app
|
||||
if (process.env.PROJECT_DOMAIN) {
|
||||
exec("refresh", (error) => {
|
||||
if (error) {
|
||||
app.log.error(error);
|
||||
}
|
||||
if (!code || typeof code !== "string" || code.length === 0) {
|
||||
res
|
||||
.writeHead(400, { "content-type": "text/plain" })
|
||||
.end("code missing or invalid");
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await setup.createAppFromCode(code, {
|
||||
// @ts-expect-error
|
||||
request: app.state.request,
|
||||
});
|
||||
} else {
|
||||
|
||||
// If using glitch, restart the app
|
||||
if (process.env.PROJECT_DOMAIN) {
|
||||
exec("refresh", (error) => {
|
||||
if (error) {
|
||||
app.log.error(error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
printRestartMessage(app);
|
||||
}
|
||||
|
||||
res
|
||||
.writeHead(302, {
|
||||
"content-type": "text/plain",
|
||||
location: `${response}/installations/new`,
|
||||
})
|
||||
.end(`Found. Redirecting to ${response}/installations/new`);
|
||||
},
|
||||
);
|
||||
|
||||
const { WEBHOOK_PROXY_URL, GHE_HOST } = process.env;
|
||||
const GH_HOST = `https://${GHE_HOST ?? "github.com"}`;
|
||||
|
||||
const importViewRendered = importView({
|
||||
name: pkg.name,
|
||||
WEBHOOK_PROXY_URL,
|
||||
GH_HOST,
|
||||
});
|
||||
|
||||
route.get(
|
||||
"/probot/import",
|
||||
(_req: IncomingMessage, res: ServerResponse) => {
|
||||
res
|
||||
.writeHead(200, {
|
||||
"content-type": "text/html",
|
||||
})
|
||||
.end(importViewRendered);
|
||||
},
|
||||
);
|
||||
|
||||
route.post(
|
||||
"/probot/import",
|
||||
express.json(),
|
||||
(req: IncomingMessage, res: ServerResponse) => {
|
||||
const { appId, pem, webhook_secret } = (req as unknown as { body: any })
|
||||
.body;
|
||||
if (!appId || !pem || !webhook_secret) {
|
||||
res
|
||||
.writeHead(400, {
|
||||
"content-type": "text/plain",
|
||||
})
|
||||
.end("appId and/or pem and/or webhook_secret missing");
|
||||
return;
|
||||
}
|
||||
updateDotenv({
|
||||
APP_ID: appId,
|
||||
PRIVATE_KEY: `"${pem}"`,
|
||||
WEBHOOK_SECRET: webhook_secret,
|
||||
});
|
||||
res.end();
|
||||
printRestartMessage(app);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
res.redirect(`${response}/installations/new`);
|
||||
});
|
||||
const successViewRendered = successView({ name: pkg.name });
|
||||
|
||||
route.get("/probot/import", async (_req, res) => {
|
||||
const { WEBHOOK_PROXY_URL, GHE_HOST } = process.env;
|
||||
const GH_HOST = `https://${GHE_HOST ?? "github.com"}`;
|
||||
res.render("import.handlebars", { WEBHOOK_PROXY_URL, GH_HOST });
|
||||
});
|
||||
route.get(
|
||||
"/probot/success",
|
||||
(_req: IncomingMessage, res: ServerResponse) => {
|
||||
res
|
||||
.writeHead(200, { "content-type": "text/html" })
|
||||
.end(successViewRendered);
|
||||
},
|
||||
);
|
||||
|
||||
route.post("/probot/import", bodyParser.json(), async (req, res) => {
|
||||
const { appId, pem, webhook_secret } = req.body;
|
||||
if (!appId || !pem || !webhook_secret) {
|
||||
res.status(400).send("appId and/or pem and/or webhook_secret missing");
|
||||
return;
|
||||
}
|
||||
updateDotenv({
|
||||
APP_ID: appId,
|
||||
PRIVATE_KEY: `"${pem}"`,
|
||||
WEBHOOK_SECRET: webhook_secret,
|
||||
});
|
||||
res.end();
|
||||
printRestartMessage(app);
|
||||
});
|
||||
|
||||
route.get("/probot/success", async (req, res) => {
|
||||
res.render("success.handlebars");
|
||||
});
|
||||
|
||||
route.get("/", (req, res, next) => res.redirect("/probot"));
|
||||
route.get("/", (_req, res: ServerResponse) =>
|
||||
res
|
||||
.writeHead(302, { "content-type": "text/plain", location: `/probot` })
|
||||
.end(`Found. Redirecting to /probot`),
|
||||
);
|
||||
};
|
||||
|
||||
function printWelcomeMessage(
|
||||
app: Probot,
|
||||
host: string | undefined,
|
||||
port: number | undefined
|
||||
port: number | undefined,
|
||||
) {
|
||||
// use glitch env to get correct domain welcome message
|
||||
// https://glitch.com/help/project/
|
||||
|
@ -127,11 +195,16 @@ function printRestartMessage(app: Probot) {
|
|||
app.log.info("");
|
||||
}
|
||||
|
||||
function getBaseUrl(req: Request): string {
|
||||
const protocols = req.headers["x-forwarded-proto"] || req.protocol;
|
||||
function getBaseUrl(req: IncomingMessage): string {
|
||||
const protocols =
|
||||
req.headers["x-forwarded-proto"] ||
|
||||
// @ts-expect-error based on the functionality of express
|
||||
req.socket?.encrypted
|
||||
? "https"
|
||||
: "http";
|
||||
const protocol =
|
||||
typeof protocols === "string" ? protocols.split(",")[0] : protocols[0];
|
||||
const host = req.headers["x-forwarded-host"] || req.get("host");
|
||||
const host = req.headers["x-forwarded-host"] || req.headers.host;
|
||||
const baseUrl = `${protocol}://${host}`;
|
||||
return baseUrl;
|
||||
}
|
||||
|
|
15
src/auth.ts
15
src/auth.ts
|
@ -1,8 +1,6 @@
|
|||
import type { Logger } from "pino";
|
||||
|
||||
import { getAuthenticatedOctokit } from "./octokit/get-authenticated-octokit";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import { State } from "./types";
|
||||
import { getAuthenticatedOctokit } from "./octokit/get-authenticated-octokit.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import type { State } from "./types.js";
|
||||
|
||||
/**
|
||||
* Authenticate and get a GitHub client that can be used to make API calls.
|
||||
|
@ -20,6 +18,7 @@ import { State } from "./types";
|
|||
* });
|
||||
* };
|
||||
* ```
|
||||
* @param state - Probot application instance state, which is used to persist
|
||||
*
|
||||
* @param id - ID of the installation, which can be extracted from
|
||||
* `context.payload.installation.id`. If called without this parameter, the
|
||||
|
@ -32,10 +31,6 @@ import { State } from "./types";
|
|||
export async function auth(
|
||||
state: State,
|
||||
installationId?: number,
|
||||
log?: Logger
|
||||
): Promise<InstanceType<typeof ProbotOctokit>> {
|
||||
return getAuthenticatedOctokit(
|
||||
Object.assign({}, state, log ? { log } : null),
|
||||
installationId
|
||||
);
|
||||
return getAuthenticatedOctokit(Object.assign({}, state), installationId);
|
||||
}
|
||||
|
|
|
@ -1,17 +1,18 @@
|
|||
// Usage: probot receive -e push -p path/to/payload app.js
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { randomUUID as uuidv4 } from "node:crypto";
|
||||
|
||||
import express, { Router } from "express";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
dotenvConfig();
|
||||
|
||||
require("dotenv").config();
|
||||
|
||||
import path from "path";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import program from "commander";
|
||||
import { program } from "commander";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import { getLog } from "../helpers/get-log";
|
||||
import { getLog } from "../helpers/get-log.js";
|
||||
|
||||
import { ApplicationFunctionOptions, Probot } from "../";
|
||||
import { resolveAppFunction } from "../helpers/resolve-app-function";
|
||||
import { Probot, type ApplicationFunctionOptions } from "../index.js";
|
||||
import { resolveAppFunction } from "../helpers/resolve-app-function.js";
|
||||
|
||||
async function main() {
|
||||
program
|
||||
|
@ -19,48 +20,48 @@ async function main() {
|
|||
.option(
|
||||
"-e, --event <event-name>",
|
||||
"Event name",
|
||||
process.env.GITHUB_EVENT_NAME
|
||||
process.env.GITHUB_EVENT_NAME,
|
||||
)
|
||||
.option(
|
||||
"-p, --payload-path <payload-path>",
|
||||
"Path to the event payload",
|
||||
process.env.GITHUB_EVENT_PATH
|
||||
process.env.GITHUB_EVENT_PATH,
|
||||
)
|
||||
.option(
|
||||
"-t, --token <access-token>",
|
||||
"Access token",
|
||||
process.env.GITHUB_TOKEN
|
||||
process.env.GITHUB_TOKEN,
|
||||
)
|
||||
.option("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||
.option(
|
||||
"-P, --private-key <file>",
|
||||
"Path to private key file (.pem) for the GitHub App",
|
||||
process.env.PRIVATE_KEY_PATH
|
||||
process.env.PRIVATE_KEY_PATH,
|
||||
)
|
||||
.option(
|
||||
"-L, --log-level <level>",
|
||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||
process.env.LOG_LEVEL
|
||||
process.env.LOG_LEVEL,
|
||||
)
|
||||
.option(
|
||||
"--log-format <format>",
|
||||
'One of: "pretty", "json"',
|
||||
process.env.LOG_LEVEL || "pretty"
|
||||
process.env.LOG_LEVEL || "pretty",
|
||||
)
|
||||
.option(
|
||||
"--log-level-in-string",
|
||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
||||
process.env.LOG_LEVEL_IN_STRING === "true"
|
||||
process.env.LOG_LEVEL_IN_STRING === "true",
|
||||
)
|
||||
.option(
|
||||
"--log-message-key",
|
||||
"Set to the string key for the 'message' in the log JSON object",
|
||||
process.env.LOG_MESSAGE_KEY || "msg"
|
||||
process.env.LOG_MESSAGE_KEY || "msg",
|
||||
)
|
||||
.option(
|
||||
"--sentry-dsn <dsn>",
|
||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||
process.env.SENTRY_DSN
|
||||
process.env.SENTRY_DSN,
|
||||
)
|
||||
.option(
|
||||
"--base-url <url>",
|
||||
|
@ -69,38 +70,51 @@ async function main() {
|
|||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||
process.env.GHE_HOST
|
||||
}/api/v3`
|
||||
: "https://api.github.com"
|
||||
: "https://api.github.com",
|
||||
)
|
||||
.parse(process.argv);
|
||||
|
||||
const githubToken = program.token;
|
||||
const {
|
||||
app: appId,
|
||||
baseUrl,
|
||||
token: githubToken,
|
||||
event,
|
||||
payloadPath,
|
||||
logLevel,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
} = program.opts();
|
||||
|
||||
if (!program.event || !program.payloadPath) {
|
||||
if (!event || !payloadPath) {
|
||||
program.help();
|
||||
}
|
||||
|
||||
const privateKey = getPrivateKey();
|
||||
if (!githubToken && (!program.app || !privateKey)) {
|
||||
if (!githubToken && (!appId || !privateKey)) {
|
||||
console.warn(
|
||||
"No token specified and no certificate found, which means you will not be able to do authenticated requests to GitHub"
|
||||
"No token specified and no certificate found, which means you will not be able to do authenticated requests to GitHub",
|
||||
);
|
||||
}
|
||||
|
||||
const payload = require(path.resolve(program.payloadPath));
|
||||
const payload = JSON.parse(
|
||||
fs.readFileSync(path.resolve(payloadPath), "utf8"),
|
||||
);
|
||||
const log = getLog({
|
||||
level: program.logLevel,
|
||||
logFormat: program.logFormat,
|
||||
logLevelInString: program.logLevelInString,
|
||||
logMessageKey: program.logMessageKey,
|
||||
sentryDsn: program.sentryDsn,
|
||||
level: logLevel,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
});
|
||||
|
||||
const probot = new Probot({
|
||||
appId: program.app,
|
||||
appId,
|
||||
privateKey: String(privateKey),
|
||||
githubToken: githubToken,
|
||||
log,
|
||||
baseUrl: program.baseUrl,
|
||||
baseUrl: baseUrl,
|
||||
});
|
||||
|
||||
const expressApp = express();
|
||||
|
@ -113,12 +127,12 @@ async function main() {
|
|||
};
|
||||
|
||||
const appFn = await resolveAppFunction(
|
||||
path.resolve(process.cwd(), program.args[0])
|
||||
path.resolve(process.cwd(), program.args[0]),
|
||||
);
|
||||
await probot.load(appFn, options);
|
||||
|
||||
probot.log.debug("Receiving event", program.event);
|
||||
probot.receive({ name: program.event, payload, id: uuidv4() }).catch(() => {
|
||||
probot.log.debug("Receiving event", event);
|
||||
probot.receive({ name: event, payload, id: uuidv4() }).catch(() => {
|
||||
// Process must exist non-zero to indicate that the action failed to run
|
||||
process.exit(1);
|
||||
});
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import { run } from "../";
|
||||
import { run } from "../index.js";
|
||||
|
||||
run(process.argv);
|
||||
|
|
|
@ -1,24 +1,30 @@
|
|||
import semver from "semver";
|
||||
import program from "commander";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
require("dotenv").config();
|
||||
import { program } from "commander";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
import { isSupportedNodeVersion } from "../helpers/is-supported-node-version.js";
|
||||
import { loadPackageJson } from "../helpers/load-package-json.js";
|
||||
|
||||
const pkg = require("../../package");
|
||||
/*import { dirname } from 'path';
|
||||
import { fileURLToPath } from 'url';*/
|
||||
|
||||
if (!semver.satisfies(process.version, pkg.engines.node)) {
|
||||
console.log(
|
||||
`Node.js version ${pkg.engines.node} is required. You have ${process.version}.`
|
||||
);
|
||||
dotenvConfig();
|
||||
|
||||
//const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const pkg = loadPackageJson(resolve(__dirname, "package.json"));
|
||||
|
||||
if (!isSupportedNodeVersion()) {
|
||||
console.log(`Node.js version 18 is required. You have ${process.version}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
program
|
||||
.version(pkg.version)
|
||||
.version(pkg.version || "0.0.0-dev")
|
||||
.usage("<command> [options]")
|
||||
.command("run", "run the bot")
|
||||
.command("receive", "Receive a single event and payload")
|
||||
.on("command:*", (cmd) => {
|
||||
if (!program.commands.find((c) => c._name == cmd[0])) {
|
||||
if (!program.commands.find((c) => c.name() == cmd[0])) {
|
||||
console.error(`Invalid command: ${program.args.join(" ")}\n`);
|
||||
program.outputHelp();
|
||||
process.exit(1);
|
||||
|
|
|
@ -1,65 +1,65 @@
|
|||
import program from "commander";
|
||||
import { program } from "commander";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import { Options as PinoOptions } from "@probot/pino";
|
||||
import type { Options as PinoOptions } from "@probot/pino";
|
||||
|
||||
import { Options } from "../types";
|
||||
import type { Options } from "../types.js";
|
||||
|
||||
export function readCliOptions(
|
||||
argv: string[]
|
||||
argv: string[],
|
||||
): Options & PinoOptions & { args: string[] } {
|
||||
program
|
||||
.usage("[options] <apps...>")
|
||||
.option(
|
||||
"-p, --port <n>",
|
||||
"Port to start the server on",
|
||||
String(process.env.PORT || 3000)
|
||||
String(process.env.PORT || 3000),
|
||||
)
|
||||
.option("-H --host <host>", "Host to start the server on", process.env.HOST)
|
||||
.option(
|
||||
"-W, --webhook-proxy <url>",
|
||||
"URL of the webhook proxy service.`",
|
||||
process.env.WEBHOOK_PROXY_URL
|
||||
process.env.WEBHOOK_PROXY_URL,
|
||||
)
|
||||
.option(
|
||||
"-w, --webhook-path <path>",
|
||||
"URL path which receives webhooks. Ex: `/webhook`",
|
||||
process.env.WEBHOOK_PATH
|
||||
process.env.WEBHOOK_PATH,
|
||||
)
|
||||
.option("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||
.option(
|
||||
"-s, --secret <secret>",
|
||||
"Webhook secret of the GitHub App",
|
||||
process.env.WEBHOOK_SECRET
|
||||
process.env.WEBHOOK_SECRET,
|
||||
)
|
||||
.option(
|
||||
"-P, --private-key <file>",
|
||||
"Path to private key file (.pem) for the GitHub App",
|
||||
process.env.PRIVATE_KEY_PATH
|
||||
process.env.PRIVATE_KEY_PATH,
|
||||
)
|
||||
.option(
|
||||
"-L, --log-level <level>",
|
||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||
process.env.LOG_LEVEL || "info"
|
||||
process.env.LOG_LEVEL || "info",
|
||||
)
|
||||
.option(
|
||||
"--log-format <format>",
|
||||
'One of: "pretty", "json"',
|
||||
process.env.LOG_FORMAT
|
||||
process.env.LOG_FORMAT,
|
||||
)
|
||||
.option(
|
||||
"--log-level-in-string",
|
||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
||||
process.env.LOG_LEVEL_IN_STRING === "true"
|
||||
process.env.LOG_LEVEL_IN_STRING === "true",
|
||||
)
|
||||
.option(
|
||||
"--sentry-dsn <dsn>",
|
||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||
process.env.SENTRY_DSN
|
||||
process.env.SENTRY_DSN,
|
||||
)
|
||||
.option(
|
||||
"--redis-url <url>",
|
||||
'Set to a "redis://" url in order to enable cluster support for request throttling. Example: "redis://:secret@redis-123.redislabs.com:12345/0"',
|
||||
process.env.REDIS_URL
|
||||
process.env.REDIS_URL,
|
||||
)
|
||||
.option(
|
||||
"--base-url <url>",
|
||||
|
@ -68,7 +68,7 @@ export function readCliOptions(
|
|||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||
process.env.GHE_HOST
|
||||
}/api/v3`
|
||||
: "https://api.github.com"
|
||||
: "https://api.github.com",
|
||||
)
|
||||
.parse(argv);
|
||||
|
||||
|
@ -77,12 +77,13 @@ export function readCliOptions(
|
|||
privateKey: privateKeyPath,
|
||||
redisUrl,
|
||||
...options
|
||||
} = program;
|
||||
} = program.opts();
|
||||
|
||||
return {
|
||||
privateKey: getPrivateKey({ filepath: privateKeyPath }) || undefined,
|
||||
appId,
|
||||
redisConfig: redisUrl,
|
||||
args: program.args,
|
||||
...options,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,13 +1,17 @@
|
|||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
import { Options as PinoOptions, LogLevel } from "@probot/pino";
|
||||
import type { Options as PinoOptions, LogLevel } from "@probot/pino";
|
||||
|
||||
export function readEnvOptions(
|
||||
env: Record<string, string | undefined> = process.env
|
||||
) {
|
||||
export function readEnvOptions(env = process.env) {
|
||||
const privateKey = getPrivateKey({ env });
|
||||
|
||||
const logFormat =
|
||||
env.LOG_FORMAT || (env.NODE_ENV === "production" ? "json" : "pretty");
|
||||
const logFormat: PinoOptions["logFormat"] =
|
||||
env.LOG_FORMAT && env.LOG_FORMAT.length !== 0
|
||||
? env.LOG_FORMAT === "pretty"
|
||||
? "pretty"
|
||||
: "json"
|
||||
: env.NODE_ENV === "production"
|
||||
? "json"
|
||||
: "pretty";
|
||||
|
||||
return {
|
||||
args: [],
|
||||
|
@ -19,7 +23,7 @@ export function readEnvOptions(
|
|||
webhookPath: env.WEBHOOK_PATH,
|
||||
webhookProxy: env.WEBHOOK_PROXY_URL,
|
||||
logLevel: env.LOG_LEVEL as LogLevel,
|
||||
logFormat: logFormat as PinoOptions["logFormat"],
|
||||
logFormat: logFormat,
|
||||
logLevelInString: env.LOG_LEVEL_IN_STRING === "true",
|
||||
logMessageKey: env.LOG_MESSAGE_KEY,
|
||||
sentryDsn: env.SENTRY_DSN,
|
||||
|
|
|
@ -1,14 +1,12 @@
|
|||
import path from "path";
|
||||
|
||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
import path from "node:path";
|
||||
import merge from "deepmerge";
|
||||
|
||||
import type {
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
EmitterWebhookEventName as WebhookEvents,
|
||||
} from "@octokit/webhooks";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import { aliasLog } from "./helpers/alias-log";
|
||||
import { DeprecatedLogger } from "./types";
|
||||
import { EmitterWebhookEventName as WebhookEvents } from "@octokit/webhooks/dist-types/types";
|
||||
import type { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
|
||||
export type MergeOptions = merge.Options;
|
||||
|
||||
|
@ -31,12 +29,12 @@ type RepoIssueNumberType<T extends WebhookEvents> =
|
|||
WebhookEvent<T>["payload"] extends { issue: { number: number } }
|
||||
? number
|
||||
: never | WebhookEvent<T>["payload"] extends {
|
||||
pull_request: { number: number };
|
||||
}
|
||||
? number
|
||||
: never | WebhookEvent<T>["payload"] extends { number: number }
|
||||
? number
|
||||
: never;
|
||||
pull_request: { number: number };
|
||||
}
|
||||
? number
|
||||
: never | WebhookEvent<T>["payload"] extends { number: number }
|
||||
? number
|
||||
: never;
|
||||
|
||||
/** Context.repo return type */
|
||||
type RepoResultType<E extends WebhookEvents> = {
|
||||
|
@ -65,20 +63,16 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
public id: string;
|
||||
public payload: WebhookEvent<E>["payload"];
|
||||
|
||||
public octokit: InstanceType<typeof ProbotOctokit>;
|
||||
public log: DeprecatedLogger;
|
||||
public octokit: ProbotOctokit;
|
||||
public log: Logger;
|
||||
|
||||
constructor(
|
||||
event: WebhookEvent<E>,
|
||||
octokit: InstanceType<typeof ProbotOctokit>,
|
||||
log: Logger
|
||||
) {
|
||||
constructor(event: WebhookEvent<E>, octokit: ProbotOctokit, log: Logger) {
|
||||
this.name = event.name;
|
||||
this.id = event.id;
|
||||
this.payload = event.payload;
|
||||
|
||||
this.octokit = octokit;
|
||||
this.log = aliasLog(log);
|
||||
this.log = log;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -94,12 +88,12 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
*
|
||||
*/
|
||||
public repo<T>(object?: T): RepoResultType<E> & T {
|
||||
// @ts-ignore `repository` is not always present in this.payload
|
||||
// @ts-expect-error `repository` is not always present in this.payload
|
||||
const repo = this.payload.repository;
|
||||
|
||||
if (!repo) {
|
||||
throw new Error(
|
||||
"context.repo() is not supported for this webhook event."
|
||||
"context.repo() is not supported for this webhook event.",
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -108,7 +102,7 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
owner: repo.owner.login,
|
||||
repo: repo.name,
|
||||
},
|
||||
object
|
||||
object,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -125,16 +119,16 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
* @param object - Params to be merged with the issue params.
|
||||
*/
|
||||
public issue<T>(
|
||||
object?: T
|
||||
object?: T,
|
||||
): RepoResultType<E> & { issue_number: RepoIssueNumberType<E> } & T {
|
||||
return Object.assign(
|
||||
{
|
||||
issue_number:
|
||||
// @ts-ignore - this.payload may not have `issue` or `pull_request` keys
|
||||
// @ts-expect-error - this.payload may not have `issue` or `pull_request` keys
|
||||
(this.payload.issue || this.payload.pull_request || this.payload)
|
||||
.number,
|
||||
},
|
||||
this.repo(object)
|
||||
this.repo(object),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -151,15 +145,15 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
* @param object - Params to be merged with the pull request params.
|
||||
*/
|
||||
public pullRequest<T>(
|
||||
object?: T
|
||||
object?: T,
|
||||
): RepoResultType<E> & { pull_number: RepoIssueNumberType<E> } & T {
|
||||
const payload = this.payload;
|
||||
return Object.assign(
|
||||
{
|
||||
// @ts-ignore - this.payload may not have `issue` or `pull_request` keys
|
||||
// @ts-expect-error - this.payload may not have `issue` or `pull_request` keys
|
||||
pull_number: (payload.issue || payload.pull_request || payload).number,
|
||||
},
|
||||
this.repo(object)
|
||||
this.repo(object),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -225,25 +219,25 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
|||
public async config<T>(
|
||||
fileName: string,
|
||||
defaultConfig?: T,
|
||||
deepMergeOptions?: MergeOptions
|
||||
deepMergeOptions?: MergeOptions,
|
||||
): Promise<T | null> {
|
||||
const params = this.repo({
|
||||
path: path.posix.join(".github", fileName),
|
||||
defaults(configs: object[]) {
|
||||
const result = merge.all(
|
||||
[defaultConfig || {}, ...configs],
|
||||
deepMergeOptions
|
||||
deepMergeOptions,
|
||||
);
|
||||
|
||||
return result;
|
||||
},
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
// @ts-expect-error
|
||||
const { config, files } = await this.octokit.config.get(params);
|
||||
|
||||
// if no default config is set, and no config files are found, return null
|
||||
if (!defaultConfig && !files.find((file: any) => file.config !== null)) {
|
||||
if (!defaultConfig && !files.find((file) => file.config !== null)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,16 +1,17 @@
|
|||
import { RequestListener } from "http";
|
||||
import { createNodeMiddleware as createWebbhooksMiddleware } from "@octokit/webhooks";
|
||||
import type { RequestListener } from "http";
|
||||
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||
|
||||
import { ApplicationFunction } from "./types";
|
||||
import { MiddlewareOptions } from "./types";
|
||||
import type { ApplicationFunction, MiddlewareOptions } from "./types.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
import { createProbot } from "./create-probot.js";
|
||||
|
||||
export function createNodeMiddleware(
|
||||
appFn: ApplicationFunction,
|
||||
{ probot, webhooksPath }: MiddlewareOptions
|
||||
{ probot = createProbot(), webhooksPath } = {} as MiddlewareOptions,
|
||||
): RequestListener {
|
||||
probot.load(appFn);
|
||||
|
||||
return createWebbhooksMiddleware(probot.webhooks, {
|
||||
path: webhooksPath || "/",
|
||||
return createWebhooksMiddleware(probot.webhooks, {
|
||||
path: webhooksPath || probot.webhookPath || defaultWebhooksPath,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,24 +1,26 @@
|
|||
import { LogLevel, Options as PinoOptions } from "@probot/pino";
|
||||
import type { LogLevel, Options as PinoOptions } from "@probot/pino";
|
||||
import { getPrivateKey } from "@probot/get-private-key";
|
||||
|
||||
import { getLog, GetLogOptions } from "./helpers/get-log";
|
||||
import { Options } from "./types";
|
||||
import { Probot } from "./probot";
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import type { Options } from "./types.js";
|
||||
import { Probot } from "./probot.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
|
||||
type CreateProbotOptions = {
|
||||
overrides?: Options;
|
||||
defaults?: Options;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
env?: Partial<NodeJS.ProcessEnv>;
|
||||
};
|
||||
|
||||
const DEFAULTS = {
|
||||
const DEFAULTS: Partial<NodeJS.ProcessEnv> = {
|
||||
APP_ID: "",
|
||||
WEBHOOK_SECRET: "",
|
||||
WEBHOOK_PATH: defaultWebhooksPath,
|
||||
GHE_HOST: "",
|
||||
GHE_PROTOCOL: "",
|
||||
LOG_FORMAT: "",
|
||||
GHE_PROTOCOL: "https",
|
||||
LOG_FORMAT: undefined,
|
||||
LOG_LEVEL: "warn",
|
||||
LOG_LEVEL_IN_STRING: "",
|
||||
LOG_LEVEL_IN_STRING: "false",
|
||||
LOG_MESSAGE_KEY: "msg",
|
||||
REDIS_URL: "",
|
||||
SENTRY_DSN: "",
|
||||
|
@ -47,6 +49,7 @@ export function createProbot({
|
|||
privateKey: (privateKey && privateKey.toString()) || undefined,
|
||||
secret: envWithDefaults.WEBHOOK_SECRET,
|
||||
redisConfig: envWithDefaults.REDIS_URL,
|
||||
webhookPath: envWithDefaults.WEBHOOK_PATH,
|
||||
baseUrl: envWithDefaults.GHE_HOST
|
||||
? `${envWithDefaults.GHE_PROTOCOL || "https"}://${
|
||||
envWithDefaults.GHE_HOST
|
||||
|
@ -60,15 +63,13 @@ export function createProbot({
|
|||
...overrides,
|
||||
};
|
||||
|
||||
const logOptions: GetLogOptions = {
|
||||
const log = getLog({
|
||||
level: probotOptions.logLevel,
|
||||
logFormat: envWithDefaults.LOG_FORMAT as PinoOptions["logFormat"],
|
||||
logLevelInString: envWithDefaults.LOG_LEVEL_IN_STRING === "true",
|
||||
logMessageKey: envWithDefaults.LOG_MESSAGE_KEY,
|
||||
sentryDsn: envWithDefaults.SENTRY_DSN,
|
||||
};
|
||||
|
||||
const log = getLog(logOptions).child({ name: "server" });
|
||||
}).child({ name: "server" });
|
||||
|
||||
return new Probot({
|
||||
log: log.child({ name: "probot" }),
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import type { Logger } from "pino";
|
||||
|
||||
import type { DeprecatedLogger } from "../types";
|
||||
|
||||
/**
|
||||
* `probot.log()`, `app.log()` and `context.log()` are aliasing `.log.info()`.
|
||||
* We will probably remove the aliasing in future.
|
||||
*/
|
||||
export function aliasLog(log: Logger): DeprecatedLogger {
|
||||
function logInfo() {
|
||||
// @ts-ignore
|
||||
log.info(...arguments);
|
||||
}
|
||||
|
||||
for (const key in log) {
|
||||
// @ts-ignore
|
||||
logInfo[key] =
|
||||
typeof log[key] === "function" ? log[key].bind(log) : log[key];
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return logInfo;
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
import type { Logger } from "pino";
|
||||
import {
|
||||
import type {
|
||||
WebhookError,
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
} from "@octokit/webhooks";
|
||||
|
||||
export function getErrorHandler(log: Logger) {
|
||||
return (error: Error) => {
|
||||
return (error: Error & { event?: WebhookEvent }) => {
|
||||
const errors = (
|
||||
error.name === "AggregateError" ? error : [error]
|
||||
) as WebhookError[];
|
||||
|
||||
const event = (error as any).event as WebhookEvent;
|
||||
const event = error.event;
|
||||
|
||||
for (const error of errors) {
|
||||
const errMessage = (error.message || "").toLowerCase();
|
||||
|
@ -18,7 +18,7 @@ export function getErrorHandler(log: Logger) {
|
|||
if (errMessage.includes("x-hub-signature-256")) {
|
||||
log.error(
|
||||
error,
|
||||
"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."
|
||||
"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
@ -26,7 +26,7 @@ export function getErrorHandler(log: Logger) {
|
|||
if (errMessage.includes("pem") || errMessage.includes("json web token")) {
|
||||
log.error(
|
||||
error,
|
||||
"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary."
|
||||
"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary.",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
@ -34,7 +34,7 @@ export function getErrorHandler(log: Logger) {
|
|||
log
|
||||
.child({
|
||||
name: "event",
|
||||
id: event ? event.id : undefined,
|
||||
id: event?.id,
|
||||
})
|
||||
.error(error);
|
||||
}
|
||||
|
|
|
@ -14,8 +14,10 @@
|
|||
* app.log.fatal("Goodbye, cruel world!");
|
||||
* ```
|
||||
*/
|
||||
import pino, { Logger, LoggerOptions } from "pino";
|
||||
import { getTransformStream, Options, LogLevel } from "@probot/pino";
|
||||
import { pino } from "pino";
|
||||
import type { Logger, LoggerOptions } from "pino";
|
||||
import { getTransformStream, type Options, type LogLevel } from "@probot/pino";
|
||||
import { rebindLog } from "./rebind-log";
|
||||
|
||||
export type GetLogOptions = {
|
||||
level?: LogLevel;
|
||||
|
@ -31,9 +33,7 @@ export function getLog(options: GetLogOptions = {}): Logger {
|
|||
messageKey: logMessageKey || "msg",
|
||||
};
|
||||
const transform = getTransformStream(getTransformStreamOptions);
|
||||
// @ts-ignore TODO: check out what's wrong here
|
||||
transform.pipe(pino.destination(1));
|
||||
const log = pino(pinoOptions, transform);
|
||||
|
||||
return log;
|
||||
return rebindLog(pino(pinoOptions, transform));
|
||||
}
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export function isSupportedNodeVersion(nodeVersion = process.versions.node) {
|
||||
return Number(nodeVersion.split(".", 10)[0]) >= 18;
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import type { PackageJson } from "../types.js";
|
||||
|
||||
export function loadPackageJson(
|
||||
filepath = path.join(process.cwd(), "package.json"),
|
||||
): PackageJson {
|
||||
let pkgContent;
|
||||
try {
|
||||
pkgContent = fs.readFileSync(filepath, "utf8");
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const pkg = pkgContent && JSON.parse(pkgContent);
|
||||
if (pkg && typeof pkg === "object") {
|
||||
return pkg;
|
||||
}
|
||||
return {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
import type { Logger } from "pino";
|
||||
|
||||
const kIsBound = Symbol("is-bound");
|
||||
|
||||
export function rebindLog(log: Logger): Logger {
|
||||
// @ts-ignore
|
||||
if (log[kIsBound]) return log;
|
||||
for (const key in log) {
|
||||
// @ts-ignore
|
||||
if (typeof log[key] !== "function") continue;
|
||||
// @ts-ignore
|
||||
log[key] = log[key].bind(log);
|
||||
}
|
||||
// @ts-ignore
|
||||
log[kIsBound] = true;
|
||||
return log;
|
||||
}
|
|
@ -1,19 +1,25 @@
|
|||
import { sync } from "resolve";
|
||||
import resolveModule from "resolve";
|
||||
|
||||
const defaultOptions: ResolveOptions = {};
|
||||
|
||||
export const resolveAppFunction = async (
|
||||
appFnId: string,
|
||||
opts?: ResolveOptions
|
||||
opts?: ResolveOptions,
|
||||
) => {
|
||||
opts = opts || defaultOptions;
|
||||
// These are mostly to ease testing
|
||||
const basedir = opts.basedir || process.cwd();
|
||||
const resolver: Resolver = opts.resolver || sync;
|
||||
const resolver: Resolver = opts.resolver || resolveModule.sync;
|
||||
const appFnPath = resolver(appFnId, { basedir });
|
||||
const mod = await import(appFnPath);
|
||||
// Note: This needs "esModuleInterop" to be set to "true" in "tsconfig.json"
|
||||
return mod.default;
|
||||
// On windows, an absolute path may start with a drive letter, e.g. C:/path/to/file.js
|
||||
// This can be interpreted as a protocol, so ensure it's prefixed with file://
|
||||
const appFnPathWithFileProtocol = appFnPath.replace(
|
||||
/^([a-zA-Z]:)/,
|
||||
"file://$1",
|
||||
);
|
||||
const { default: mod } = await import(appFnPathWithFileProtocol);
|
||||
// mod.default gets exported by transpiled TypeScript code
|
||||
return mod.__esModule && mod.default ? mod.default : mod;
|
||||
};
|
||||
|
||||
export type Resolver = (appFnId: string, opts: { basedir: string }) => string;
|
||||
|
|
|
@ -2,28 +2,30 @@ import EventSource from "eventsource";
|
|||
|
||||
import type { Logger } from "pino";
|
||||
|
||||
export const createWebhookProxy = (
|
||||
opts: WebhookProxyOptions
|
||||
): EventSource | undefined => {
|
||||
export const createWebhookProxy = async (
|
||||
opts: WebhookProxyOptions,
|
||||
): Promise<EventSource | undefined> => {
|
||||
try {
|
||||
const SmeeClient = require("smee-client");
|
||||
const SmeeClient = (await import("smee-client")).default;
|
||||
const smee = new SmeeClient({
|
||||
logger: opts.logger,
|
||||
source: opts.url,
|
||||
target: `http://localhost:${opts.port}${opts.path}`,
|
||||
fetch: opts.fetch,
|
||||
});
|
||||
return smee.start();
|
||||
} catch (error) {
|
||||
opts.logger.warn(
|
||||
"Run `npm install --save-dev smee-client` to proxy webhooks to localhost."
|
||||
"Run `npm install --save-dev smee-client` to proxy webhooks to localhost.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export interface WebhookProxyOptions {
|
||||
url?: string;
|
||||
url: string;
|
||||
port?: number;
|
||||
path?: string;
|
||||
logger: Logger;
|
||||
fetch?: Function;
|
||||
}
|
||||
|
|
170
src/index.ts
170
src/index.ts
|
@ -1,28 +1,154 @@
|
|||
import { Logger } from "pino";
|
||||
export type { Logger } from "pino";
|
||||
|
||||
import { Context } from "./context";
|
||||
import {
|
||||
export { Context } from "./context.js";
|
||||
|
||||
export { Probot } from "./probot.js";
|
||||
export { Server } from "./server/server.js";
|
||||
export { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
export { run } from "./run.js";
|
||||
export { createNodeMiddleware } from "./create-node-middleware.js";
|
||||
export { createProbot } from "./create-probot.js";
|
||||
|
||||
/** NOTE: exported types might change at any point in time */
|
||||
export type {
|
||||
Options,
|
||||
ApplicationFunction,
|
||||
ApplicationFunctionOptions,
|
||||
} from "./types";
|
||||
import { Probot } from "./probot";
|
||||
import { Server } from "./server/server";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import { run } from "./run";
|
||||
import { createNodeMiddleware } from "./create-node-middleware";
|
||||
import { createProbot } from "./create-probot";
|
||||
} from "./types.js";
|
||||
|
||||
export {
|
||||
Logger,
|
||||
Context,
|
||||
ProbotOctokit,
|
||||
run,
|
||||
Probot,
|
||||
Server,
|
||||
createNodeMiddleware,
|
||||
createProbot,
|
||||
};
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
/**
|
||||
* The App ID assigned to your GitHub App.
|
||||
* @example '1234'
|
||||
*/
|
||||
APP_ID?: string;
|
||||
|
||||
/** NOTE: exported types might change at any point in time */
|
||||
export { Options, ApplicationFunction, ApplicationFunctionOptions };
|
||||
/**
|
||||
* By default, logs are formatted for readability in development. You can
|
||||
* set this to `json` in order to disable the formatting.
|
||||
*/
|
||||
LOG_FORMAT?: "json" | "pretty";
|
||||
|
||||
/**
|
||||
* The verbosity of logs to show when running your app, which can be
|
||||
* `fatal`, `error`, `warn`, `info`, `debug`, `trace` or `silent`.
|
||||
* @default 'info'
|
||||
*/
|
||||
LOG_LEVEL?:
|
||||
| "trace"
|
||||
| "debug"
|
||||
| "info"
|
||||
| "warn"
|
||||
| "error"
|
||||
| "fatal"
|
||||
| "silent";
|
||||
|
||||
/**
|
||||
* By default, when using the `json` format, the level printed in the log
|
||||
* records is an int (`10`, `20`, ..). This option tells the logger to
|
||||
* print level as a string: `{"level": "info"}`. Default `false`
|
||||
*/
|
||||
LOG_LEVEL_IN_STRING?: "true" | "false";
|
||||
|
||||
/**
|
||||
* Only relevant when `LOG_FORMAT` is set to `json`. Sets the json key for the log message.
|
||||
* @default 'msg'
|
||||
*/
|
||||
LOG_MESSAGE_KEY?: string;
|
||||
|
||||
/**
|
||||
* The organization where you want to register the app in the app
|
||||
* creation manifest flow. If set, the app is registered for an
|
||||
* organization
|
||||
* (https://github.com/organizations/ORGANIZATION/settings/apps/new), if
|
||||
* not set, the GitHub app would be registered for the user account
|
||||
* (https://github.com/settings/apps/new).
|
||||
*/
|
||||
GH_ORG?: string;
|
||||
|
||||
/**
|
||||
* The hostname of your GitHub Enterprise instance.
|
||||
* @example github.mycompany.com
|
||||
*/
|
||||
GHE_HOST?: string;
|
||||
|
||||
/**
|
||||
* The protocol of your GitHub Enterprise instance. Defaults to HTTPS.
|
||||
* Do not change unless you are certain.
|
||||
* @default 'https'
|
||||
*/
|
||||
GHE_PROTOCOL?: string;
|
||||
|
||||
/**
|
||||
* The contents of the private key for your GitHub App. If you're unable
|
||||
* to use multiline environment variables, use base64 encoding to
|
||||
* convert the key to a single line string. See the Deployment docs for
|
||||
* provider specific usage.
|
||||
*/
|
||||
PRIVATE_KEY?: string;
|
||||
|
||||
/**
|
||||
* When using the `PRIVATE_KEY_PATH` environment variable, set it to the
|
||||
* path of the `.pem` file that you downloaded from your GitHub App registration.
|
||||
* @example 'path/to/key.pem'
|
||||
*/
|
||||
PRIVATE_KEY_PATH?: string;
|
||||
/**
|
||||
* The port to start the local server on.
|
||||
* @default '3000'
|
||||
*/
|
||||
PORT?: string;
|
||||
|
||||
/**
|
||||
* The host to start the local server on.
|
||||
*/
|
||||
HOST?: string;
|
||||
|
||||
/**
|
||||
* Set to a `redis://` url as connection option for
|
||||
* [ioredis](https://github.com/luin/ioredis#connect-to-redis) in order
|
||||
* to enable
|
||||
* [cluster support for request throttling](https://github.com/octokit/plugin-throttling.js#clustering).
|
||||
* @example 'redis://:secret@redis-123.redislabs.com:12345/0'
|
||||
*/
|
||||
REDIS_URL?: string;
|
||||
|
||||
/**
|
||||
* Set to a [Sentry](https://sentry.io/) DSN to report all errors thrown
|
||||
* by your app.
|
||||
* @example 'https://1234abcd@sentry.io/12345'
|
||||
*/
|
||||
SENTRY_DSN?: string;
|
||||
|
||||
/**
|
||||
* The URL path which will receive webhooks.
|
||||
* @default '/api/github/webhooks'
|
||||
*/
|
||||
WEBHOOK_PATH?: string;
|
||||
|
||||
/**
|
||||
* Allows your local development environment to receive GitHub webhook
|
||||
* events. Go to https://smee.io/new to get started.
|
||||
* @example 'https://smee.io/your-custom-url'
|
||||
*/
|
||||
WEBHOOK_PROXY_URL?: string;
|
||||
|
||||
/**
|
||||
* **Required**
|
||||
* The webhook secret used when creating a GitHub App. 'development' is
|
||||
* used as a default, but the value in `.env` needs to match the value
|
||||
* configured in your App settings on GitHub. Note: GitHub marks this
|
||||
* value as optional, but for optimal security it's required for Probot
|
||||
* apps.
|
||||
*
|
||||
* @example 'development'
|
||||
* @default 'development'
|
||||
*/
|
||||
WEBHOOK_SECRET?: string;
|
||||
|
||||
NODE_ENV?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,73 +1,69 @@
|
|||
import fs from "fs";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import yaml from "js-yaml";
|
||||
import path from "path";
|
||||
import updateDotenv from "update-dotenv";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import type { RequestParameters } from "@octokit/types";
|
||||
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import { loadPackageJson } from "./helpers/load-package-json.js";
|
||||
import type { Env, Manifest, OctokitOptions, PackageJson } from "./types.js";
|
||||
|
||||
export class ManifestCreation {
|
||||
get pkg() {
|
||||
let pkg: any;
|
||||
try {
|
||||
pkg = require(path.join(process.cwd(), "package.json"));
|
||||
} catch (e) {
|
||||
pkg = {};
|
||||
}
|
||||
return pkg;
|
||||
return loadPackageJson();
|
||||
}
|
||||
|
||||
public async createWebhookChannel() {
|
||||
public async createWebhookChannel(): Promise<string | undefined> {
|
||||
try {
|
||||
// tslint:disable:no-var-requires
|
||||
const SmeeClient = require("smee-client");
|
||||
const SmeeClient = (await import("smee-client")).default;
|
||||
|
||||
const WEBHOOK_PROXY_URL = await SmeeClient.createChannel();
|
||||
await this.updateEnv({
|
||||
WEBHOOK_PROXY_URL: await SmeeClient.createChannel(),
|
||||
WEBHOOK_PROXY_URL,
|
||||
});
|
||||
return WEBHOOK_PROXY_URL;
|
||||
} catch (error) {
|
||||
// Smee is not available, so we'll just move on
|
||||
// tslint:disable:no-console
|
||||
console.warn("Unable to connect to smee.io, try restarting your server.");
|
||||
return void 0;
|
||||
}
|
||||
}
|
||||
|
||||
public getManifest(pkg: any, baseUrl: any) {
|
||||
let manifest: any = {};
|
||||
public getManifest(pkg: PackageJson, baseUrl: string) {
|
||||
let manifest: Partial<Manifest> = {};
|
||||
try {
|
||||
const file = fs.readFileSync(path.join(process.cwd(), "app.yml"), "utf8");
|
||||
manifest = yaml.safeLoad(file);
|
||||
manifest = yaml.load(file) as Manifest;
|
||||
} catch (error) {
|
||||
// App config does not exist, which is ok.
|
||||
// @ts-ignore - in theory error can be anything
|
||||
if (error.code !== "ENOENT") {
|
||||
if ((error as Error & { code?: string }).code !== "ENOENT") {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
const generatedManifest = JSON.stringify(
|
||||
Object.assign(
|
||||
{
|
||||
description: manifest.description || pkg.description,
|
||||
hook_attributes: {
|
||||
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`,
|
||||
},
|
||||
name: process.env.PROJECT_DOMAIN || manifest.name || pkg.name,
|
||||
public: manifest.public || true,
|
||||
redirect_url: `${baseUrl}/probot/setup`,
|
||||
// TODO: add setup url
|
||||
// setup_url:`${baseUrl}/probot/success`,
|
||||
url: manifest.url || pkg.homepage || pkg.repository,
|
||||
version: "v1",
|
||||
},
|
||||
manifest
|
||||
)
|
||||
);
|
||||
const generatedManifest = JSON.stringify({
|
||||
description: manifest.description || pkg.description,
|
||||
hook_attributes: {
|
||||
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`,
|
||||
},
|
||||
name: process.env.PROJECT_DOMAIN || manifest.name || pkg.name,
|
||||
public: manifest.public || true,
|
||||
redirect_url: `${baseUrl}/probot/setup`,
|
||||
// TODO: add setup url
|
||||
// setup_url:`${baseUrl}/probot/success`,
|
||||
url: manifest.url || pkg.homepage || pkg.repository,
|
||||
version: "v1",
|
||||
...manifest,
|
||||
});
|
||||
|
||||
return generatedManifest;
|
||||
}
|
||||
|
||||
public async createAppFromCode(code: any) {
|
||||
const octokit = new ProbotOctokit();
|
||||
const options: any = {
|
||||
public async createAppFromCode(code: string, probotOptions?: OctokitOptions) {
|
||||
const octokit = new ProbotOctokit(probotOptions);
|
||||
const options: RequestParameters = {
|
||||
...probotOptions,
|
||||
code,
|
||||
mediaType: {
|
||||
previews: ["fury"], // needed for GHES 2.20 and older
|
||||
|
@ -80,7 +76,7 @@ export class ManifestCreation {
|
|||
};
|
||||
const response = await octokit.request(
|
||||
"POST /app-manifests/:code/conversions",
|
||||
options
|
||||
options,
|
||||
);
|
||||
|
||||
const { id, client_id, client_secret, webhook_secret, pem } = response.data;
|
||||
|
@ -95,7 +91,7 @@ export class ManifestCreation {
|
|||
return response.data.html_url;
|
||||
}
|
||||
|
||||
public async updateEnv(env: any) {
|
||||
public async updateEnv(env: Env) {
|
||||
// Needs to be public due to tests
|
||||
return updateDotenv(env);
|
||||
}
|
||||
|
@ -103,7 +99,7 @@ export class ManifestCreation {
|
|||
get createAppUrl() {
|
||||
const githubHost = process.env.GHE_HOST || `github.com`;
|
||||
return `${process.env.GHE_PROTOCOL || "https"}://${githubHost}${
|
||||
process.env.GH_ORG ? "/organizations/".concat(process.env.GH_ORG) : ""
|
||||
process.env.GH_ORG ? `/organizations/${process.env.GH_ORG}` : ""
|
||||
}/settings/apps/new`;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,18 +1,17 @@
|
|||
import { State } from "../types";
|
||||
import { ProbotOctokit } from "./probot-octokit";
|
||||
import type { State } from "../types.js";
|
||||
import type { ProbotOctokit } from "./probot-octokit.js";
|
||||
import type { OctokitOptions } from "../types.js";
|
||||
import type { LogFn, Level } from "pino";
|
||||
|
||||
type FactoryOptions = {
|
||||
octokit: InstanceType<typeof ProbotOctokit>;
|
||||
octokitOptions: ConstructorParameters<typeof ProbotOctokit> & {
|
||||
throttle?: Record<string, unknown>;
|
||||
auth?: Record<string, unknown>;
|
||||
};
|
||||
octokit: ProbotOctokit;
|
||||
octokitOptions: OctokitOptions;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export async function getAuthenticatedOctokit(
|
||||
state: State,
|
||||
installationId?: number
|
||||
installationId?: number,
|
||||
) {
|
||||
const { log, octokit } = state;
|
||||
|
||||
|
@ -24,7 +23,9 @@ export async function getAuthenticatedOctokit(
|
|||
factory: ({ octokit, octokitOptions, ...otherOptions }: FactoryOptions) => {
|
||||
const pinoLog = log.child({ name: "github" });
|
||||
|
||||
const options = {
|
||||
const options: ConstructorParameters<typeof ProbotOctokit>[0] & {
|
||||
log: Record<Level, LogFn>;
|
||||
} = {
|
||||
...octokitOptions,
|
||||
log: {
|
||||
fatal: pinoLog.fatal.bind(pinoLog),
|
||||
|
@ -34,10 +35,12 @@ export async function getAuthenticatedOctokit(
|
|||
debug: pinoLog.debug.bind(pinoLog),
|
||||
trace: pinoLog.trace.bind(pinoLog),
|
||||
},
|
||||
throttle: {
|
||||
...octokitOptions.throttle,
|
||||
id: installationId,
|
||||
},
|
||||
throttle: octokitOptions.throttle?.enabled
|
||||
? {
|
||||
...octokitOptions.throttle,
|
||||
id: String(installationId),
|
||||
}
|
||||
: { enabled: false },
|
||||
auth: {
|
||||
...octokitOptions.auth,
|
||||
otherOptions,
|
||||
|
@ -49,5 +52,5 @@ export async function getAuthenticatedOctokit(
|
|||
|
||||
return new Octokit(options);
|
||||
},
|
||||
}) as Promise<InstanceType<typeof ProbotOctokit>>;
|
||||
}) as Promise<ProbotOctokit>;
|
||||
}
|
||||
|
|
|
@ -1,16 +1,38 @@
|
|||
import Bottleneck from "bottleneck";
|
||||
import Redis from "ioredis";
|
||||
import { Logger } from "pino";
|
||||
import { Redis, type RedisOptions } from "ioredis";
|
||||
import type { Logger } from "pino";
|
||||
import type { ThrottlingOptions } from "@octokit/plugin-throttling";
|
||||
|
||||
type Options = {
|
||||
log: Logger;
|
||||
redisConfig?: Redis.RedisOptions | string;
|
||||
redisConfig?: RedisOptions | string;
|
||||
};
|
||||
|
||||
export function getOctokitThrottleOptions(options: Options) {
|
||||
let { log, redisConfig } = options;
|
||||
|
||||
if (!redisConfig) return;
|
||||
const throttlingOptions: ThrottlingOptions = {
|
||||
onRateLimit: (retryAfter, options: { [key: string]: any }) => {
|
||||
log.warn(
|
||||
`Request quota exhausted for request ${options.method} ${options.url}`,
|
||||
);
|
||||
|
||||
// Retry twice after hitting a rate limit error, then give up
|
||||
if (options.request.retryCount <= 2) {
|
||||
log.info(`Retrying after ${retryAfter} seconds!`);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
onSecondaryRateLimit: (_retryAfter, options: { [key: string]: any }) => {
|
||||
// does not retry, only logs a warning
|
||||
log.warn(
|
||||
`Secondary quota detected for request ${options.method} ${options.url}`,
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
if (!redisConfig) return throttlingOptions;
|
||||
|
||||
const connection = new Bottleneck.IORedisConnection({
|
||||
client: getRedisClient(options),
|
||||
|
@ -19,12 +41,12 @@ export function getOctokitThrottleOptions(options: Options) {
|
|||
log.error(Object.assign(error, { source: "bottleneck" }));
|
||||
});
|
||||
|
||||
return {
|
||||
Bottleneck,
|
||||
connection,
|
||||
};
|
||||
throttlingOptions.Bottleneck = Bottleneck;
|
||||
throttlingOptions.connection = connection;
|
||||
|
||||
return throttlingOptions;
|
||||
}
|
||||
|
||||
function getRedisClient({ log, redisConfig }: Options): Redis.Redis | void {
|
||||
if (redisConfig) return new Redis(redisConfig as Redis.RedisOptions);
|
||||
function getRedisClient({ redisConfig }: Options): Redis | void {
|
||||
if (redisConfig) return new Redis(redisConfig as RedisOptions);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import LRUCache from "lru-cache";
|
||||
import { ProbotOctokit } from "./probot-octokit";
|
||||
import Redis from "ioredis";
|
||||
import type { LRUCache } from "lru-cache";
|
||||
import { ProbotOctokit } from "./probot-octokit.js";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
import { request } from "@octokit/request";
|
||||
|
||||
import { getOctokitThrottleOptions } from "./get-octokit-throttle-options";
|
||||
import { aliasLog } from "../helpers/alias-log";
|
||||
import { getOctokitThrottleOptions } from "./get-octokit-throttle-options.js";
|
||||
|
||||
import type { Logger } from "pino";
|
||||
import type { RequestRequestOptions } from "@octokit/types";
|
||||
import type { OctokitOptions } from "../types.js";
|
||||
|
||||
type Options = {
|
||||
cache: LRUCache<number, string>;
|
||||
|
@ -14,8 +16,10 @@ type Options = {
|
|||
githubToken?: string;
|
||||
appId?: number;
|
||||
privateKey?: string;
|
||||
redisConfig?: Redis.RedisOptions | string;
|
||||
redisConfig?: RedisOptions | string;
|
||||
webhookPath?: string;
|
||||
baseUrl?: string;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -32,11 +36,21 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
|||
const authOptions = options.githubToken
|
||||
? {
|
||||
token: options.githubToken,
|
||||
request: request.defaults({
|
||||
request: {
|
||||
fetch: options.request?.fetch,
|
||||
},
|
||||
}),
|
||||
}
|
||||
: {
|
||||
cache: options.cache,
|
||||
appId: options.appId,
|
||||
privateKey: options.privateKey,
|
||||
request: request.defaults({
|
||||
request: {
|
||||
fetch: options.request?.fetch,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
const octokitThrottleOptions = getOctokitThrottleOptions({
|
||||
|
@ -44,10 +58,10 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
|||
redisConfig: options.redisConfig,
|
||||
});
|
||||
|
||||
let defaultOptions: any = {
|
||||
let defaultOptions: Partial<OctokitOptions> = {
|
||||
auth: authOptions,
|
||||
log: options.log.child
|
||||
? aliasLog(options.log.child({ name: "octokit" }))
|
||||
? options.log.child({ name: "octokit" })
|
||||
: options.log,
|
||||
};
|
||||
|
||||
|
@ -59,7 +73,7 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
|||
defaultOptions.throttle = octokitThrottleOptions;
|
||||
}
|
||||
|
||||
return options.Octokit.defaults((instanceOptions: any) => {
|
||||
return options.Octokit.defaults((instanceOptions: OctokitOptions) => {
|
||||
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
||||
auth: instanceOptions.auth
|
||||
? Object.assign({}, defaultOptions.auth, instanceOptions.auth)
|
||||
|
@ -70,7 +84,7 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
|||
options.throttle = Object.assign(
|
||||
{},
|
||||
defaultOptions.throttle,
|
||||
instanceOptions.throttle
|
||||
instanceOptions.throttle,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,18 +1,13 @@
|
|||
import { Webhooks } from "@octokit/webhooks";
|
||||
|
||||
import { State } from "../types";
|
||||
import { getErrorHandler } from "../helpers/get-error-handler";
|
||||
import { webhookTransform } from "./octokit-webhooks-transform";
|
||||
|
||||
// import { Context } from "../context";
|
||||
import type { State } from "../types.js";
|
||||
import { getErrorHandler } from "../helpers/get-error-handler.js";
|
||||
import { webhookTransform } from "./octokit-webhooks-transform.js";
|
||||
|
||||
export function getWebhooks(state: State) {
|
||||
// TODO: This should be webhooks = new Webhooks<Context>({...}) but fails with
|
||||
// > The context of the event that was triggered, including the payload and
|
||||
// helpers for extracting information can be passed to GitHub API calls
|
||||
const webhooks = new Webhooks({
|
||||
secret: state.webhooks.secret!,
|
||||
transform: webhookTransform.bind(null, state),
|
||||
transform: (hook) => webhookTransform(state, hook),
|
||||
});
|
||||
webhooks.onError(getErrorHandler(state.log));
|
||||
return webhooks;
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
// tslint:disable-next-line
|
||||
import type { Octokit } from "@octokit/core";
|
||||
|
||||
export function probotRequestLogging(octokit: Octokit) {
|
||||
|
@ -20,7 +19,7 @@ export function probotRequestLogging(octokit: Octokit) {
|
|||
octokit.request.endpoint.parse(options);
|
||||
const msg = `GitHub request: ${method} ${url} - ${result.status}`;
|
||||
|
||||
// @ts-ignore log.debug is a pino log method and accepts a fields object
|
||||
// @ts-expect-error log.debug is a pino log method and accepts a fields object
|
||||
octokit.log.debug(params.body || {}, msg);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
|
||||
import { Context } from "../context";
|
||||
import { State } from "../types";
|
||||
import { Context } from "../context.js";
|
||||
import type { State } from "../types.js";
|
||||
|
||||
/**
|
||||
* Probot's transform option, which extends the `event` object that is passed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Octokit } from "@octokit/core";
|
||||
import { enterpriseCompatibility } from "@octokit/plugin-enterprise-compatibility";
|
||||
import { RequestOptions } from "@octokit/types";
|
||||
import type { RequestOptions } from "@octokit/types";
|
||||
import { paginateRest } from "@octokit/plugin-paginate-rest";
|
||||
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
||||
import { retry } from "@octokit/plugin-retry";
|
||||
|
@ -8,29 +8,30 @@ import { throttling } from "@octokit/plugin-throttling";
|
|||
import { config } from "@probot/octokit-plugin-config";
|
||||
import { createProbotAuth } from "octokit-auth-probot";
|
||||
|
||||
import { probotRequestLogging } from "./octokit-plugin-probot-request-logging";
|
||||
import { VERSION } from "../version";
|
||||
import { probotRequestLogging } from "./octokit-plugin-probot-request-logging.js";
|
||||
import { VERSION } from "../version.js";
|
||||
|
||||
const defaultOptions = {
|
||||
authStrategy: createProbotAuth,
|
||||
throttle: {
|
||||
onAbuseLimit: (
|
||||
enabled: true,
|
||||
onSecondaryRateLimit: (
|
||||
retryAfter: number,
|
||||
options: RequestOptions,
|
||||
octokit: Octokit
|
||||
octokit: Octokit,
|
||||
) => {
|
||||
octokit.log.warn(
|
||||
`Abuse limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`
|
||||
`Secondary Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
onRateLimit: (
|
||||
retryAfter: number,
|
||||
options: RequestOptions,
|
||||
octokit: Octokit
|
||||
octokit: Octokit,
|
||||
) => {
|
||||
octokit.log.warn(
|
||||
`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`
|
||||
`Rate limit hit with "${options.method} ${options.url}", retrying in ${retryAfter} seconds.`,
|
||||
);
|
||||
return true;
|
||||
},
|
||||
|
@ -45,14 +46,18 @@ export const ProbotOctokit = Octokit.plugin(
|
|||
legacyRestEndpointMethods,
|
||||
enterpriseCompatibility,
|
||||
probotRequestLogging,
|
||||
config
|
||||
config,
|
||||
).defaults((instanceOptions: any) => {
|
||||
// merge throttle options deeply
|
||||
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
||||
throttle: instanceOptions.throttle
|
||||
? Object.assign({}, defaultOptions.throttle, instanceOptions.throttle)
|
||||
: defaultOptions.throttle,
|
||||
});
|
||||
const options = {
|
||||
...defaultOptions,
|
||||
...instanceOptions,
|
||||
...{
|
||||
throttle: { ...defaultOptions.throttle, ...instanceOptions?.throttle },
|
||||
},
|
||||
};
|
||||
|
||||
return options;
|
||||
});
|
||||
|
||||
export type ProbotOctokit = InstanceType<typeof ProbotOctokit>;
|
||||
|
|
|
@ -1,28 +1,28 @@
|
|||
import LRUCache from "lru-cache";
|
||||
import { Logger } from "pino";
|
||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
import { LRUCache } from "lru-cache";
|
||||
import type { Logger } from "pino";
|
||||
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
|
||||
import { aliasLog } from "./helpers/alias-log";
|
||||
import { auth } from "./auth";
|
||||
import { getLog } from "./helpers/get-log";
|
||||
import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-defaults";
|
||||
import { getWebhooks } from "./octokit/get-webhooks";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import { VERSION } from "./version";
|
||||
import {
|
||||
import { auth } from "./auth.js";
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-defaults.js";
|
||||
import { getWebhooks } from "./octokit/get-webhooks.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
import { VERSION } from "./version.js";
|
||||
import type {
|
||||
ApplicationFunction,
|
||||
ApplicationFunctionOptions,
|
||||
DeprecatedLogger,
|
||||
Options,
|
||||
ProbotWebhooks,
|
||||
State,
|
||||
} from "./types";
|
||||
} from "./types.js";
|
||||
import { defaultWebhooksPath } from "./server/server.js";
|
||||
import { rebindLog } from "./helpers/rebind-log.js";
|
||||
|
||||
export type Constructor<T> = new (...args: any[]) => T;
|
||||
export type Constructor<T = any> = new (...args: any[]) => T;
|
||||
|
||||
export class Probot {
|
||||
static version = VERSION;
|
||||
static defaults<S extends Constructor<any>>(this: S, defaults: Options) {
|
||||
static defaults<S extends Constructor>(this: S, defaults: Options) {
|
||||
const ProbotWithDefaults = class extends this {
|
||||
constructor(...args: any[]) {
|
||||
const options = args[0] || {};
|
||||
|
@ -34,15 +34,16 @@ export class Probot {
|
|||
}
|
||||
|
||||
public webhooks: ProbotWebhooks;
|
||||
public log: DeprecatedLogger;
|
||||
public webhookPath: string;
|
||||
public log: Logger;
|
||||
public version: String;
|
||||
public on: ProbotWebhooks["on"];
|
||||
public onAny: ProbotWebhooks["onAny"];
|
||||
public onError: ProbotWebhooks["onError"];
|
||||
public auth: (
|
||||
installationId?: number,
|
||||
log?: Logger
|
||||
) => Promise<InstanceType<typeof ProbotOctokit>>;
|
||||
log?: Logger,
|
||||
) => Promise<ProbotOctokit>;
|
||||
|
||||
private state: State;
|
||||
|
||||
|
@ -52,14 +53,16 @@ export class Probot {
|
|||
let level = options.logLevel;
|
||||
const logMessageKey = options.logMessageKey;
|
||||
|
||||
this.log = aliasLog(options.log || getLog({ level, logMessageKey }));
|
||||
this.log = options.log
|
||||
? rebindLog(options.log)
|
||||
: getLog({ level, logMessageKey });
|
||||
|
||||
// TODO: support redis backend for access token cache if `options.redisConfig`
|
||||
const cache = new LRUCache<number, string>({
|
||||
// cache max. 15000 tokens, that will use less than 10mb memory
|
||||
max: 15000,
|
||||
// Cache for 1 minute less than GitHub expiry
|
||||
maxAge: 1000 * 60 * 59,
|
||||
ttl: 1000 * 60 * 59,
|
||||
});
|
||||
|
||||
const Octokit = getProbotOctokitWithDefaults({
|
||||
|
@ -68,16 +71,25 @@ export class Probot {
|
|||
appId: Number(options.appId),
|
||||
privateKey: options.privateKey,
|
||||
cache,
|
||||
log: this.log,
|
||||
log: rebindLog(this.log),
|
||||
redisConfig: options.redisConfig,
|
||||
baseUrl: options.baseUrl,
|
||||
});
|
||||
const octokit = new Octokit();
|
||||
const octokitLogger = rebindLog(this.log.child({ name: "octokit" }));
|
||||
const octokit = new Octokit({
|
||||
request: options.request,
|
||||
log: {
|
||||
debug: octokitLogger.debug.bind(octokitLogger),
|
||||
info: octokitLogger.info.bind(octokitLogger),
|
||||
warn: octokitLogger.warn.bind(octokitLogger),
|
||||
error: octokitLogger.error.bind(octokitLogger),
|
||||
},
|
||||
});
|
||||
|
||||
this.state = {
|
||||
cache,
|
||||
githubToken: options.githubToken,
|
||||
log: this.log,
|
||||
log: rebindLog(this.log),
|
||||
Octokit,
|
||||
octokit,
|
||||
webhooks: {
|
||||
|
@ -87,11 +99,14 @@ export class Probot {
|
|||
privateKey: options.privateKey,
|
||||
host: options.host,
|
||||
port: options.port,
|
||||
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||
request: options.request,
|
||||
};
|
||||
|
||||
this.auth = auth.bind(null, this.state);
|
||||
|
||||
this.webhooks = getWebhooks(this.state);
|
||||
this.webhookPath = this.state.webhookPath;
|
||||
|
||||
this.on = this.webhooks.on;
|
||||
this.onAny = this.webhooks.onAny;
|
||||
|
@ -107,7 +122,7 @@ export class Probot {
|
|||
|
||||
public async load(
|
||||
appFn: ApplicationFunction | ApplicationFunction[],
|
||||
options: ApplicationFunctionOptions = {}
|
||||
options: ApplicationFunctionOptions = {},
|
||||
) {
|
||||
if (Array.isArray(appFn)) {
|
||||
for (const fn of appFn) {
|
||||
|
|
39
src/run.ts
39
src/run.ts
|
@ -1,18 +1,19 @@
|
|||
import pkgConf from "pkg-conf";
|
||||
|
||||
import { ApplicationFunction, Options, ServerOptions } from "./types";
|
||||
import { Probot } from "./index";
|
||||
import { setupAppFactory } from "./apps/setup";
|
||||
import { getLog, GetLogOptions } from "./helpers/get-log";
|
||||
import { readCliOptions } from "./bin/read-cli-options";
|
||||
import { readEnvOptions } from "./bin/read-env-options";
|
||||
import { Server } from "./server/server";
|
||||
import { defaultApp } from "./apps/default";
|
||||
import { resolveAppFunction } from "./helpers/resolve-app-function";
|
||||
import { isProduction } from "./helpers/is-production";
|
||||
import type { ApplicationFunction, Options, ServerOptions } from "./types.js";
|
||||
import { Probot } from "./index.js";
|
||||
import { setupAppFactory } from "./apps/setup.js";
|
||||
import { getLog } from "./helpers/get-log.js";
|
||||
import { readCliOptions } from "./bin/read-cli-options.js";
|
||||
import { readEnvOptions } from "./bin/read-env-options.js";
|
||||
import { Server } from "./server/server.js";
|
||||
import { defaultApp } from "./apps/default.js";
|
||||
import { resolveAppFunction } from "./helpers/resolve-app-function.js";
|
||||
import { isProduction } from "./helpers/is-production.js";
|
||||
import { config as dotenvConfig } from "dotenv";
|
||||
|
||||
type AdditionalOptions = {
|
||||
env: Record<string, string | undefined>;
|
||||
env: NodeJS.ProcessEnv;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -21,9 +22,9 @@ type AdditionalOptions = {
|
|||
*/
|
||||
export async function run(
|
||||
appFnOrArgv: ApplicationFunction | string[],
|
||||
additionalOptions?: AdditionalOptions
|
||||
additionalOptions?: AdditionalOptions,
|
||||
) {
|
||||
require("dotenv").config();
|
||||
dotenvConfig();
|
||||
|
||||
const envOptions = readEnvOptions(additionalOptions?.env);
|
||||
const cliOptions = Array.isArray(appFnOrArgv)
|
||||
|
@ -55,15 +56,13 @@ export async function run(
|
|||
args,
|
||||
} = { ...envOptions, ...cliOptions };
|
||||
|
||||
const logOptions: GetLogOptions = {
|
||||
const log = getLog({
|
||||
level,
|
||||
logFormat,
|
||||
logLevelInString,
|
||||
logMessageKey,
|
||||
sentryDsn,
|
||||
};
|
||||
|
||||
const log = getLog(logOptions);
|
||||
});
|
||||
|
||||
const probotOptions: Options = {
|
||||
appId,
|
||||
|
@ -90,12 +89,12 @@ export async function run(
|
|||
if (!appId) {
|
||||
throw new Error(
|
||||
"App ID is missing, and is required to run in production mode. " +
|
||||
"To resolve, ensure the APP_ID environment variable is set."
|
||||
"To resolve, ensure the APP_ID environment variable is set.",
|
||||
);
|
||||
} else if (!privateKey) {
|
||||
throw new Error(
|
||||
"Certificate is missing, and is required to run in production mode. " +
|
||||
"To resolve, ensure either the PRIVATE_KEY or PRIVATE_KEY_PATH environment variable is set and contains a valid certificate"
|
||||
"To resolve, ensure either the PRIVATE_KEY or PRIVATE_KEY_PATH environment variable is set and contains a valid certificate",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -122,7 +121,7 @@ export async function run(
|
|||
if (Array.isArray(appFnOrArgv)) {
|
||||
const pkg = await pkgConf("probot");
|
||||
|
||||
const combinedApps: ApplicationFunction = async (app) => {
|
||||
const combinedApps: ApplicationFunction = async (_app) => {
|
||||
await server.load(defaultApp);
|
||||
|
||||
if (Array.isArray(pkg.apps)) {
|
||||
|
|
|
@ -1,19 +1,21 @@
|
|||
import pinoHttp from "pino-http";
|
||||
import type { Logger } from "pino";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { randomUUID as uuidv4 } from "node:crypto";
|
||||
|
||||
export function getLoggingMiddleware(logger: Logger, options?: pinoHttp.Options) {
|
||||
import { pinoHttp, startTime, type Options, type HttpLogger } from "pino-http";
|
||||
import type { Logger } from "pino";
|
||||
|
||||
export function getLoggingMiddleware(
|
||||
logger: Logger,
|
||||
options?: Options,
|
||||
): HttpLogger {
|
||||
return pinoHttp({
|
||||
...options,
|
||||
logger: logger.child({ name: "http" }),
|
||||
customSuccessMessage(res) {
|
||||
const responseTime = Date.now() - res[pinoHttp.startTime];
|
||||
// @ts-ignore
|
||||
customSuccessMessage(_req, res) {
|
||||
const responseTime = Date.now() - res[startTime];
|
||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||
},
|
||||
customErrorMessage(err, res) {
|
||||
const responseTime = Date.now() - res[pinoHttp.startTime];
|
||||
// @ts-ignore
|
||||
customErrorMessage(_err, res) {
|
||||
const responseTime = Date.now() - res[startTime];
|
||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||
},
|
||||
genReqId: (req) =>
|
||||
|
|
|
@ -1,20 +1,23 @@
|
|||
import { Server as HttpServer } from "http";
|
||||
import type { Server as HttpServer } from "node:http";
|
||||
import { join } from "node:path";
|
||||
|
||||
import express, { Application, Router } from "express";
|
||||
import { join } from "path";
|
||||
import { Logger } from "pino";
|
||||
import express, { Router, type Application } from "express";
|
||||
import type { Logger } from "pino";
|
||||
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||
|
||||
import { getLog } from "../helpers/get-log";
|
||||
import { getLoggingMiddleware } from "./logging-middleware";
|
||||
import { createWebhookProxy } from "../helpers/webhook-proxy";
|
||||
import { VERSION } from "../version";
|
||||
import { ApplicationFunction, ServerOptions } from "../types";
|
||||
import { Probot } from "../";
|
||||
import { engine } from "express-handlebars";
|
||||
import EventSource from "eventsource";
|
||||
import { getLoggingMiddleware } from "./logging-middleware.js";
|
||||
import { createWebhookProxy } from "../helpers/webhook-proxy.js";
|
||||
import { VERSION } from "../version.js";
|
||||
import type { ApplicationFunction, ServerOptions } from "../types.js";
|
||||
import type { Probot } from "../index.js";
|
||||
import type EventSource from "eventsource";
|
||||
import { rebindLog } from "../helpers/rebind-log.js";
|
||||
|
||||
// the default path as defined in @octokit/webhooks
|
||||
export const defaultWebhooksPath = "/api/github/webhooks";
|
||||
|
||||
type State = {
|
||||
cwd?: string;
|
||||
httpServer?: HttpServer;
|
||||
port?: number;
|
||||
host?: string;
|
||||
|
@ -35,69 +38,66 @@ export class Server {
|
|||
|
||||
constructor(options: ServerOptions = {} as ServerOptions) {
|
||||
this.expressApp = express();
|
||||
this.log = options.log || getLog().child({ name: "server" });
|
||||
this.probotApp = new options.Probot();
|
||||
this.probotApp = new options.Probot({
|
||||
request: options.request,
|
||||
});
|
||||
this.log = options.log
|
||||
? rebindLog(options.log)
|
||||
: rebindLog(this.probotApp.log.child({ name: "server" }));
|
||||
|
||||
this.state = {
|
||||
cwd: options.cwd || process.cwd(),
|
||||
port: options.port,
|
||||
host: options.host,
|
||||
webhookPath: options.webhookPath || "/",
|
||||
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||
webhookProxy: options.webhookProxy,
|
||||
};
|
||||
|
||||
this.expressApp.use(getLoggingMiddleware(this.log, options.loggingOptions));
|
||||
this.expressApp.use(
|
||||
"/probot/static/",
|
||||
express.static(join(__dirname, "..", "..", "static"))
|
||||
express.static(join(__dirname, "..", "..", "static")),
|
||||
);
|
||||
this.expressApp.use(
|
||||
this.state.webhookPath,
|
||||
createWebhooksMiddleware(this.probotApp.webhooks, {
|
||||
path: "/",
|
||||
})
|
||||
path: this.state.webhookPath,
|
||||
}),
|
||||
);
|
||||
|
||||
this.expressApp.engine(
|
||||
"handlebars",
|
||||
engine({
|
||||
defaultLayout: false,
|
||||
})
|
||||
);
|
||||
this.expressApp.set("view engine", "handlebars");
|
||||
this.expressApp.set("views", join(__dirname, "..", "..", "views"));
|
||||
this.expressApp.get("/ping", (req, res) => res.end("PONG"));
|
||||
this.expressApp.get("/ping", (_req, res) => res.end("PONG"));
|
||||
}
|
||||
|
||||
public async load(appFn: ApplicationFunction) {
|
||||
await appFn(this.probotApp, {
|
||||
cwd: this.state.cwd,
|
||||
getRouter: (path) => this.router(path),
|
||||
});
|
||||
}
|
||||
|
||||
public async start() {
|
||||
this.log.info(
|
||||
`Running Probot v${this.version} (Node.js: ${process.version})`
|
||||
`Running Probot v${this.version} (Node.js: ${process.version})`,
|
||||
);
|
||||
const port = this.state.port || 3000;
|
||||
const { host, webhookPath, webhookProxy } = this.state;
|
||||
const printableHost = host ?? "localhost";
|
||||
|
||||
this.state.httpServer = (await new Promise((resolve, reject) => {
|
||||
this.state.httpServer = await new Promise((resolve, reject) => {
|
||||
const server = this.expressApp.listen(
|
||||
port,
|
||||
...((host ? [host] : []) as any),
|
||||
() => {
|
||||
async () => {
|
||||
if (webhookProxy) {
|
||||
this.state.eventSource = createWebhookProxy({
|
||||
this.state.eventSource = await createWebhookProxy({
|
||||
logger: this.log,
|
||||
path: webhookPath,
|
||||
port: port,
|
||||
url: webhookProxy,
|
||||
}) as EventSource;
|
||||
});
|
||||
}
|
||||
this.log.info(`Listening on http://${printableHost}:${port}`);
|
||||
resolve(server);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
server.on("error", (error: NodeJS.ErrnoException) => {
|
||||
|
@ -110,7 +110,7 @@ export class Server {
|
|||
this.log.error(error);
|
||||
reject(error);
|
||||
});
|
||||
})) as HttpServer;
|
||||
});
|
||||
|
||||
return this.state.httpServer;
|
||||
}
|
||||
|
|
134
src/types.ts
134
src/types.ts
|
@ -1,17 +1,18 @@
|
|||
import express from "express";
|
||||
import {
|
||||
import type {
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
Webhooks,
|
||||
} from "@octokit/webhooks";
|
||||
import LRUCache from "lru-cache";
|
||||
import Redis from "ioredis";
|
||||
import { Options as LoggingOptions } from "pino-http";
|
||||
import type { LRUCache } from "lru-cache";
|
||||
import type { RedisOptions } from "ioredis";
|
||||
import type { Options as LoggingOptions } from "pino-http";
|
||||
|
||||
import { Probot } from "./index";
|
||||
import { Context } from "./context";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
||||
import { Probot } from "./index.js";
|
||||
import { Context } from "./context.js";
|
||||
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||
|
||||
import type { Logger, LogFn } from "pino";
|
||||
import type { Logger } from "pino";
|
||||
import type { RequestRequestOptions } from "@octokit/types";
|
||||
|
||||
export interface Options {
|
||||
privateKey?: string;
|
||||
|
@ -20,13 +21,15 @@ export interface Options {
|
|||
|
||||
Octokit?: typeof ProbotOctokit;
|
||||
log?: Logger;
|
||||
redisConfig?: Redis.RedisOptions | string;
|
||||
redisConfig?: RedisOptions | string;
|
||||
secret?: string;
|
||||
logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||
logMessageKey?: string;
|
||||
port?: number;
|
||||
host?: string;
|
||||
baseUrl?: string;
|
||||
request?: RequestRequestOptions;
|
||||
webhookPath?: string;
|
||||
}
|
||||
|
||||
export type State = {
|
||||
|
@ -35,7 +38,7 @@ export type State = {
|
|||
githubToken?: string;
|
||||
log: Logger;
|
||||
Octokit: typeof ProbotOctokit;
|
||||
octokit: InstanceType<typeof ProbotOctokit>;
|
||||
octokit: ProbotOctokit;
|
||||
cache?: LRUCache<number, string>;
|
||||
webhooks: {
|
||||
secret?: string;
|
||||
|
@ -43,23 +46,31 @@ export type State = {
|
|||
port?: number;
|
||||
host?: string;
|
||||
baseUrl?: string;
|
||||
webhookPath: string;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
// Omit the `payload`, `id`,`name` properties from the `Context` class as they are already present in the types of `WebhookEvent`
|
||||
// The `Webhooks` class accepts a type parameter (`TTransformed`) that is used to transform the event payload in the form of
|
||||
// WebhookEvent["payload"] & T
|
||||
// Simply passing `Context` as `TTransformed` would result in the payload types being too complex for TypeScript to infer
|
||||
// See https://github.com/probot/probot/issues/1388
|
||||
// See https://github.com/probot/probot/issues/1815 as for why this is in a seperate type, and not directly passed to `Webhooks`
|
||||
type SimplifiedObject = Omit<Context, keyof WebhookEvent>;
|
||||
export type ProbotWebhooks = Webhooks<SimplifiedObject>;
|
||||
|
||||
export type DeprecatedLogger = LogFn & Logger;
|
||||
|
||||
export type ApplicationFunctionOptions = {
|
||||
getRouter?: (path?: string) => express.Router;
|
||||
cwd?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
export type ApplicationFunction = (
|
||||
app: Probot,
|
||||
options: ApplicationFunctionOptions
|
||||
options: ApplicationFunctionOptions,
|
||||
) => void | Promise<void>;
|
||||
|
||||
export type ServerOptions = {
|
||||
cwd?: string;
|
||||
log?: Logger;
|
||||
port?: number;
|
||||
host?: string;
|
||||
|
@ -67,6 +78,7 @@ export type ServerOptions = {
|
|||
webhookProxy?: string;
|
||||
Probot: typeof Probot;
|
||||
loggingOptions?: LoggingOptions;
|
||||
request?: RequestRequestOptions;
|
||||
};
|
||||
|
||||
export type MiddlewareOptions = {
|
||||
|
@ -74,3 +86,99 @@ export type MiddlewareOptions = {
|
|||
webhooksPath?: string;
|
||||
[key: string]: unknown;
|
||||
};
|
||||
|
||||
export type OctokitOptions = NonNullable<
|
||||
ConstructorParameters<typeof ProbotOctokit>[0]
|
||||
>;
|
||||
|
||||
export type PackageJson = {
|
||||
name?: string;
|
||||
version?: string;
|
||||
description?: string;
|
||||
homepage?: string;
|
||||
repository?: string;
|
||||
engines?: {
|
||||
[key: string]: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type Env = Record<Uppercase<string>, string>;
|
||||
|
||||
type ManifestPermissionValue = "read" | "write" | "none";
|
||||
type ManifestPermissionScope =
|
||||
| "actions"
|
||||
| "checks"
|
||||
| "contents"
|
||||
| "deployments"
|
||||
| "id-token"
|
||||
| "issues"
|
||||
| "discussions"
|
||||
| "packages"
|
||||
| "pages"
|
||||
| "pull-requests"
|
||||
| "repository-projects"
|
||||
| "security-events"
|
||||
| "statuses";
|
||||
|
||||
export type Manifest = {
|
||||
/**
|
||||
* The name of the GitHub App.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* __Required.__ The homepage of your GitHub App.
|
||||
*/
|
||||
url: string;
|
||||
/**
|
||||
* The configuration of the GitHub App's webhook.
|
||||
*/
|
||||
hook_attributes?: {
|
||||
/*
|
||||
* __Required.__ The URL of the server that will receive the webhook POST requests.
|
||||
*/
|
||||
url: string;
|
||||
/*
|
||||
* Deliver event details when this hook is triggered, defaults to true.
|
||||
*/
|
||||
active?: boolean;
|
||||
};
|
||||
/**
|
||||
* The full URL to redirect to after a user initiates the registration of a GitHub App from a manifest.
|
||||
*/
|
||||
redirect_url?: string;
|
||||
/**
|
||||
* A full URL to redirect to after someone authorizes an installation. You can provide up to 10 callback URLs.
|
||||
*/
|
||||
callback_urls?: string[];
|
||||
/**
|
||||
* A full URL to redirect users to after they install your GitHub App if additional setup is required.
|
||||
*/
|
||||
setup_url?: string;
|
||||
/**
|
||||
* A description of the GitHub App.
|
||||
*/
|
||||
description?: string;
|
||||
/**
|
||||
* Set to `true` when your GitHub App is available to the public or `false` when it is only accessible to the owner of the app.
|
||||
*/
|
||||
public?: boolean;
|
||||
/**
|
||||
* The list of events the GitHub App subscribes to.
|
||||
*/
|
||||
default_events?: WebhookEvent[];
|
||||
/**
|
||||
* The set of permissions needed by the GitHub App. The format of the object uses the permission name for the key (for example, `issues`) and the access type for the value (for example, `write`).
|
||||
*/
|
||||
default_permissions?:
|
||||
| "read-all"
|
||||
| "write-all"
|
||||
| Record<ManifestPermissionScope, ManifestPermissionValue>;
|
||||
/**
|
||||
* Set to `true` to request the user to authorize the GitHub App, after the GitHub App is installed.
|
||||
*/
|
||||
request_oauth_on_install?: boolean;
|
||||
/**
|
||||
* Set to `true` to redirect users to the setup_url after they update your GitHub App installation.
|
||||
*/
|
||||
setup_on_update?: boolean;
|
||||
};
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
export function importView({
|
||||
name,
|
||||
GH_HOST,
|
||||
WEBHOOK_PROXY_URL = "",
|
||||
}: {
|
||||
name?: string;
|
||||
GH_HOST: string;
|
||||
WEBHOOK_PROXY_URL?: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Import {{#if pkg.name }}{{ pkg.name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>Import ${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
@ -20,9 +29,9 @@
|
|||
<h3>Step 1:</h3>
|
||||
<p class="d-block mt-2">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b>{{ WEBHOOK_PROXY_URL }}</b>
|
||||
<b>${WEBHOOK_PROXY_URL}</b>
|
||||
</p>
|
||||
<a class="d-block mt-2" href="{{ GH_HOST }}/settings/apps" target="__blank" rel="noreferrer">
|
||||
<a class="d-block mt-2" href="${GH_HOST}/settings/apps" target="__blank" rel="noreferrer">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
|
@ -49,7 +58,7 @@
|
|||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -81,4 +90,5 @@
|
|||
</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
|
@ -1,10 +1,19 @@
|
|||
<!DOCTYPE html>
|
||||
export function probotView({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
}: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>{{#if name }}{{ name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
@ -13,26 +22,28 @@
|
|||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
||||
<div class="box-shadow rounded-2 border p-6 bg-white">
|
||||
<h1>
|
||||
Welcome to {{#if name }}{{ name }}{{else}}your Probot App{{/if}}
|
||||
{{#if version}}
|
||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ version }}</span>
|
||||
{{/if}}
|
||||
</h1>
|
||||
Welcome to ${name || "your Probot App"}
|
||||
${
|
||||
version
|
||||
? ` <span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>\n`
|
||||
: ""
|
||||
} </h1>
|
||||
|
||||
{{#if description }}
|
||||
<p>{{ description }}</p>
|
||||
{{else}}
|
||||
<p>This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
{{/if}}
|
||||
<p>${
|
||||
description
|
||||
? description
|
||||
: 'This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||
}</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4">
|
||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
|
@ -1,10 +1,24 @@
|
|||
export function setupView({
|
||||
name,
|
||||
description,
|
||||
version,
|
||||
createAppUrl,
|
||||
manifest,
|
||||
}: {
|
||||
name?: string;
|
||||
description?: string;
|
||||
version?: string;
|
||||
createAppUrl: string;
|
||||
manifest: string;
|
||||
}): string {
|
||||
return `<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Setup {{#if pkg.name }}{{ pkg.name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>Setup ${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
@ -13,17 +27,19 @@
|
|||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
||||
<div class="box-shadow rounded-2 border p-6 bg-white">
|
||||
<h1>
|
||||
Welcome to {{#if pkg.name }}{{ pkg.name }}{{else}}your Probot App{{/if}}
|
||||
{{#if version}}
|
||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ pkg.version }}</span>
|
||||
{{/if}}
|
||||
Welcome to ${name || "your Probot App"}
|
||||
${
|
||||
version
|
||||
? `<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>`
|
||||
: ""
|
||||
}
|
||||
</h1>
|
||||
|
||||
{{#if pkg.description }}
|
||||
<p>{{ pkg.description }}</p>
|
||||
{{else}}
|
||||
<p>This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
{{/if}}
|
||||
<p>${
|
||||
description
|
||||
? description
|
||||
: 'This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||
}</p>
|
||||
|
||||
<div class="text-left mt-6">
|
||||
<h2 class="alt-h3 mb-2">Getting Started</h2>
|
||||
|
@ -31,8 +47,8 @@
|
|||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action="{{ createAppUrl }}" method="post" target="_blank" class="d-flex flex-items-center">
|
||||
<button class="btn btn-outline" name="manifest" id="manifest" value='{{ manifest }}' >Register GitHub App</button>
|
||||
<form action="${createAppUrl}" method="post" target="_blank" class="d-flex flex-items-center">
|
||||
<button class="btn btn-outline" name="manifest" id="manifest" value='${manifest}' >Register GitHub App</button>
|
||||
<a href="/probot/import" class="ml-2">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
|
@ -42,9 +58,10 @@
|
|||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
|
@ -1,10 +1,11 @@
|
|||
<!DOCTYPE html>
|
||||
export function successView({ name }: { name?: string }): string {
|
||||
return `<!DOCTYPE html>
|
||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||
<title>Setup {{#if pkg.name }}{{ pkg.name }}{{else}}Your App{{/if}} | built with Probot</title>
|
||||
<title>Setup ${name || "Your App"} | built with Probot</title>
|
||||
<link rel="icon" href="/probot/static/probot-head.png">
|
||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||
</head>
|
||||
|
@ -23,9 +24,10 @@
|
|||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||
<div class="d-flex flex-justify-center mt-2">
|
||||
<a href="https://probot.github.io/docs/" class="btn btn-outline mr-2">Documentation</a>
|
||||
<a href="https://probot-slackin.herokuapp.com/" class="btn btn-outline">Chat on Slack</a>
|
||||
<a href="https://github.com/probot/probot/discussions" class="btn btn-outline">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
</html>`;
|
||||
}
|
|
@ -1,11 +1,11 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Probot webhooks responds with the correct error if the PEM file is missing 1`] = `"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary."`;
|
||||
exports[`Probot > webhooks > responds with the correct error if the PEM file is missing 1`] = `"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary."`;
|
||||
|
||||
exports[`Probot webhooks responds with the correct error if the jwt could not be decoded 1`] = `"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary."`;
|
||||
exports[`Probot > webhooks > responds with the correct error if the jwt could not be decoded 1`] = `"Your private key (a .pem file or PRIVATE_KEY environment variable) or APP_ID is incorrect. Go to https://github.com/settings/apps/YOUR_APP, verify that APP_ID is set correctly, and generate a new PEM file if necessary."`;
|
||||
|
||||
exports[`Probot webhooks responds with the correct error if webhook secret does not match 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
exports[`Probot > webhooks > responds with the correct error if webhook secret does not match 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
|
||||
exports[`Probot webhooks responds with the correct error if webhook secret is not found 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
exports[`Probot > webhooks > responds with the correct error if webhook secret is not found 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
|
||||
exports[`Probot webhooks responds with the correct error if webhook secret is wrong 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
exports[`Probot > webhooks > responds with the correct error if webhook secret is wrong 1`] = `"Go to https://github.com/settings/apps/YOUR_APP and verify that the Webhook secret matches the value of the WEBHOOK_SECRET environment variable."`;
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`default app > GET /probot > get info from package.json > returns the correct HTML with values 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>probot | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to probot
|
||||
<span class=\\"Label Label--outline v-align-middle ml-2 text-gray-light\\">v0.0.0-development</span>
|
||||
</h1>
|
||||
|
||||
<p>A framework for building GitHub Apps to automate and improve your workflow</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`default app > GET /probot > get info from package.json > returns the correct HTML without values 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
</h1>
|
||||
|
||||
<p>This bot was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
|
@ -1,14 +1,108 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`Setup app GET /probot/setup returns a redirect 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
exports[`Setup app > GET /probot/import > renders importView 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Import probot | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
|
||||
<body class=\\"bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full py-6\\">
|
||||
<a href=\\"/probot\\"><img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\"></a>
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h2>Use existing Github App</h2>
|
||||
<br>
|
||||
|
||||
<h3>Step 1:</h3>
|
||||
<p class=\\"d-block mt-2\\">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b></b>
|
||||
</p>
|
||||
<a class=\\"d-block mt-2\\" href=\\"https://github.com/settings/apps\\" target=\\"__blank\\" rel=\\"noreferrer\\">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<h3>Step 2:</h3>
|
||||
<p class=\\"mt-2\\">Fill out this form</p>
|
||||
<form onsubmit=\\"return onSubmit(event) || false\\">
|
||||
<label class=\\"d-block mt-2\\" for=\\"appId\\">App Id</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"text\\" required=\\"true\\" id=\\"appId\\" name=\\"appId\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"whs\\">Webhook secret (required!)</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"password\\" required=\\"true\\" id=\\"whs\\" name=\\"whs\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"pem\\">Private Key</label>
|
||||
<input class=\\"form-control width-full m-2\\" type=\\"file\\" accept=\\".pem\\" required=\\"true\\" id=\\"pem\\"
|
||||
name=\\"pem\\">
|
||||
<br>
|
||||
|
||||
<button class=\\"btn btn-outline m-2\\" type=\\"submit\\">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const idEl = document.getElementById('appId');
|
||||
const appId = idEl.value;
|
||||
|
||||
|
||||
const secretEl = document.getElementById('whs');
|
||||
const webhook_secret = secretEl.value;
|
||||
|
||||
const fileEl = document.getElementById('pem');
|
||||
const file = fileEl.files[0];
|
||||
|
||||
file.text().then((text) => fetch('', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ appId, pem: text, webhook_secret })
|
||||
})).then((r) => {
|
||||
if (r.ok) {
|
||||
location.replace('/probot/success');
|
||||
}
|
||||
}).catch((e) => alert(e));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`Setup app > GET /probot/setup > returns a redirect 1`] = `"Found. Redirecting to /apps/my-app/installations/new"`;
|
||||
|
||||
exports[`Setup app > GET /probot/setup > returns a redirect 2`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
"APP_ID": "id",
|
||||
"GITHUB_CLIENT_ID": "Iv1.8a61f9b3a7aba766",
|
||||
"GITHUB_CLIENT_SECRET": "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
||||
|
@ -19,15 +113,55 @@ Array [
|
|||
]
|
||||
`;
|
||||
|
||||
exports[`Setup app POST /probot/import updates .env 1`] = `
|
||||
Array [
|
||||
Array [
|
||||
Object {
|
||||
exports[`Setup app > GET /probot/setup > throws a 400 Error if code is an empty string 1`] = `"code missing or invalid"`;
|
||||
|
||||
exports[`Setup app > GET /probot/setup > throws a 400 Error if code is not provided 1`] = `"code missing or invalid"`;
|
||||
|
||||
exports[`Setup app > GET /probot/success > returns a 200 response 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup probot | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<div class=\\"text-center\\">
|
||||
<h1 class=\\"alt-h3 mb-2\\">Congrats! You have successfully installed your app!
|
||||
<br>
|
||||
Checkout <a href=\\"https://probot.github.io/docs/webhooks/\\">Receiving webhooks</a> and <a href=\\"https://probot.github.io/docs/github-api/\\">Interacting with GitHub</a> to learn more!</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`Setup app > POST /probot/import > 400 when keys are missing 1`] = `"appId and/or pem and/or webhook_secret missing"`;
|
||||
|
||||
exports[`Setup app > POST /probot/import > updates .env 1`] = `
|
||||
[
|
||||
[
|
||||
{
|
||||
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||
},
|
||||
],
|
||||
Array [
|
||||
Object {
|
||||
[
|
||||
{
|
||||
"APP_ID": "foo",
|
||||
"PRIVATE_KEY": "\\"bar\\"",
|
||||
"WEBHOOK_SECRET": "baz",
|
||||
|
|
|
@ -1,71 +1,70 @@
|
|||
import Stream from "stream";
|
||||
import Stream from "node:stream";
|
||||
|
||||
import pino from "pino";
|
||||
import { pino } from "pino";
|
||||
import request from "supertest";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { Probot, Server } from "../../src";
|
||||
import { defaultApp } from "../../src/apps/default";
|
||||
import { Probot, Server } from "../../src/index.js";
|
||||
import { defaultApp } from "../../src/apps/default.js";
|
||||
|
||||
describe("default app", () => {
|
||||
let server: Server;
|
||||
let output: any;
|
||||
let output = [];
|
||||
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
async function instantiateServer(cwd = process.cwd()) {
|
||||
output = [];
|
||||
server = new Server({
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
appId: 1,
|
||||
privateKey: "private key",
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
cwd,
|
||||
});
|
||||
|
||||
await server.load(defaultApp);
|
||||
});
|
||||
return server;
|
||||
}
|
||||
|
||||
describe("GET /probot", () => {
|
||||
it("returns a 200 response", () => {
|
||||
it("returns a 200 response", async () => {
|
||||
const server = await instantiateServer();
|
||||
return request(server.expressApp).get("/probot").expect(200);
|
||||
});
|
||||
|
||||
describe("get info from package.json", () => {
|
||||
let cwd: string;
|
||||
beforeEach(() => {
|
||||
cwd = process.cwd();
|
||||
});
|
||||
|
||||
it("returns the correct HTML with values", async () => {
|
||||
const server = await instantiateServer();
|
||||
const actual = await request(server.expressApp)
|
||||
.get("/probot")
|
||||
.expect(200);
|
||||
expect(actual.text).toMatch("Welcome to probot");
|
||||
expect(actual.text).toMatch("A framework for building GitHub Apps");
|
||||
expect(actual.text).toMatch(/v\d+\.\d+\.\d+/);
|
||||
expect(actual.text).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("returns the correct HTML without values", async () => {
|
||||
process.chdir(__dirname);
|
||||
const server = await instantiateServer(__dirname);
|
||||
const actual = await request(server.expressApp)
|
||||
.get("/probot")
|
||||
.expect(200);
|
||||
expect(actual.text).toMatch("Welcome to your Probot App");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(cwd);
|
||||
expect(actual.text).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// Redirect does not work because webhooks middleware is using root path
|
||||
describe("GET /", () => {
|
||||
it("redirects to /probot", () => {
|
||||
return request(server.expressApp)
|
||||
it("redirects to /probot", async () => {
|
||||
const server = await instantiateServer(__dirname);
|
||||
await request(server.expressApp)
|
||||
.get("/")
|
||||
.expect(302)
|
||||
.expect("location", "/probot");
|
||||
|
|
|
@ -1,15 +1,26 @@
|
|||
const createChannel = jest.fn().mockResolvedValue("mocked proxy URL");
|
||||
const updateDotenv = jest.fn().mockResolvedValue({});
|
||||
jest.mock("smee-client", () => ({ createChannel }));
|
||||
jest.mock("update-dotenv", () => updateDotenv);
|
||||
import { Stream } from "node:stream";
|
||||
|
||||
import nock from "nock";
|
||||
import { Stream } from "stream";
|
||||
import fetchMock from "fetch-mock";
|
||||
import { pino } from "pino";
|
||||
import request from "supertest";
|
||||
import pino from "pino";
|
||||
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { Probot, Server } from "../../src";
|
||||
import { setupAppFactory } from "../../src/apps/setup";
|
||||
import { Probot, Server } from "../../src/index.js";
|
||||
import { setupAppFactory } from "../../src/apps/setup.js";
|
||||
|
||||
const mocks = vi.hoisted(() => {
|
||||
return {
|
||||
createChannel: vi.fn().mockResolvedValue("mocked proxy URL"),
|
||||
updateDotenv: vi.fn().mockResolvedValue({}),
|
||||
};
|
||||
});
|
||||
vi.mock("smee-client", () => ({
|
||||
default: { createChannel: mocks.createChannel },
|
||||
createChannel: mocks.createChannel,
|
||||
}));
|
||||
vi.mock("update-dotenv", () => ({
|
||||
default: mocks.updateDotenv,
|
||||
}));
|
||||
|
||||
describe("Setup app", () => {
|
||||
let server: Server;
|
||||
|
@ -37,7 +48,7 @@ describe("Setup app", () => {
|
|||
});
|
||||
|
||||
afterEach(async () => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("logs", () => {
|
||||
|
@ -91,31 +102,98 @@ describe("Setup app", () => {
|
|||
|
||||
describe("GET /probot/setup", () => {
|
||||
it("returns a redirect", async () => {
|
||||
nock("https://api.github.com")
|
||||
.post("/app-manifests/123/conversions")
|
||||
.reply(201, {
|
||||
html_url: "/apps/my-app",
|
||||
id: "id",
|
||||
pem: "pem",
|
||||
webhook_secret: "webhook_secret",
|
||||
client_id: "Iv1.8a61f9b3a7aba766",
|
||||
client_secret: "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.postOnce("https://api.github.com/app-manifests/123/conversions", {
|
||||
status: 201,
|
||||
body: {
|
||||
html_url: "/apps/my-app",
|
||||
id: "id",
|
||||
pem: "pem",
|
||||
webhook_secret: "webhook_secret",
|
||||
client_id: "Iv1.8a61f9b3a7aba766",
|
||||
client_secret: "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
||||
},
|
||||
});
|
||||
|
||||
await request(server.expressApp)
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
log: pino(streamLogsToOutput),
|
||||
// workaround for https://github.com/probot/probot/issues/1512
|
||||
appId: 1,
|
||||
privateKey: "dummy value for setup, see #1512",
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
request: {
|
||||
fetch: async (url: string, options: { [key: string]: any }) => {
|
||||
return fetch(url, options);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await server.load(setupAppFactory(undefined, undefined));
|
||||
|
||||
const setupResponse = await request(server.expressApp)
|
||||
.get("/probot/setup")
|
||||
.query({ code: "123" })
|
||||
.expect(302)
|
||||
.expect("Location", "/apps/my-app/installations/new");
|
||||
|
||||
expect(createChannel).toHaveBeenCalledTimes(1);
|
||||
expect(updateDotenv.mock.calls).toMatchSnapshot();
|
||||
expect(setupResponse.text).toMatchSnapshot();
|
||||
|
||||
expect(mocks.createChannel).toHaveBeenCalledTimes(2);
|
||||
expect(mocks.updateDotenv.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("throws a 400 Error if code is not provided", async () => {
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
log: pino(streamLogsToOutput),
|
||||
// workaround for https://github.com/probot/probot/issues/1512
|
||||
appId: 1,
|
||||
privateKey: "dummy value for setup, see #1512",
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
});
|
||||
|
||||
await server.load(setupAppFactory(undefined, undefined));
|
||||
|
||||
const setupResponse = await request(server.expressApp)
|
||||
.get("/probot/setup")
|
||||
.expect(400);
|
||||
|
||||
expect(setupResponse.text).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("throws a 400 Error if code is an empty string", async () => {
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
log: pino(streamLogsToOutput),
|
||||
// workaround for https://github.com/probot/probot/issues/1512
|
||||
appId: 1,
|
||||
privateKey: "dummy value for setup, see #1512",
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
});
|
||||
|
||||
await server.load(setupAppFactory(undefined, undefined));
|
||||
|
||||
const setupResponse = await request(server.expressApp)
|
||||
.get("/probot/setup")
|
||||
.query({ code: "" })
|
||||
.expect(400);
|
||||
|
||||
expect(setupResponse.text).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /probot/import", () => {
|
||||
it("renders import.handlebars", async () => {
|
||||
await request(server.expressApp).get("/probot/import").expect(200);
|
||||
it("renders importView", async () => {
|
||||
const importView = await request(server.expressApp)
|
||||
.get("/probot/import")
|
||||
.expect(200);
|
||||
|
||||
expect(importView.text).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -134,7 +212,7 @@ describe("Setup app", () => {
|
|||
.expect(200)
|
||||
.expect("");
|
||||
|
||||
expect(updateDotenv.mock.calls).toMatchSnapshot();
|
||||
expect(mocks.updateDotenv.mock.calls).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("400 when keys are missing", async () => {
|
||||
|
@ -144,19 +222,25 @@ describe("Setup app", () => {
|
|||
webhook_secret: "baz",
|
||||
});
|
||||
|
||||
await request(server.expressApp)
|
||||
const importResponse = await request(server.expressApp)
|
||||
.post("/probot/import")
|
||||
.set("content-type", "application/json")
|
||||
.send(body)
|
||||
.expect(400);
|
||||
|
||||
expect(importResponse.text).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("GET /probot/success", () => {
|
||||
it("returns a 200 response", async () => {
|
||||
await request(server.expressApp).get("/probot/success").expect(200);
|
||||
const successResponse = await request(server.expressApp)
|
||||
.get("/probot/success")
|
||||
.expect(200);
|
||||
|
||||
expect(createChannel).toHaveBeenCalledTimes(1);
|
||||
expect(successResponse.text).toMatchSnapshot();
|
||||
|
||||
expect(mocks.createChannel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { isSupportedNodeVersion } from "../../src/helpers/is-supported-node-version.js";
|
||||
import { loadPackageJson } from "../../src/helpers/load-package-json.js";
|
||||
|
||||
describe("isSupportedNodeVersion", () => {
|
||||
const { engines } = loadPackageJson();
|
||||
it(`engines value is set to ">=18"`, () => {
|
||||
expect(engines!.node).toBe(">=18");
|
||||
});
|
||||
|
||||
it("returns true if node is bigger or equal v18", () => {
|
||||
expect(isSupportedNodeVersion("18.0.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("19.0.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("20.0.0")).toBe(true);
|
||||
expect(isSupportedNodeVersion("21.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
it("returns false if node is smaller than v18", () => {
|
||||
expect(isSupportedNodeVersion("17.0.0")).toBe(false);
|
||||
expect(isSupportedNodeVersion("17.9.0")).toBe(false);
|
||||
expect(isSupportedNodeVersion("17.9.9")).toBe(false);
|
||||
});
|
||||
});
|
|
@ -1,29 +1,31 @@
|
|||
import fs = require("fs");
|
||||
import path = require("path");
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
import WebhookExamples, { WebhookDefinition } from "@octokit/webhooks-examples";
|
||||
import nock from "nock";
|
||||
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||
import WebhookExamples from "@octokit/webhooks-examples";
|
||||
import type { WebhookDefinition } from "@octokit/webhooks-examples";
|
||||
import fetchMock from "fetch-mock";
|
||||
import { describe, expect, test, beforeEach, it, vi } from "vitest";
|
||||
|
||||
import { Context } from "../src";
|
||||
import { ProbotOctokit } from "../src/octokit/probot-octokit";
|
||||
import { PushEvent } from "@octokit/webhooks-types";
|
||||
import { Context } from "../src/index.js";
|
||||
import { ProbotOctokit } from "../src/octokit/probot-octokit.js";
|
||||
import type { PushEvent } from "@octokit/webhooks-types";
|
||||
|
||||
const pushEventPayload = (
|
||||
WebhookExamples.filter(
|
||||
(event) => event.name === "push"
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "push",
|
||||
)[0] as WebhookDefinition<"push">
|
||||
).examples[0];
|
||||
const issuesEventPayload = (
|
||||
WebhookExamples.filter(
|
||||
(event) => event.name === "issues"
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "issues",
|
||||
)[0] as WebhookDefinition<"issues">
|
||||
).examples[0];
|
||||
const pullRequestEventPayload = (
|
||||
WebhookExamples.filter(
|
||||
(event) => event.name === "pull_request"
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "pull_request",
|
||||
)[0] as WebhookDefinition<"pull_request">
|
||||
).examples[0];
|
||||
).examples[0] as WebhookEvent<"pull_request">["payload"];
|
||||
|
||||
describe("Context", () => {
|
||||
let event: WebhookEvent<"push"> = {
|
||||
|
@ -34,7 +36,7 @@ describe("Context", () => {
|
|||
let context: Context<"push"> = new Context<"push">(
|
||||
event,
|
||||
{} as any,
|
||||
{} as any
|
||||
{} as any,
|
||||
);
|
||||
|
||||
it("inherits the payload", () => {
|
||||
|
@ -97,8 +99,8 @@ describe("Context", () => {
|
|||
try {
|
||||
context.repo();
|
||||
} catch (e) {
|
||||
expect(e.message).toMatch(
|
||||
"context.repo() is not supported for this webhook event."
|
||||
expect((e as Error).message).toMatch(
|
||||
"context.repo() is not supported for this webhook event.",
|
||||
);
|
||||
}
|
||||
});
|
||||
|
@ -179,15 +181,15 @@ describe("Context", () => {
|
|||
owner: "muahaha",
|
||||
pull_number: 5,
|
||||
repo: "Hello-World",
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("config", () => {
|
||||
let octokit: InstanceType<typeof ProbotOctokit>;
|
||||
let octokit: ProbotOctokit;
|
||||
|
||||
function nockConfigResponseDataFile(fileName: string) {
|
||||
function getConfigFile(fileName: string) {
|
||||
const configPath = path.join(__dirname, "fixtures", "config", fileName);
|
||||
return fs.readFileSync(configPath, { encoding: "utf8" });
|
||||
}
|
||||
|
@ -202,9 +204,21 @@ describe("Context", () => {
|
|||
});
|
||||
|
||||
it("gets a valid configuration", async () => {
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
||||
.reply(200, nockConfigResponseDataFile("basic.yml"));
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.getOnce(
|
||||
"https://api.github.com/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml",
|
||||
getConfigFile("basic.yml"),
|
||||
);
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
retry: { enabled: false },
|
||||
throttle: { enabled: false },
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
const context = new Context(event, octokit, {} as any);
|
||||
|
||||
const config = await context.config("test-file.yml");
|
||||
expect(config).toEqual({
|
||||
|
@ -212,39 +226,67 @@ describe("Context", () => {
|
|||
baz: 11,
|
||||
foo: 5,
|
||||
});
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns null when the file and base repository are missing", async () => {
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
||||
.reply(404)
|
||||
.get("/repos/Codertocat/.github/contents/.github%2Ftest-file.yml")
|
||||
.reply(404);
|
||||
const NOT_FOUND_RESPONSE = {
|
||||
status: 404,
|
||||
body: {
|
||||
message: "Not Found",
|
||||
documentation_url:
|
||||
"https://docs.github.com/rest/reference/repos#get-repository-content",
|
||||
},
|
||||
};
|
||||
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.getOnce(
|
||||
"https://api.github.com/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml",
|
||||
NOT_FOUND_RESPONSE,
|
||||
)
|
||||
.getOnce(
|
||||
"https://api.github.com/repos/Codertocat/.github/contents/.github%2Ftest-file.yml",
|
||||
NOT_FOUND_RESPONSE,
|
||||
);
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
retry: { enabled: false },
|
||||
throttle: { enabled: false },
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
const context = new Context(event, octokit, {} as any);
|
||||
|
||||
expect(await context.config("test-file.yml")).toBe(null);
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("accepts deepmerge options", async () => {
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
||||
.reply(
|
||||
200,
|
||||
"foo:\n - name: master\n shouldChange: changed\n_extends: .github"
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.getOnce(
|
||||
"https://api.github.com/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml",
|
||||
"foo:\n - name: master\n shouldChange: changed\n_extends: .github",
|
||||
)
|
||||
.get("/repos/Codertocat/.github/contents/.github%2Ftest-file.yml")
|
||||
.reply(
|
||||
200,
|
||||
"foo:\n - name: develop\n - name: master\n shouldChange: should"
|
||||
.getOnce(
|
||||
"https://api.github.com/repos/Codertocat/.github/contents/.github%2Ftest-file.yml",
|
||||
"foo:\n - name: develop\n - name: master\n shouldChange: should",
|
||||
);
|
||||
|
||||
const customMerge = jest.fn(
|
||||
(_target: any[], _source: any[], _options: any): any[] => []
|
||||
const octokit = new ProbotOctokit({
|
||||
retry: { enabled: false },
|
||||
throttle: { enabled: false },
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
const context = new Context(event, octokit, {} as any);
|
||||
|
||||
const customMerge = vi.fn(
|
||||
(_target: any[], _source: any[], _options: any): any[] => [],
|
||||
);
|
||||
await context.config("test-file.yml", {}, { arrayMerge: customMerge });
|
||||
expect(customMerge).toHaveBeenCalled();
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -1,31 +1,56 @@
|
|||
import { createServer, IncomingMessage, ServerResponse } from "http";
|
||||
import Stream from "stream";
|
||||
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
||||
import Stream from "node:stream";
|
||||
|
||||
import pino from "pino";
|
||||
import { pino } from "pino";
|
||||
import getPort from "get-port";
|
||||
import got from "got";
|
||||
import { sign } from "@octokit/webhooks-methods";
|
||||
import { describe, expect, test, beforeEach } from "vitest";
|
||||
|
||||
import { createNodeMiddleware, createProbot, Probot } from "../src";
|
||||
import { ApplicationFunction } from "../src/types";
|
||||
import { createNodeMiddleware, createProbot, Probot } from "../src/index.js";
|
||||
import type { ApplicationFunction } from "../src/types.js";
|
||||
import WebhookExamples, {
|
||||
type WebhookDefinition,
|
||||
} from "@octokit/webhooks-examples";
|
||||
|
||||
const APP_ID = "1";
|
||||
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
const WEBHOOK_SECRET = "secret";
|
||||
const pushEvent = require("./fixtures/webhook/push.json");
|
||||
const pushEvent = (
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "push",
|
||||
)[0] as WebhookDefinition<"push">
|
||||
).examples[0];
|
||||
|
||||
describe("createNodeMiddleware", () => {
|
||||
let output: any[];
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
@ -61,7 +86,8 @@ describe("createNodeMiddleware", () => {
|
|||
|
||||
const body = JSON.stringify(pushEvent);
|
||||
|
||||
await got.post(`http://127.0.0.1:${port}`, {
|
||||
await fetch(`http://127.0.0.1:${port}/api/github/webhooks`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-github-event": "push",
|
||||
|
@ -76,6 +102,94 @@ describe("createNodeMiddleware", () => {
|
|||
server.close();
|
||||
});
|
||||
|
||||
test("with createProbot and setting the webhookPath via WEBHOOK_PATH to the root", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const app: ApplicationFunction = (app) => {
|
||||
app.on("push", (event) => {
|
||||
expect(event.name).toEqual("push");
|
||||
});
|
||||
};
|
||||
const middleware = createNodeMiddleware(app, {
|
||||
probot: createProbot({
|
||||
overrides: {
|
||||
log: pino(streamLogsToOutput),
|
||||
},
|
||||
env: {
|
||||
APP_ID,
|
||||
PRIVATE_KEY,
|
||||
WEBHOOK_SECRET,
|
||||
WEBHOOK_PATH: "/",
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const server = createServer(middleware);
|
||||
const port = await getPort();
|
||||
server.listen(port);
|
||||
|
||||
const body = JSON.stringify(pushEvent);
|
||||
|
||||
await fetch(`http://127.0.0.1:${port}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-github-event": "push",
|
||||
"x-github-delivery": "1",
|
||||
"x-hub-signature-256": await sign("secret", body),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
server.close();
|
||||
});
|
||||
|
||||
test("with createProbot and setting the webhookPath to the root via the deprecated webhooksPath", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const app: ApplicationFunction = (app) => {
|
||||
app.on("push", (event) => {
|
||||
expect(event.name).toEqual("push");
|
||||
});
|
||||
};
|
||||
const middleware = createNodeMiddleware(app, {
|
||||
webhooksPath: "/",
|
||||
probot: createProbot({
|
||||
overrides: {
|
||||
log: pino(streamLogsToOutput),
|
||||
},
|
||||
env: {
|
||||
APP_ID,
|
||||
PRIVATE_KEY,
|
||||
WEBHOOK_SECRET,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const server = createServer(middleware);
|
||||
const port = await getPort();
|
||||
server.listen(port);
|
||||
|
||||
const body = JSON.stringify(pushEvent);
|
||||
|
||||
await fetch(`http://127.0.0.1:${port}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-github-event": "push",
|
||||
"x-github-delivery": "1",
|
||||
"x-hub-signature-256": await sign("secret", body),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||
|
||||
server.close();
|
||||
}, 1000);
|
||||
|
||||
test("loads app only once", async () => {
|
||||
let counter = 0;
|
||||
const appFn = () => {
|
||||
|
@ -90,11 +204,11 @@ describe("createNodeMiddleware", () => {
|
|||
|
||||
middleware(
|
||||
{} as IncomingMessage,
|
||||
{ end() {}, writeHead() {} } as unknown as ServerResponse
|
||||
{ end() {}, writeHead() {} } as unknown as ServerResponse,
|
||||
);
|
||||
middleware(
|
||||
{} as IncomingMessage,
|
||||
{ end() {}, writeHead() {} } as unknown as ServerResponse
|
||||
{ end() {}, writeHead() {} } as unknown as ServerResponse,
|
||||
);
|
||||
|
||||
expect(counter).toEqual(1);
|
||||
|
|
|
@ -1,20 +1,62 @@
|
|||
import { createProbot, Probot } from "../src";
|
||||
import { captureLogOutput } from "./helpers/capture-log-output";
|
||||
import { createProbot, Probot } from "../src/index.js";
|
||||
import { captureLogOutput } from "./helpers/capture-log-output.js";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
const env = {
|
||||
APP_ID: "1",
|
||||
PRIVATE_KEY: `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
||||
MIIJKAIBAAKCAgEAu0E+tR6wfOAJZ4lASzRUmvorCgbI5nQyvZl3WLu6ko2pcEnq
|
||||
1t1/W/Yaovt9W8eMFVfoFXKhsHOAM5dFlktxOlcaUQiRYSO7fBbZYVNYoawnCRqD
|
||||
HKQ1oKC6B23EKfW5NH8NLaI/+QJFG7fpr0P4HkHghLsOe7rIUDt7EjRsSSRhM2+Y
|
||||
sFmRsnj0PWESWyI5exdKys0Mw25CmGsA27ltmebgHFYQ4ac+z0Esbjujcxec5wtn
|
||||
oAMT6jIBjEPHYTy0Cbe/wDN0cZkg6QyNC09lMnUx8vP1gwAVP20VXfjdFHZ8cR80
|
||||
ungLmBG0SWgVglqv52C5Gad2hEDsWyi28/XZ9/mNGatZJ1SSmA6+TSuSlrs/Dm0K
|
||||
hjOx21SdPAii38fBs6xtMk8d8WhGqwUR0nAVDdm1H/03BJssuh78xL5/WEcDZ2Gn
|
||||
QSQERNna/nP7uwbIXYORYLcPTY80RrYp6MCTrHydIArurGrtGW9f2RU2cP5+5SkV
|
||||
qvSSU6NefYYw55XyVXrIfkTZXJ4UejlnpWZ+syXbYIRn/CNBPbQa6OY/ZBUgSDKW
|
||||
xjiQQcr71ANeW41Od+k+TCiCkoK2fEPbtD/LXDXKZNTwzZqUA5ol//wOk+cDms9z
|
||||
A+vbA8IWP6TBBqxVMe8z8D7AVytQTNHPBf/33tNfneWvuElHP9CG3q8/FYkCAwEA
|
||||
AQKCAf9Punf4phh/EuTuMIIvgxiC5AFvQ3RGqzLvh2hJX6UQwUBjjxVuZuDTRvYQ
|
||||
bwPxEAWVENjASQ6PEp6DWOVIGNcc//k0h3Fe6tfo/dGQnuwd6i60sZUhnMk4mzaZ
|
||||
8yKSuw0gTPhPdcXHQDAsnSHifg4m0+XEneCMzfp8Ltc36RoyCktYmytn6rseQmG7
|
||||
wJkQNIJE5qXxs1y72TaBrw2ugEUqQiMp7XtCmPMlS5qfVMVDO8qSlUiJ2MWh8ai3
|
||||
ECTUQgRmHtaF/2KU+54HnFBxgFyWH1AlIbpnDKH/X3K5kDyReeGCSMcqnfJRzTf2
|
||||
CVsfJX3ABm7JfYP4y6vXJH7BYOxs7YMBEiR0o/7mhcBNbj8reEy42hUIaomQQNRE
|
||||
mw5iiHiCBE/P6Y46SFyddnwuwD9HVk9ojyz5A70OjZLEWBfRajLOqSEp8VO7aM7H
|
||||
YEQ00Jj9nNAKkaRh3BP9zEuL3dtYF//myr1QHgDCg0lsKacmJOFxxJwzmiTgvXFd
|
||||
y6ZajugDY//7kA4iXPmRY0/nIznyee9AiAUvf2kvJov/jL36HH8fFWFH+RVS3/+V
|
||||
BGM5hlWdVyGr+y+PNU6wTz637Qg/23GhwuF0Wi6qie0jertuzPW0RkUlOzX5y2v2
|
||||
p6mTTJxpOmXCPjq1UZQUz+KkUZuUVlWTRmL3l+133eh6jTr1AoIBAQDxKAXhDBxR
|
||||
kvgIomBF0Q9lQGkIpT6yw+YsuzL9WiJM0ZvvnES/2XqwGSMOfNHnwsrrKXxxgLRY
|
||||
vpN/5pEJK8TYrLyvargWW6qQptGqnkt4unb30EENgDT3SARKfhM9ytXaCn+JrJyI
|
||||
4yN0qAVDOEkv1TqP0oIjMO5QVqVEYhO8CAyKdBiXc0XY7FYZTYMLbHs7tkqZFHF+
|
||||
OgfEi6pnH61hFoCdPtskmjxlmPbwRP4K18J6rovlq3/KMbSw2NQEADFZUmaalcSa
|
||||
nn9O+0MkzvrCcanDmA1ZgZkd/06izo76u7vUoHdMflWoOAwBYvjQJntN7wUzX/3z
|
||||
QNiFg1HEDqtrAoIBAQDGx+EZz1RUI+6o+3Swy9yNQk4jGAueH1OXsdazn6lOpzBt
|
||||
YvG7BxIbyMfJuRBrIN7q0FiyRFSChXgjenD3aq5r84DAegMDHHL6bnLQTfnuzvHL
|
||||
oQ5TZ0i8a29V3FinamYEaFziZQuFs1nCPdnPd41GX3oaTvlYyfTc2J9UjxBtRIoA
|
||||
vTViJ2NKxaklFMEBhRoUsqQXT4Jh3a6+3r9xpFkaQ/LYRp8XzWXJntqwhy6+Nvf1
|
||||
B4CVYF9My3r4KGNa6UmnK7A92VqnkHuN4rAlDnu1Q0BZa5dy+vw+Kkxsg4qSoTAF
|
||||
41tCI5aJd5t+THQMAJmrOG9Wzfwk83g3V0oTzJPbAoIBAEqKJW8PUD2CoPoCPqG1
|
||||
4f1Y8F5EvWGCHcZbwoH+9zUpYPqqIbHvJfYCfwx+Vl89nX0coKNwtc3scikJenEM
|
||||
P1b95YCPCwGWKd12Qr5rGUbi09z7WPA0XarFbtYbrBTgekNgFVXXrba+BnqLaL0D
|
||||
S9PmI6jK14DLIg5hCcpeSl1HW6D8C5Hcho1rV52QkN3aFSk6ykoQwJfUlgwRY4Vm
|
||||
jC/DRdPU1uW0atC4fDN+D8wILsu+4e0GmoRD4ub6zmXCLX6/col7m35zWURvc6yP
|
||||
8YBio6eaex3cahiUjpjSIe2sU32Ab/+L2SwaztMq5V9pVZmcNM5RcGxc8dAq6/4e
|
||||
zqsCggEBAKKevNHvos6fAs1twd4tOUa7Gs9tCXwXprxwOfSDRvBYqK6khpv6Qd9H
|
||||
F+M4qmzp3FR/lEBq1DRfWpSzw501wnIAKLHOX455BLtKBlXRpQmwdXGgVeb3lTLI
|
||||
NbIpbMGxsroiYvK3tYBw5JqbHQi0hngu/eZt+2Ge/tp5wYdc7xRlQP0vzW96R6nR
|
||||
IPp8CxXiPR73snR7kG/d+uqdskMXL+nj8tTqmZbQa1hRxBkszpnAwIPN2mzaBbz+
|
||||
rqA78mRae+3uOOWwXpC9C8dcz7vRKHV3CjrdYW4oVJnK4vDXgFNK2M3IXU0zbiES
|
||||
H7xocXusNgs0RSnfpEraf9vOZoTiFYcCggEBANnrbN0StE2Beni2sWl9+9H2CbXS
|
||||
s4sFJ1Q+KGN1QdKXHQ28qc8eYbifF/gA7TaSXmSiEXWinTQQKRSfZ/Xj573RZtaf
|
||||
nvrsLNmx/Mxj/KPHKrlgep5aJ+JsHsb93PpHacG38OBgcWhzDf6PYLQilzY12jr7
|
||||
vAWZUbyCsH+5FYuz0ahl6Cef8w6IrFpvk0hW2aQsoVWvgH727D+uM48DZ9mpVy9I
|
||||
bHNB2yFIuUmmT92T7Pw28wJZ6Wd/3T+5s4CBe+FWplQcgquPGIFkq4dVxPpVg6uq
|
||||
wB98bfAGtcuCZWzgjgL67CS0pcNxadFA/TFo/NnynLBC4qRXSfFslKVE+Og=
|
||||
-----END RSA PRIVATE KEY-----`,
|
||||
WEBHOOK_SECRET: "secret",
|
||||
};
|
||||
// tslint:disable:no-empty
|
||||
describe("createProbot", () => {
|
||||
test("createProbot()", () => {
|
||||
const probot = createProbot({ env });
|
||||
|
@ -41,6 +83,20 @@ describe("createProbot", () => {
|
|||
expect(probot.log.level).toEqual("trace");
|
||||
});
|
||||
|
||||
test("defaults, custom host", () => {
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
...env,
|
||||
GHE_HOST: "github.acme-inc.com",
|
||||
GHE_PROTOCOL: "https",
|
||||
},
|
||||
});
|
||||
// @ts-expect-error This is private
|
||||
expect(probot.state.octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
||||
"https://github.acme-inc.com/api/v3",
|
||||
);
|
||||
});
|
||||
|
||||
test("env, overrides", () => {
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
|
@ -65,36 +121,34 @@ describe("createProbot", () => {
|
|||
});
|
||||
|
||||
test("env, logger message key", async () => {
|
||||
const outputData = await captureLogOutput(() => {
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
...env,
|
||||
LOG_LEVEL: "info",
|
||||
LOG_FORMAT: "json",
|
||||
LOG_MESSAGE_KEY: "myMessage",
|
||||
},
|
||||
defaults: { logLevel: "trace" },
|
||||
});
|
||||
|
||||
probot.log.info("Ciao");
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
...env,
|
||||
LOG_LEVEL: "info",
|
||||
LOG_FORMAT: "json",
|
||||
LOG_MESSAGE_KEY: "myMessage",
|
||||
},
|
||||
defaults: { logLevel: "trace" },
|
||||
});
|
||||
const outputData = await captureLogOutput(() => {
|
||||
probot.log.info("Ciao");
|
||||
}, probot.log);
|
||||
expect(JSON.parse(outputData).myMessage).toEqual("Ciao");
|
||||
});
|
||||
|
||||
test("env, octokit logger set", async () => {
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
...env,
|
||||
LOG_LEVEL: "info",
|
||||
LOG_FORMAT: "json",
|
||||
LOG_MESSAGE_KEY: "myMessage",
|
||||
},
|
||||
});
|
||||
const outputData = await captureLogOutput(async () => {
|
||||
const probot = createProbot({
|
||||
env: {
|
||||
...env,
|
||||
LOG_LEVEL: "info",
|
||||
LOG_FORMAT: "json",
|
||||
LOG_MESSAGE_KEY: "myMessage",
|
||||
},
|
||||
});
|
||||
|
||||
const octokit = await probot.auth();
|
||||
octokit.log.info("Ciao");
|
||||
});
|
||||
}, probot.log);
|
||||
expect(JSON.parse(outputData)).toMatchObject({
|
||||
myMessage: "Ciao",
|
||||
name: "octokit",
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import { describe, it } from "vitest";
|
||||
|
||||
describe("Deprecations", () => {
|
||||
it("no deprecations exists at this point", () => {});
|
||||
});
|
||||
|
|
|
@ -2,22 +2,20 @@ import execa from "execa";
|
|||
import getPort from "get-port";
|
||||
|
||||
import { sign } from "@octokit/webhooks-methods";
|
||||
import bodyParser from "body-parser";
|
||||
import express from "express";
|
||||
import got from "got";
|
||||
|
||||
jest.setTimeout(10000);
|
||||
import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
|
||||
|
||||
/**
|
||||
* In these tests we are starting probot apps by running "npm run [path to app.js]" using ghub.io/execa.
|
||||
* This allows us to pass dynamic environment variables for configuration.
|
||||
*
|
||||
* We also spawn a mock server which receives the Octokit requests from the app and uses jest assertions
|
||||
* We also spawn a mock server which receives the Octokit requests from the app and uses vitest assertions
|
||||
* to verify they are what we expect
|
||||
*/
|
||||
describe("end-to-end-tests", () => {
|
||||
let server: any;
|
||||
let probotProcess: any;
|
||||
let probotProcess: execa.ExecaChildProcess<string> | null;
|
||||
let probotPort: number;
|
||||
let mockServerPort: number;
|
||||
|
||||
|
@ -34,7 +32,7 @@ describe("end-to-end-tests", () => {
|
|||
|
||||
it("hello-world app", async () => {
|
||||
const app = express();
|
||||
const httpMock = jest
|
||||
const httpMock = vi
|
||||
.fn()
|
||||
.mockImplementationOnce((req, res) => {
|
||||
expect(req.method).toEqual("POST");
|
||||
|
@ -52,37 +50,39 @@ describe("end-to-end-tests", () => {
|
|||
.mockImplementationOnce((req, res) => {
|
||||
expect(req.method).toEqual("POST");
|
||||
expect(req.path).toEqual(
|
||||
"/repos/octocat/hello-world/issues/1/comments"
|
||||
"/repos/octocat/hello-world/issues/1/comments",
|
||||
);
|
||||
expect(req.body).toStrictEqual({ body: "Hello World!" });
|
||||
|
||||
res.status(201).json({});
|
||||
});
|
||||
|
||||
// tslint:disable-next-line
|
||||
app.use(bodyParser.json());
|
||||
app.use(express.json());
|
||||
app.use("/api/v3", httpMock);
|
||||
server = app.listen(mockServerPort);
|
||||
|
||||
probotProcess = execa(
|
||||
"bin/probot.js",
|
||||
["run", "./test/e2e/hello-world.js"],
|
||||
"node",
|
||||
["bin/probot.js", "run", "./test/e2e/hello-world.cjs"],
|
||||
{
|
||||
env: {
|
||||
APP_ID: "1",
|
||||
PRIVATE_KEY:
|
||||
"-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY\nFl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo\n/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY\nwQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv\nA1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq\nNKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U\nr1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=\n-----END RSA PRIVATE KEY-----",
|
||||
PRIVATE_KEY: `-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH\nlWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE\np6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ\nrO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS\nye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX\ngzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB\nK1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4\n4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL\nUlrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse\nhnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD\n8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC\n21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3\nxs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz\nc/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm\nI3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy\nMa+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+\nns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT\nu/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6\ny5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC\nS4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW\nCQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX\nZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn\n7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0\n9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh\nx//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==\n-----END RSA PRIVATE KEY-----`,
|
||||
WEBHOOK_SECRET: "test",
|
||||
PORT: String(probotPort),
|
||||
GHE_HOST: `127.0.0.1:${mockServerPort}`,
|
||||
GHE_PROTOCOL: "http",
|
||||
LOG_LEVEL: "trace",
|
||||
WEBHOOK_PATH: "/",
|
||||
},
|
||||
}
|
||||
stdio: "inherit",
|
||||
},
|
||||
);
|
||||
|
||||
// give probot a moment to start
|
||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||
// the probot process should be successfully started
|
||||
expect(probotProcess.exitCode).toBeNull();
|
||||
|
||||
// send webhook event request
|
||||
const body = JSON.stringify({
|
||||
|
@ -102,7 +102,8 @@ describe("end-to-end-tests", () => {
|
|||
});
|
||||
|
||||
try {
|
||||
await got.post(`http://127.0.0.1:${probotPort}`, {
|
||||
await fetch(`http://127.0.0.1:${probotPort}/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-github-event": "issues",
|
||||
|
@ -113,7 +114,9 @@ describe("end-to-end-tests", () => {
|
|||
});
|
||||
} catch (error) {
|
||||
probotProcess.cancel();
|
||||
console.log((await probotProcess).stdout);
|
||||
const awaitedProcess = await probotProcess;
|
||||
console.log(awaitedProcess.stdout);
|
||||
console.log(awaitedProcess.stderr);
|
||||
}
|
||||
|
||||
expect(httpMock).toHaveBeenCalledTimes(2);
|
||||
|
|
|
@ -13,9 +13,7 @@ module.exports = (app) => {
|
|||
const params = context.issue({ body: "Hello World!" });
|
||||
|
||||
// Post a comment on the issue
|
||||
await context.octokit.issues.createComment(params).then(
|
||||
() => console.log("issue comment created"),
|
||||
(error) => console.log("not ok", error)
|
||||
);
|
||||
await context.octokit.issues.createComment(params);
|
||||
console.log("issue comment created");
|
||||
});
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
{
|
|
@ -0,0 +1 @@
|
|||
"null"
|
|
@ -1,2 +1 @@
|
|||
// tslint:disable-next-line:no-empty
|
||||
export = () => {};
|
||||
export default () => {};
|
||||
|
|
|
@ -1,18 +1,27 @@
|
|||
import SonicBoom from "sonic-boom";
|
||||
import { symbols as pinoSymbols } from "pino";
|
||||
import type { Logger } from "pino";
|
||||
import { type SpyInstance, vi } from "vitest";
|
||||
|
||||
export async function captureLogOutput(action: () => any): Promise<string> {
|
||||
export async function captureLogOutput(
|
||||
action: () => any,
|
||||
log: Logger,
|
||||
): Promise<string> {
|
||||
let outputData = "";
|
||||
|
||||
const sbWrite = SonicBoom.prototype.write;
|
||||
SonicBoom.prototype.write = function (data) {
|
||||
const stdoutSpy: SpyInstance = vi.spyOn(
|
||||
// @ts-expect-error
|
||||
log[pinoSymbols.streamSym],
|
||||
"write",
|
||||
);
|
||||
stdoutSpy.mockImplementation((data) => {
|
||||
outputData += data;
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
await action();
|
||||
|
||||
return outputData;
|
||||
} finally {
|
||||
SonicBoom.prototype.write = sbWrite;
|
||||
stdoutSpy.mockRestore();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { isProduction } from "../../src/helpers/is-production";
|
||||
import { isProduction } from "../../src/helpers/is-production.js";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("isProduction", () => {
|
||||
it("returns true if the NODE_ENV is set to production", () => {
|
||||
|
@ -11,6 +12,6 @@ describe("isProduction", () => {
|
|||
(value) => {
|
||||
process.env.NODE_ENV = value;
|
||||
expect(isProduction()).toBe(false);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,62 @@
|
|||
import { loadPackageJson } from "../../src/helpers/load-package-json.js";
|
||||
import { resolve } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
describe("loadPackageJson", () => {
|
||||
it("returns empty object if filepath is invalid", () => {
|
||||
expect(JSON.stringify(loadPackageJson("/invalid/path/package.json"))).toBe(
|
||||
"{}",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns package.json content", () => {
|
||||
expect(loadPackageJson()).toHaveProperty("name");
|
||||
expect(loadPackageJson().name).toBe("probot");
|
||||
});
|
||||
|
||||
it("returns package.json content if filepath is valid", () => {
|
||||
expect(
|
||||
loadPackageJson(resolve(process.cwd(), "package.json")),
|
||||
).toHaveProperty("name");
|
||||
expect(loadPackageJson(resolve(process.cwd(), "package.json")).name).toBe(
|
||||
"probot",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns empty object if file is invalid", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
loadPackageJson(
|
||||
resolve(
|
||||
process.cwd(),
|
||||
"test/fixtures/load-package-json/invalid.json",
|
||||
),
|
||||
),
|
||||
),
|
||||
).toBe("{}");
|
||||
});
|
||||
it("returns empty object if file is empty string", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
loadPackageJson(
|
||||
resolve(
|
||||
process.cwd(),
|
||||
"test/fixtures/load-package-json/empty-string.json",
|
||||
),
|
||||
),
|
||||
),
|
||||
).toBe("{}");
|
||||
});
|
||||
it("returns empty object if file is 'null'", () => {
|
||||
expect(
|
||||
JSON.stringify(
|
||||
loadPackageJson(
|
||||
resolve(
|
||||
process.cwd(),
|
||||
"test/fixtures/load-package-json/empty-string.json",
|
||||
),
|
||||
),
|
||||
),
|
||||
).toBe("{}");
|
||||
});
|
||||
});
|
|
@ -0,0 +1,141 @@
|
|||
import { Writable } from "node:stream";
|
||||
import { ManifestCreation } from "../../src/manifest-creation.js";
|
||||
import { describe, test, expect, afterEach } from "vitest";
|
||||
import getPort from "get-port";
|
||||
import { ApplicationFunction, Probot, Server } from "../../src/index.js";
|
||||
import { pino } from "pino";
|
||||
import WebhookExamples, {
|
||||
type WebhookDefinition,
|
||||
} from "@octokit/webhooks-examples";
|
||||
import { sign } from "@octokit/webhooks-methods";
|
||||
|
||||
describe("smee-client", () => {
|
||||
afterEach(async () => {
|
||||
delete process.env.WEBHOOK_PROXY_URL;
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 5000));
|
||||
});
|
||||
|
||||
describe("ManifestCreation", () => {
|
||||
test("create a smee proxy", async () => {
|
||||
await new ManifestCreation().createWebhookChannel();
|
||||
|
||||
expect(process.env.WEBHOOK_PROXY_URL).toMatch(
|
||||
/^https:\/\/smee\.io\/[0-9a-zA-Z]{10,}$/,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Server", () => {
|
||||
const APP_ID = "1";
|
||||
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
const WEBHOOK_SECRET = "secret";
|
||||
|
||||
const pushEvent = (
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "push",
|
||||
)[0] as WebhookDefinition<"push">
|
||||
).examples[0];
|
||||
|
||||
let output: any[] = [];
|
||||
const streamLogsToOutput = new Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
||||
test(
|
||||
"with createProbot and setting the webhookPath via WEBHOOK_PATH to the root",
|
||||
async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const promise: {
|
||||
resolve: any;
|
||||
reject: any;
|
||||
promise: any;
|
||||
} = {
|
||||
resolve: null,
|
||||
reject: null,
|
||||
promise: null,
|
||||
};
|
||||
|
||||
promise.promise = new Promise((resolve, reject) => {
|
||||
promise.resolve = resolve;
|
||||
promise.reject = reject;
|
||||
});
|
||||
|
||||
const WEBHOOK_PROXY_URL =
|
||||
await new ManifestCreation().createWebhookChannel();
|
||||
|
||||
const app: ApplicationFunction = (app) => {
|
||||
app.on("push", (event) => {
|
||||
expect(event.name).toEqual("push");
|
||||
promise.resolve();
|
||||
});
|
||||
};
|
||||
|
||||
const port = await getPort();
|
||||
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
appId: APP_ID,
|
||||
privateKey: PRIVATE_KEY,
|
||||
secret: WEBHOOK_SECRET,
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
port,
|
||||
webhookProxy: WEBHOOK_PROXY_URL,
|
||||
webhookPath: "/",
|
||||
});
|
||||
|
||||
server.load(app);
|
||||
|
||||
await server.start();
|
||||
|
||||
const body = JSON.stringify(pushEvent);
|
||||
|
||||
await fetch(`${WEBHOOK_PROXY_URL}/`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-github-event": "push",
|
||||
"x-github-delivery": "1",
|
||||
"x-hub-signature-256": await sign(WEBHOOK_SECRET, body),
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
await promise.promise;
|
||||
|
||||
server.stop();
|
||||
},
|
||||
{ retry: 10, timeout: 3000 },
|
||||
);
|
||||
});
|
||||
});
|
|
@ -1,12 +1,21 @@
|
|||
import fs from "fs";
|
||||
import nock from "nock";
|
||||
import pkg from "../package.json";
|
||||
import { ManifestCreation } from "../src/manifest-creation";
|
||||
import response from "./fixtures/setup/response.json";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import fetchMock from "fetch-mock";
|
||||
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
import { ManifestCreation } from "../src/manifest-creation.js";
|
||||
import { loadPackageJson } from "../src/helpers/load-package-json.js";
|
||||
|
||||
describe("ManifestCreation", () => {
|
||||
let setup: ManifestCreation;
|
||||
|
||||
const pkg = loadPackageJson();
|
||||
const response = JSON.parse(
|
||||
fs.readFileSync(
|
||||
path.resolve(process.cwd(), "./test/fixtures/setup/response.json"),
|
||||
"utf8",
|
||||
),
|
||||
);
|
||||
beforeEach(() => {
|
||||
setup = new ManifestCreation();
|
||||
});
|
||||
|
@ -17,10 +26,10 @@ describe("ManifestCreation", () => {
|
|||
delete process.env.PROJECT_DOMAIN;
|
||||
delete process.env.WEBHOOK_PROXY_URL;
|
||||
|
||||
setup.updateEnv = jest.fn();
|
||||
setup.updateEnv = vi.fn();
|
||||
|
||||
const SmeeClient: typeof import("smee-client") = require("smee-client");
|
||||
SmeeClient.createChannel = jest
|
||||
SmeeClient.createChannel = vi
|
||||
.fn()
|
||||
.mockReturnValue("https://smee.io/1234abc");
|
||||
});
|
||||
|
@ -52,21 +61,21 @@ describe("ManifestCreation", () => {
|
|||
|
||||
test("creates an app url", () => {
|
||||
expect(setup.createAppUrl).toEqual(
|
||||
"https://github.com/settings/apps/new"
|
||||
"https://github.com/settings/apps/new",
|
||||
);
|
||||
});
|
||||
|
||||
test("creates an app url when github org is set", () => {
|
||||
process.env.GH_ORG = "testorg";
|
||||
expect(setup.createAppUrl).toEqual(
|
||||
"https://github.com/organizations/testorg/settings/apps/new"
|
||||
"https://github.com/organizations/testorg/settings/apps/new",
|
||||
);
|
||||
});
|
||||
|
||||
test("creates an app url when github host env is set", () => {
|
||||
process.env.GHE_HOST = "hiimbex.github.com";
|
||||
expect(setup.createAppUrl).toEqual(
|
||||
"https://hiimbex.github.com/settings/apps/new"
|
||||
"https://hiimbex.github.com/settings/apps/new",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -74,7 +83,7 @@ describe("ManifestCreation", () => {
|
|||
process.env.GHE_HOST = "hiimbex.github.com";
|
||||
process.env.GH_ORG = "testorg";
|
||||
expect(setup.createAppUrl).toEqual(
|
||||
"https://hiimbex.github.com/organizations/testorg/settings/apps/new"
|
||||
"https://hiimbex.github.com/organizations/testorg/settings/apps/new",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -82,14 +91,14 @@ describe("ManifestCreation", () => {
|
|||
process.env.GHE_HOST = "hiimbex.github.com";
|
||||
process.env.GHE_PROTOCOL = "http";
|
||||
expect(setup.createAppUrl).toEqual(
|
||||
"http://hiimbex.github.com/settings/apps/new"
|
||||
"http://hiimbex.github.com/settings/apps/new",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("createAppFromCode", () => {
|
||||
beforeEach(() => {
|
||||
setup.updateEnv = jest.fn();
|
||||
setup.updateEnv = vi.fn();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -100,11 +109,18 @@ describe("ManifestCreation", () => {
|
|||
});
|
||||
|
||||
test("creates an app from a code", async () => {
|
||||
nock("https://api.github.com")
|
||||
.post("/app-manifests/123abc/conversions")
|
||||
.reply(200, response);
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.postOnce("https://api.github.com/app-manifests/123abc/conversions", {
|
||||
status: 200,
|
||||
body: response,
|
||||
});
|
||||
|
||||
const createdApp = await setup.createAppFromCode("123abc");
|
||||
const createdApp = await setup.createAppFromCode("123abc", {
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
expect(createdApp).toEqual("https://github.com/apps/testerino0000000");
|
||||
// expect dotenv to be called with id, webhook_secret, pem
|
||||
expect(setup.updateEnv).toHaveBeenCalledWith({
|
||||
|
@ -118,11 +134,21 @@ describe("ManifestCreation", () => {
|
|||
test("creates an app from a code when github host env is set", async () => {
|
||||
process.env.GHE_HOST = "swinton.github.com";
|
||||
|
||||
nock("https://swinton.github.com")
|
||||
.post("/api/v3/app-manifests/123abc/conversions")
|
||||
.reply(200, response);
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.postOnce(
|
||||
"https://swinton.github.com/api/v3/app-manifests/123abc/conversions",
|
||||
{
|
||||
status: 200,
|
||||
body: response,
|
||||
},
|
||||
);
|
||||
|
||||
const createdApp = await setup.createAppFromCode("123abc");
|
||||
const createdApp = await setup.createAppFromCode("123abc", {
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
expect(createdApp).toEqual("https://github.com/apps/testerino0000000");
|
||||
// expect dotenv to be called with id, webhook_secret, pem
|
||||
expect(setup.updateEnv).toHaveBeenCalledWith({
|
||||
|
@ -136,24 +162,24 @@ describe("ManifestCreation", () => {
|
|||
|
||||
describe("getManifest", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test("creates an app from a code", () => {
|
||||
// checks that getManifest returns a JSON.stringified manifest
|
||||
expect(setup.getManifest(pkg, "localhost://3000")).toEqual(
|
||||
'{"description":"A framework for building GitHub Apps to automate and improve your workflow","hook_attributes":{"url":"localhost://3000/"},"name":"probot","public":true,"redirect_url":"localhost://3000/probot/setup","url":"https://probot.github.io","version":"v1"}'
|
||||
'{"description":"A framework for building GitHub Apps to automate and improve your workflow","hook_attributes":{"url":"localhost://3000/"},"name":"probot","public":true,"redirect_url":"localhost://3000/probot/setup","url":"https://probot.github.io","version":"v1"}',
|
||||
);
|
||||
});
|
||||
|
||||
test("creates an app from a code with overrided values from app.yml", () => {
|
||||
const appYaml =
|
||||
"name: cool-app\ndescription: A description for a cool app";
|
||||
jest.spyOn(fs, "readFileSync").mockReturnValue(appYaml);
|
||||
vi.spyOn(fs, "readFileSync").mockReturnValue(appYaml);
|
||||
|
||||
// checks that getManifest returns the correct JSON.stringified manifest
|
||||
expect(setup.getManifest(pkg, "localhost://3000")).toEqual(
|
||||
'{"description":"A description for a cool app","hook_attributes":{"url":"localhost://3000/"},"name":"cool-app","public":true,"redirect_url":"localhost://3000/probot/setup","url":"https://probot.github.io","version":"v1"}'
|
||||
'{"description":"A description for a cool app","hook_attributes":{"url":"localhost://3000/"},"name":"cool-app","public":true,"redirect_url":"localhost://3000/probot/setup","url":"https://probot.github.io","version":"v1"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import nock from "nock";
|
||||
import { ProbotOctokit } from "../src/octokit/probot-octokit";
|
||||
|
||||
type Options = ConstructorParameters<typeof ProbotOctokit>[0];
|
||||
import fetchMock from "fetch-mock";
|
||||
import { ProbotOctokit } from "../src/octokit/probot-octokit.js";
|
||||
import type { RequestError } from "@octokit/types";
|
||||
import type { OctokitOptions } from "../src/types.js";
|
||||
import { describe, expect, test, vi, it } from "vitest";
|
||||
|
||||
describe("ProbotOctokit", () => {
|
||||
let octokit: InstanceType<typeof ProbotOctokit>;
|
||||
|
||||
const defaultOptions: Options = {
|
||||
const defaultOptions: OctokitOptions = {
|
||||
retry: {
|
||||
// disable retries to test error states
|
||||
enabled: false,
|
||||
|
@ -17,177 +16,455 @@ describe("ProbotOctokit", () => {
|
|||
},
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
octokit = new ProbotOctokit(defaultOptions);
|
||||
});
|
||||
|
||||
test("works without options", async () => {
|
||||
octokit = new ProbotOctokit();
|
||||
const user = { login: "ohai" };
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.getOnce("https://api.github.com/user", '{"login": "ohai"}');
|
||||
|
||||
nock("https://api.github.com").get("/user").reply(200, user);
|
||||
expect((await octokit.users.getAuthenticated({})).data).toEqual(user);
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
|
||||
expect((await octokit.users.getAuthenticated({})).data).toEqual(
|
||||
'{"login": "ohai"}',
|
||||
);
|
||||
});
|
||||
|
||||
test("logs request errors", async () => {
|
||||
nock("https://api.github.com").get("/").reply(500, {});
|
||||
const fetch = fetchMock.sandbox().getOnce("https://api.github.com/", {
|
||||
status: 500,
|
||||
body: {
|
||||
message: "Internal Server Error",
|
||||
documentation_url:
|
||||
"https://docs.github.com/rest/reference/repos#get-repository-content",
|
||||
},
|
||||
});
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
|
||||
try {
|
||||
await octokit.request("/");
|
||||
throw new Error("should throw");
|
||||
} catch (error) {
|
||||
expect(error.status).toBe(500);
|
||||
expect((error as RequestError).status).toBe(500);
|
||||
}
|
||||
});
|
||||
|
||||
describe("with retry enabled", () => {
|
||||
beforeEach(() => {
|
||||
const options: Options = {
|
||||
...defaultOptions,
|
||||
retry: {
|
||||
enabled: true,
|
||||
},
|
||||
};
|
||||
test("with retry enabled retries failed requests", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
octokit = new ProbotOctokit(options);
|
||||
});
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
retry: {
|
||||
enabled: true,
|
||||
},
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
expect(url).toEqual("https://api.github.com/");
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
test("retries failed requests", async () => {
|
||||
nock("https://api.github.com").get("/").once().reply(500, {});
|
||||
|
||||
nock("https://api.github.com").get("/").once().reply(200, {});
|
||||
|
||||
const response = await octokit.request("/");
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
});
|
||||
|
||||
describe("with throttling enabled", () => {
|
||||
beforeEach(() => {
|
||||
const options: Options = {
|
||||
...defaultOptions,
|
||||
throttle: {
|
||||
enabled: true,
|
||||
minimumAbuseRetryAfter: 1,
|
||||
onRateLimit() {
|
||||
return true;
|
||||
},
|
||||
onAbuseLimit() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
octokit = new ProbotOctokit(options);
|
||||
});
|
||||
|
||||
test("retries requests when being rate limited", async () => {
|
||||
nock("https://api.github.com")
|
||||
.get("/")
|
||||
.reply(
|
||||
403,
|
||||
{},
|
||||
{
|
||||
"X-RateLimit-Limit": "60",
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": `${new Date().getTime() / 1000}`,
|
||||
if (callCount++ === 0) {
|
||||
return Promise.reject({});
|
||||
}
|
||||
)
|
||||
|
||||
.get("/")
|
||||
.reply(200, {});
|
||||
|
||||
const { status } = await octokit.request("/");
|
||||
expect(status).toBe(200);
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
test("retries requests when hitting the abuse limiter", async () => {
|
||||
nock("https://api.github.com").get("/").once().reply(403, {
|
||||
message:
|
||||
"The throttle plugin just looks for the word abuse in the error message",
|
||||
});
|
||||
|
||||
nock("https://api.github.com").get("/").once().reply(200, {});
|
||||
|
||||
const response = await octokit.request("/");
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
const response = await octokit.request("/");
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
describe("paginate", () => {
|
||||
// Prepare an array of issue objects
|
||||
const issues = new Array(5).fill(0).map((_, i, arr) => {
|
||||
return {
|
||||
id: i,
|
||||
number: i,
|
||||
title: `Issue number ${i}`,
|
||||
};
|
||||
test("with throttling enabled retries requests when being rate limited", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
throttle: {
|
||||
enabled: true,
|
||||
fallbackSecondaryRateRetryAfter: 1,
|
||||
onRateLimit() {
|
||||
return true;
|
||||
},
|
||||
onSecondaryRateLimit() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
expect(url).toEqual("https://api.github.com/");
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
if (callCount++ === 0) {
|
||||
return Promise.resolve({
|
||||
status: 403,
|
||||
headers: new Headers({
|
||||
"X-RateLimit-Limit": "60",
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": `${new Date().getTime() / 1000}`,
|
||||
}),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
nock("https://api.github.com")
|
||||
.get("/repos/JasonEtco/pizza/issues?per_page=1")
|
||||
.reply(200, [issues[0]], {
|
||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=2>; rel="next"',
|
||||
})
|
||||
.get("/repositories/123/issues?per_page=1&page=2")
|
||||
.reply(200, [issues[1]], {
|
||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=3>; rel="next"',
|
||||
})
|
||||
.get("/repositories/123/issues?per_page=1&page=3")
|
||||
.reply(200, [issues[2]], {
|
||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=4>; rel="next"',
|
||||
})
|
||||
.get("/repositories/123/issues?per_page=1&page=4")
|
||||
.reply(200, [issues[3]], {
|
||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=5>; rel="next"',
|
||||
})
|
||||
.get("/repositories/123/issues?per_page=1&page=5")
|
||||
.reply(200, [issues[4]], {
|
||||
link: "",
|
||||
});
|
||||
const { status } = await octokit.request("/");
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
test("with throttling enabled using default onPrimaryRateLimit", async () => {
|
||||
expect.assertions(14);
|
||||
let callCount = 0;
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
// @ts-expect-error just need to mock the warn method
|
||||
log: {
|
||||
warn(message) {
|
||||
expect(message).toEqual(
|
||||
'Rate limit hit with "GET /", retrying in 1 seconds.',
|
||||
);
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
throttle: {
|
||||
enabled: true,
|
||||
fallbackSecondaryRateRetryAfter: 1,
|
||||
},
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
expect(url).toEqual("https://api.github.com/");
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
if (callCount++ === 0) {
|
||||
return Promise.resolve({
|
||||
status: 403,
|
||||
headers: new Headers({
|
||||
"X-RateLimit-Limit": "60",
|
||||
"X-RateLimit-Remaining": "0",
|
||||
"X-RateLimit-Reset": `${new Date().getTime() / 1000}`,
|
||||
}),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("returns an array of pages", async () => {
|
||||
const spy = jest.fn();
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
}),
|
||||
spy
|
||||
);
|
||||
expect(Array.isArray(res)).toBeTruthy();
|
||||
expect(res.length).toBe(5);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
const { status } = await octokit.request("/");
|
||||
expect(status).toBe(200);
|
||||
});
|
||||
|
||||
test("with throttling enabled retries requests when hitting the secondary rate limiter", async () => {
|
||||
let callCount = 0;
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
throttle: {
|
||||
enabled: true,
|
||||
fallbackSecondaryRateRetryAfter: 1,
|
||||
onRateLimit() {
|
||||
return true;
|
||||
},
|
||||
onSecondaryRateLimit() {
|
||||
return true;
|
||||
},
|
||||
},
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
expect(url).toEqual("https://api.github.com/");
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
if (callCount++ === 0) {
|
||||
return Promise.resolve({
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
"The throttle plugin just looks for the word secondary rate in the error message",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("stops iterating if the done() function is called in the callback", async () => {
|
||||
const spy = jest.fn((response, done) => {
|
||||
if (response.data[0].id === 2) done();
|
||||
}) as any;
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
}),
|
||||
spy
|
||||
);
|
||||
expect(res.length).toBe(3);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
const response = await octokit.request("/");
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
test("with throttling enabled using default onSecondaryRateLimit", async () => {
|
||||
expect.assertions(14);
|
||||
let callCount = 0;
|
||||
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
// @ts-expect-error just need to mock the warn method
|
||||
log: {
|
||||
warn(message) {
|
||||
expect(message).toEqual(
|
||||
'Secondary Rate limit hit with "GET /", retrying in 1 seconds.',
|
||||
);
|
||||
},
|
||||
},
|
||||
// @ts-expect-error
|
||||
throttle: {
|
||||
enabled: true,
|
||||
fallbackSecondaryRateRetryAfter: 1,
|
||||
},
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
expect(url).toEqual("https://api.github.com/");
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
if (callCount++ === 0) {
|
||||
return Promise.resolve({
|
||||
status: 403,
|
||||
headers: new Headers(),
|
||||
text: () =>
|
||||
Promise.resolve(
|
||||
"The throttle plugin just looks for the word secondary rate in the error message",
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
headers: new Headers(),
|
||||
text: () => Promise.resolve("{}"),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it("maps the responses to data by default", async () => {
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
})
|
||||
);
|
||||
expect(res).toEqual(issues);
|
||||
const response = await octokit.request("/");
|
||||
expect(response.status).toBe(200);
|
||||
});
|
||||
|
||||
// Prepare an array of issue objects
|
||||
const issues = new Array(5).fill(0).map((_, i) => {
|
||||
return {
|
||||
id: i,
|
||||
number: i,
|
||||
title: `Issue number ${i}`,
|
||||
};
|
||||
});
|
||||
|
||||
it("paginate returns an array of pages", async () => {
|
||||
let callCount = 0;
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
if (callCount === 0) {
|
||||
expect(url).toEqual(
|
||||
"https://api.github.com/repos/JasonEtco/pizza/issues?per_page=1",
|
||||
);
|
||||
} else {
|
||||
expect(url).toMatch(
|
||||
new RegExp(
|
||||
"^https://api\\.github\\.com/repositories/[0-9]+/issues\\?per_page=[0-9]+&page=[0-9]+$",
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
text: () => Promise.resolve([issues[callCount++]]),
|
||||
headers: new Headers({
|
||||
link:
|
||||
callCount !== 4
|
||||
? `link: '<https://api.github.com/repositories/123/issues?per_page=1&page=${callCount}>; rel="next"',`
|
||||
: "",
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const spy = vi.fn();
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
}),
|
||||
spy,
|
||||
);
|
||||
expect(Array.isArray(res)).toBeTruthy();
|
||||
expect(res.length).toBe(5);
|
||||
expect(spy).toHaveBeenCalledTimes(5);
|
||||
});
|
||||
|
||||
it("paginate stops iterating if the done() function is called in the callback", async () => {
|
||||
let callCount = 0;
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
if (callCount === 0) {
|
||||
expect(url).toEqual(
|
||||
"https://api.github.com/repos/JasonEtco/pizza/issues?per_page=1",
|
||||
);
|
||||
} else {
|
||||
expect(url).toMatch(
|
||||
new RegExp(
|
||||
"^https://api\\.github\\.com/repositories/[0-9]+/issues\\?per_page=[0-9]+&page=[0-9]+$",
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
text: () => Promise.resolve([issues[callCount++]]),
|
||||
headers: new Headers({
|
||||
link:
|
||||
callCount !== 4
|
||||
? `link: '<https://api.github.com/repositories/123/issues?per_page=1&page=${callCount}>; rel="next"',`
|
||||
: "",
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const spy = vi.fn((response, done) => {
|
||||
if (response.data[0].id === 2) done();
|
||||
}) as any;
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
}),
|
||||
spy,
|
||||
);
|
||||
expect(res.length).toBe(3);
|
||||
expect(spy).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it("paginate maps the responses to data by default", async () => {
|
||||
let callCount = 0;
|
||||
const octokit = new ProbotOctokit({
|
||||
...defaultOptions,
|
||||
request: {
|
||||
fetch: (url: string, options: { [key: string]: any }) => {
|
||||
if (callCount === 0) {
|
||||
expect(url).toEqual(
|
||||
"https://api.github.com/repos/JasonEtco/pizza/issues?per_page=1",
|
||||
);
|
||||
} else {
|
||||
expect(url).toMatch(
|
||||
new RegExp(
|
||||
"^https://api\\.github\\.com/repositories/[0-9]+/issues\\?per_page=[0-9]+&page=[0-9]+$",
|
||||
),
|
||||
);
|
||||
}
|
||||
expect(options.method).toEqual("GET");
|
||||
expect(options.headers.accept).toEqual(
|
||||
"application/vnd.github.v3+json",
|
||||
);
|
||||
expect(options.headers["user-agent"]).toMatch(/^probot\//);
|
||||
expect(options.signal).toEqual(undefined);
|
||||
expect(options.body).toEqual(undefined);
|
||||
|
||||
return Promise.resolve({
|
||||
status: 200,
|
||||
text: () => Promise.resolve([issues[callCount++]]),
|
||||
headers: new Headers({
|
||||
link:
|
||||
callCount !== 4
|
||||
? `link: '<https://api.github.com/repositories/123/issues?per_page=1&page=${callCount}>; rel="next"',`
|
||||
: "",
|
||||
}),
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
const res = await octokit.paginate(
|
||||
octokit.issues.listForRepo.endpoint.merge({
|
||||
owner: "JasonEtco",
|
||||
repo: "pizza",
|
||||
per_page: 1,
|
||||
}),
|
||||
);
|
||||
expect(res).toEqual(issues);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,37 +1,59 @@
|
|||
import Stream from "stream";
|
||||
import Stream from "node:stream";
|
||||
|
||||
import {
|
||||
import type {
|
||||
EmitterWebhookEvent,
|
||||
EmitterWebhookEvent as WebhookEvent,
|
||||
} from "@octokit/webhooks";
|
||||
import Bottleneck from "bottleneck";
|
||||
import nock from "nock";
|
||||
import pino from "pino";
|
||||
import fetchMock from "fetch-mock";
|
||||
import { pino, type LogFn } from "pino";
|
||||
import { describe, expect, test, beforeEach, it, vi, type Mock } from "vitest";
|
||||
|
||||
import { Probot, ProbotOctokit, Context } from "../src";
|
||||
import { Probot, ProbotOctokit, Context } from "../src/index.js";
|
||||
|
||||
import webhookExamples from "@octokit/webhooks-examples";
|
||||
import { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
|
||||
import webhookExamples, {
|
||||
type WebhookDefinition,
|
||||
} from "@octokit/webhooks-examples";
|
||||
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types.js";
|
||||
|
||||
const appId = 1;
|
||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
|
||||
const getPayloadExamples = <TName extends EmitterWebhookEventName>(
|
||||
name: TName
|
||||
name: TName,
|
||||
) => {
|
||||
return webhookExamples.filter((event) => event.name === name.split(".")[0])[0]
|
||||
.examples as EmitterWebhookEvent<TName>["payload"][];
|
||||
return (webhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === name.split(".")[0],
|
||||
)[0].examples as EmitterWebhookEvent<TName>["payload"][];
|
||||
};
|
||||
const getPayloadExample = <TName extends EmitterWebhookEventName>(
|
||||
name: TName
|
||||
name: TName,
|
||||
) => {
|
||||
const examples = getPayloadExamples<TName>(name);
|
||||
if (name.includes(".")) {
|
||||
|
@ -43,7 +65,6 @@ const getPayloadExample = <TName extends EmitterWebhookEventName>(
|
|||
}
|
||||
return examples[0];
|
||||
};
|
||||
// tslint:disable:no-empty
|
||||
describe("Probot", () => {
|
||||
let probot: Probot;
|
||||
let event: WebhookEvent<
|
||||
|
@ -52,7 +73,7 @@ describe("Probot", () => {
|
|||
let output: any;
|
||||
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
@ -69,22 +90,26 @@ describe("Probot", () => {
|
|||
|
||||
describe(".defaults()", () => {
|
||||
test("sets default options for constructor", async () => {
|
||||
const mock = nock("https://api.github.com").get("/app").reply(200, {
|
||||
id: 1,
|
||||
const fetch = fetchMock.sandbox().getOnce("https://api.github.com/app", {
|
||||
status: 200,
|
||||
body: {
|
||||
id: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const MyProbot = Probot.defaults({ appId, privateKey });
|
||||
const probot = new MyProbot();
|
||||
const probot = new MyProbot({
|
||||
request: { fetch },
|
||||
});
|
||||
const octokit = await probot.auth();
|
||||
await octokit.apps.getAuthenticated();
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructor", () => {
|
||||
it("no options", () => {
|
||||
expect(() => new Probot()).toThrow(
|
||||
"[@octokit/auth-app] appId option is required"
|
||||
"[@octokit/auth-app] appId option is required",
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -101,8 +126,8 @@ describe("Probot", () => {
|
|||
it("shouldn't overwrite `options.throttle` passed to `{Octokit: ProbotOctokit.defaults(options)}`", () => {
|
||||
expect.assertions(1);
|
||||
|
||||
const MyOctokit = ProbotOctokit.plugin((octokit, options) => {
|
||||
expect(options.throttle.enabled).toEqual(false);
|
||||
const MyOctokit = ProbotOctokit.plugin((_octokit, options) => {
|
||||
expect(options.throttle?.enabled).toEqual(true);
|
||||
}).defaults({
|
||||
appId,
|
||||
privateKey,
|
||||
|
@ -133,7 +158,7 @@ describe("Probot", () => {
|
|||
it("responds with the correct error if webhook secret does not match", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
probot.log.error = jest.fn();
|
||||
probot.log.error = vi.fn() as LogFn;
|
||||
probot.webhooks.on("push", () => {
|
||||
throw new Error("X-Hub-Signature-256 does not match blob signature");
|
||||
});
|
||||
|
@ -141,16 +166,14 @@ describe("Probot", () => {
|
|||
try {
|
||||
await probot.webhooks.receive(event);
|
||||
} catch (e) {
|
||||
expect(
|
||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
||||
).toMatchSnapshot();
|
||||
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("responds with the correct error if webhook secret is not found", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
probot.log.error = jest.fn();
|
||||
probot.log.error = vi.fn() as LogFn;
|
||||
probot.webhooks.on("push", () => {
|
||||
throw new Error("No X-Hub-Signature-256 found on request");
|
||||
});
|
||||
|
@ -158,66 +181,58 @@ describe("Probot", () => {
|
|||
try {
|
||||
await probot.webhooks.receive(event);
|
||||
} catch (e) {
|
||||
expect(
|
||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
||||
).toMatchSnapshot();
|
||||
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("responds with the correct error if webhook secret is wrong", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
probot.log.error = jest.fn();
|
||||
probot.log.error = vi.fn() as LogFn;
|
||||
probot.webhooks.on("push", () => {
|
||||
throw Error(
|
||||
"webhooks:receiver ignored: POST / due to missing headers: x-hub-signature-256"
|
||||
"webhooks:receiver ignored: POST / due to missing headers: x-hub-signature-256",
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await probot.webhooks.receive(event);
|
||||
} catch (e) {
|
||||
expect(
|
||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
||||
).toMatchSnapshot();
|
||||
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("responds with the correct error if the PEM file is missing", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
probot.log.error = jest.fn();
|
||||
probot.log.error = vi.fn() as LogFn;
|
||||
probot.webhooks.onAny(() => {
|
||||
throw new Error(
|
||||
"error:0906D06C:PEM routines:PEM_read_bio:no start line"
|
||||
"error:0906D06C:PEM routines:PEM_read_bio:no start line",
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await probot.webhooks.receive(event);
|
||||
} catch (e) {
|
||||
expect(
|
||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
||||
).toMatchSnapshot();
|
||||
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
|
||||
it("responds with the correct error if the jwt could not be decoded", async () => {
|
||||
expect.assertions(1);
|
||||
|
||||
probot.log.error = jest.fn();
|
||||
probot.log.error = vi.fn() as LogFn;
|
||||
probot.webhooks.onAny(() => {
|
||||
throw new Error(
|
||||
'{"message":"A JSON web token could not be decoded","documentation_url":"https://developer.github.com/v3"}'
|
||||
'{"message":"A JSON web token could not be decoded","documentation_url":"https://developer.github.com/v3"}',
|
||||
);
|
||||
});
|
||||
|
||||
try {
|
||||
await probot.webhooks.receive(event);
|
||||
} catch (e) {
|
||||
expect(
|
||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
||||
).toMatchSnapshot();
|
||||
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -227,7 +242,7 @@ describe("Probot", () => {
|
|||
const appFn = async (app: Probot) => {
|
||||
const octokit = await app.auth();
|
||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
||||
"https://notreallygithub.com/api/v3"
|
||||
"https://notreallygithub.com/api/v3",
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -242,7 +257,7 @@ describe("Probot", () => {
|
|||
const appFn = async (app: Probot) => {
|
||||
const octokit = await app.auth();
|
||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
||||
"https://notreallygithub.com/api/v3"
|
||||
"https://notreallygithub.com/api/v3",
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -261,7 +276,7 @@ describe("Probot", () => {
|
|||
const appFn = async (app: Probot) => {
|
||||
const octokit = await app.auth();
|
||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
||||
"http://notreallygithub.com/api/v3"
|
||||
"http://notreallygithub.com/api/v3",
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -273,42 +288,48 @@ describe("Probot", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe.skip("options.redisConfig as string", () => {
|
||||
it("sets throttle options", async () => {
|
||||
expect.assertions(2);
|
||||
describe.skipIf(process.env.REDIS_URL === undefined)(
|
||||
"options.redisConfig as string",
|
||||
() => {
|
||||
it("sets throttle options", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
probot = new Probot({
|
||||
githubToken: "faketoken",
|
||||
redisConfig: "test",
|
||||
Octokit: ProbotOctokit.plugin((octokit, options) => {
|
||||
expect(options.throttle.Bottleneck).toBe(Bottleneck);
|
||||
expect(options.throttle.connection).toBeInstanceOf(
|
||||
Bottleneck.IORedisConnection
|
||||
);
|
||||
}),
|
||||
probot = new Probot({
|
||||
githubToken: "faketoken",
|
||||
redisConfig: process.env.REDIS_URL,
|
||||
Octokit: ProbotOctokit.plugin((_octokit, options) => {
|
||||
expect(options.throttle?.Bottleneck).toBe(Bottleneck);
|
||||
expect(options.throttle?.connection).toBeInstanceOf(
|
||||
Bottleneck.IORedisConnection,
|
||||
);
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe.skip("redis configuration object", () => {
|
||||
it("sets throttle options", async () => {
|
||||
expect.assertions(2);
|
||||
const redisConfig = {
|
||||
host: "test",
|
||||
};
|
||||
describe.skipIf(process.env.REDIS_URL === undefined)(
|
||||
"redis configuration object",
|
||||
() => {
|
||||
it("sets throttle options", async () => {
|
||||
expect.assertions(2);
|
||||
const redisConfig = {
|
||||
host: process.env.REDIS_URL,
|
||||
};
|
||||
|
||||
probot = new Probot({
|
||||
githubToken: "faketoken",
|
||||
redisConfig,
|
||||
Octokit: ProbotOctokit.plugin((octokit, options) => {
|
||||
expect(options.throttle.Bottleneck).toBe(Bottleneck);
|
||||
expect(options.throttle.connection).toBeInstanceOf(
|
||||
Bottleneck.IORedisConnection
|
||||
);
|
||||
}),
|
||||
probot = new Probot({
|
||||
githubToken: "faketoken",
|
||||
redisConfig,
|
||||
Octokit: ProbotOctokit.plugin((_octokit, options) => {
|
||||
expect(options.throttle?.Bottleneck).toBe(Bottleneck);
|
||||
expect(options.throttle?.connection).toBeInstanceOf(
|
||||
Bottleneck.IORedisConnection,
|
||||
);
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe("on", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -325,7 +346,7 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on("pull_request", spy);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(0);
|
||||
|
@ -341,9 +362,15 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on("pull_request.opened", spy);
|
||||
|
||||
const event: WebhookEvent<"pull_request.opened"> = {
|
||||
id: "123-456",
|
||||
name: "pull_request",
|
||||
payload: getPayloadExample("pull_request.opened"),
|
||||
};
|
||||
|
||||
await probot.receive(event);
|
||||
expect(spy).toHaveBeenCalled();
|
||||
});
|
||||
|
@ -354,7 +381,7 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on("pull_request.closed", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
|
@ -367,7 +394,7 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.onAny(spy);
|
||||
|
||||
await probot.receive(event);
|
||||
|
@ -380,13 +407,19 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const event: WebhookEvent<"pull_request.opened"> = {
|
||||
id: "123-456",
|
||||
name: "pull_request",
|
||||
payload: getPayloadExample("pull_request.opened"),
|
||||
};
|
||||
|
||||
const event2: WebhookEvent<"issues.opened"> = {
|
||||
id: "123",
|
||||
name: "issues",
|
||||
payload: getPayloadExample("issues.opened"),
|
||||
};
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on(["pull_request.opened", "issues.opened"], spy);
|
||||
|
||||
await probot.receive(event);
|
||||
|
@ -401,7 +434,7 @@ describe("Probot", () => {
|
|||
log: pino(streamLogsToOutput),
|
||||
});
|
||||
|
||||
const handler = jest.fn().mockImplementation((context) => {
|
||||
const handler = vi.fn().mockImplementation((context) => {
|
||||
expect(context.log.info).toBeDefined();
|
||||
context.log.info("testing");
|
||||
|
||||
|
@ -409,7 +442,7 @@ describe("Probot", () => {
|
|||
expect.objectContaining({
|
||||
id: context.id,
|
||||
msg: "testing",
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -419,9 +452,40 @@ describe("Probot", () => {
|
|||
});
|
||||
|
||||
it("returns an authenticated client for installation.created", async () => {
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.postOnce("https://api.github.com/app/installations/1/access_tokens", {
|
||||
status: 201,
|
||||
body: {
|
||||
token: "v1.1f699f1069f60xxx",
|
||||
permissions: {
|
||||
issues: "write",
|
||||
contents: "read",
|
||||
},
|
||||
},
|
||||
})
|
||||
.getOnce(
|
||||
function (url, opts) {
|
||||
if (url === "https://api.github.com/") {
|
||||
expect(
|
||||
(opts.headers as Record<string, string>).authorization,
|
||||
).toEqual("token v1.1f699f1069f60xxx");
|
||||
return true;
|
||||
}
|
||||
throw new Error("Should have matched");
|
||||
},
|
||||
{
|
||||
status: 200,
|
||||
body: {},
|
||||
},
|
||||
);
|
||||
|
||||
const probot = new Probot({
|
||||
appId,
|
||||
privateKey,
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
|
||||
event = {
|
||||
|
@ -431,32 +495,35 @@ describe("Probot", () => {
|
|||
};
|
||||
event.payload.installation.id = 1;
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.post("/app/installations/1/access_tokens")
|
||||
.reply(201, {
|
||||
token: "v1.1f699f1069f60xxx",
|
||||
permissions: {
|
||||
issues: "write",
|
||||
contents: "read",
|
||||
},
|
||||
})
|
||||
.get("/")
|
||||
.matchHeader("authorization", "token v1.1f699f1069f60xxx")
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("installation.created", async (context) => {
|
||||
await context.octokit.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns an unauthenticated client for installation.deleted", async () => {
|
||||
const fetch = fetchMock.sandbox().getOnce(
|
||||
function (url, opts) {
|
||||
if (url === "https://api.github.com/") {
|
||||
expect(
|
||||
(opts.headers as Record<string, string>).authorization,
|
||||
).toEqual(undefined);
|
||||
return true;
|
||||
}
|
||||
throw new Error("Should have matched");
|
||||
},
|
||||
{
|
||||
body: {},
|
||||
},
|
||||
);
|
||||
|
||||
const probot = new Probot({
|
||||
appId,
|
||||
privateKey,
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
|
||||
event = {
|
||||
|
@ -466,46 +533,50 @@ describe("Probot", () => {
|
|||
};
|
||||
event.payload.installation.id = 1;
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/")
|
||||
.matchHeader("authorization", (value) => value === undefined)
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("installation.deleted", async (context) => {
|
||||
await context.octokit.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event).catch(console.log);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
|
||||
it("returns an authenticated client for events without an installation", async () => {
|
||||
const fetch = fetchMock.sandbox().mock(
|
||||
function (url, opts) {
|
||||
if (url === "https://api.github.com/") {
|
||||
expect(
|
||||
(opts.headers as Record<string, string>).authorization,
|
||||
).toEqual(undefined);
|
||||
return true;
|
||||
}
|
||||
throw new Error("Should have matched");
|
||||
},
|
||||
{
|
||||
body: {},
|
||||
},
|
||||
);
|
||||
|
||||
const probot = new Probot({
|
||||
appId,
|
||||
privateKey,
|
||||
request: {
|
||||
fetch,
|
||||
},
|
||||
});
|
||||
|
||||
event = {
|
||||
id: "123-456",
|
||||
name: "check_run",
|
||||
payload: getPayloadExamples("check_run").filter(
|
||||
(event) => typeof event.installation === "undefined"
|
||||
(event) => typeof event.installation === "undefined",
|
||||
)[0],
|
||||
};
|
||||
|
||||
const mock = nock("https://api.github.com")
|
||||
.get("/")
|
||||
.matchHeader("authorization", (value) => value === undefined)
|
||||
.reply(200, {});
|
||||
|
||||
probot.on("check_run", async (context) => {
|
||||
await context.octokit.request("/");
|
||||
});
|
||||
|
||||
await probot.receive(event).catch(console.log);
|
||||
|
||||
expect(mock.activeMocks()).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -524,7 +595,7 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on("pull_request", spy);
|
||||
|
||||
await probot.receive(event);
|
||||
|
@ -538,7 +609,7 @@ describe("Probot", () => {
|
|||
privateKey,
|
||||
});
|
||||
|
||||
const spy = jest.fn();
|
||||
const spy = vi.fn();
|
||||
probot.on("pull_request", () => {
|
||||
return new Promise((resolve) => {
|
||||
setTimeout(() => {
|
||||
|
@ -568,7 +639,7 @@ describe("Probot", () => {
|
|||
await probot.receive(event);
|
||||
throw new Error("expected error to be raised from app");
|
||||
} catch (error) {
|
||||
expect(error.message).toMatch(/error from app/);
|
||||
expect((error as Error).message).toMatch(/error from app/);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,23 +1,24 @@
|
|||
import { resolveAppFunction } from "../src/helpers/resolve-app-function";
|
||||
import { resolveAppFunction } from "../src/helpers/resolve-app-function.js";
|
||||
import { describe, expect, vi, it } from "vitest";
|
||||
|
||||
const stubAppFnPath = require.resolve("./fixtures/plugin/stub-plugin");
|
||||
const stubAppFnPath = require.resolve("./fixtures/plugin/stub-plugin.ts");
|
||||
const stubTranspiledAppFnPath = require.resolve(
|
||||
"./fixtures/plugin/stub-typescript-transpiled-plugin"
|
||||
"./fixtures/plugin/stub-typescript-transpiled-plugin.ts",
|
||||
);
|
||||
const basedir = process.cwd();
|
||||
|
||||
describe("resolver", () => {
|
||||
it("loads the module at the resolved path", async () => {
|
||||
const stubResolver = jest.fn().mockReturnValue(stubAppFnPath);
|
||||
const stubResolver = vi.fn().mockReturnValue(stubAppFnPath);
|
||||
const module = await resolveAppFunction("foo", { resolver: stubResolver });
|
||||
expect(module).toBe(require(stubAppFnPath));
|
||||
expect(module).toBeInstanceOf(Function);
|
||||
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
||||
});
|
||||
|
||||
it("loads module transpiled from TypeScript (https://github.com/probot/probot/issues/1447)", async () => {
|
||||
const stubResolver = jest.fn().mockReturnValue(stubTranspiledAppFnPath);
|
||||
const stubResolver = vi.fn().mockReturnValue(stubTranspiledAppFnPath);
|
||||
const module = await resolveAppFunction("foo", { resolver: stubResolver });
|
||||
expect(module).toBe(require(stubTranspiledAppFnPath).default);
|
||||
expect(module).toBeInstanceOf(Function);
|
||||
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import path = require("path");
|
||||
import path from "node:path";
|
||||
|
||||
import request from "supertest";
|
||||
import { sign } from "@octokit/webhooks-methods";
|
||||
import { describe, expect, it, beforeEach } from "vitest";
|
||||
|
||||
import { Probot, run, Server } from "../src";
|
||||
import { Probot, run, Server } from "../src/index.js";
|
||||
|
||||
import { captureLogOutput } from "./helpers/capture-log-output";
|
||||
import { captureLogOutput } from "./helpers/capture-log-output.js";
|
||||
import WebhookExamples, {
|
||||
type WebhookDefinition,
|
||||
} from "@octokit/webhooks-examples";
|
||||
|
||||
// tslint:disable:no-empty
|
||||
describe("run", () => {
|
||||
let server: Server;
|
||||
let env: NodeJS.ProcessEnv;
|
||||
|
@ -18,7 +21,7 @@ describe("run", () => {
|
|||
PRIVATE_KEY_PATH: path.join(
|
||||
__dirname,
|
||||
"fixtures",
|
||||
"test-private-key.pem"
|
||||
"test-private-key.pem",
|
||||
),
|
||||
WEBHOOK_PROXY_URL: "https://smee.io/EfHXC9BFfGAxbM6J",
|
||||
WEBHOOK_SECRET: "secret",
|
||||
|
@ -34,7 +37,7 @@ describe("run", () => {
|
|||
() => {
|
||||
initialized = true;
|
||||
},
|
||||
{ env }
|
||||
{ env },
|
||||
);
|
||||
expect(initialized).toBeTruthy();
|
||||
await server.stop();
|
||||
|
@ -58,10 +61,10 @@ describe("run", () => {
|
|||
|
||||
return new Promise(async (resolve) => {
|
||||
server = await run(
|
||||
(app: Probot) => {
|
||||
(_app: Probot) => {
|
||||
initialized = true;
|
||||
},
|
||||
{ env }
|
||||
{ env },
|
||||
);
|
||||
expect(initialized).toBeFalsy();
|
||||
await server.stop();
|
||||
|
@ -71,33 +74,39 @@ describe("run", () => {
|
|||
});
|
||||
|
||||
it("defaults to JSON logs if NODE_ENV is set to 'production'", async () => {
|
||||
const outputData = await captureLogOutput(async () => {
|
||||
env.NODE_ENV = "production";
|
||||
let outputData = "";
|
||||
env.NODE_ENV = "production";
|
||||
|
||||
server = await run(
|
||||
(app) => {
|
||||
server = await run(
|
||||
async (app) => {
|
||||
outputData = await captureLogOutput(async () => {
|
||||
app.log.fatal("test");
|
||||
},
|
||||
{ env }
|
||||
);
|
||||
await server.stop();
|
||||
});
|
||||
}, app.log);
|
||||
},
|
||||
{ env },
|
||||
);
|
||||
await server.stop();
|
||||
|
||||
expect(outputData).toMatch(/"msg":"test"/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("webhooks", () => {
|
||||
const pushEvent = require("./fixtures/webhook/push.json");
|
||||
const pushEvent = (
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "push",
|
||||
)[0] as WebhookDefinition<"push">
|
||||
).examples[0];
|
||||
|
||||
it("POST /", async () => {
|
||||
it("POST /api/github/webhooks", async () => {
|
||||
server = await run(() => {}, { env });
|
||||
|
||||
const dataString = JSON.stringify(pushEvent);
|
||||
|
||||
await request(server.expressApp)
|
||||
.post("/")
|
||||
.post("/api/github/webhooks")
|
||||
.send(dataString)
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||
.set("x-github-delivery", "123")
|
||||
|
@ -120,6 +129,7 @@ describe("run", () => {
|
|||
await request(server.expressApp)
|
||||
.post("/custom-webhook")
|
||||
.send(dataString)
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||
.set("x-github-delivery", "123")
|
||||
|
|
|
@ -1,31 +1,57 @@
|
|||
import Stream from "stream";
|
||||
import Stream from "node:stream";
|
||||
|
||||
import { NextFunction, Request, Response } from "express";
|
||||
import type { NextFunction, Request, Response } from "express";
|
||||
import request from "supertest";
|
||||
import pino from "pino";
|
||||
import { pino } from "pino";
|
||||
import { sign } from "@octokit/webhooks-methods";
|
||||
import getPort from "get-port";
|
||||
import WebhookExamples, {
|
||||
type WebhookDefinition,
|
||||
} from "@octokit/webhooks-examples";
|
||||
import { describe, expect, it, beforeEach, test } from "vitest";
|
||||
|
||||
import { Server, Probot } from "../src";
|
||||
import { Server, Probot } from "../src/index.js";
|
||||
|
||||
const appId = 1;
|
||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
||||
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||
K1r1/gycfDkUCQRP4DbZHt+458JlFHm8QL6VstKzkrp8mYDRhffY0WJnYJL98tr4
|
||||
4tohsDbqFGwmw2mIaHjl24LuWXyyP4xpAGDpl9IcusjXBxLQLp2m4AKXbWpzb0OL
|
||||
Ulrfc1ZooPck2uz7xlMIZOtLlOPjLz2DuejVe24JcwwHzrQWKOfA11R/9e50DVse
|
||||
hnSH/w46Q763y4I0E3BIoUMsolEKzh2ydAAyzkgabGQBUuamZotNfvJoDXeCi1LD
|
||||
8yNCWyTlYpJZJDDXooBU5EAsCvhN1sSRoaXWrlMSDB7r/E+aQyKua4KONqvmoJuC
|
||||
21vSKeECgYEA7yW6wBkVoNhgXnk8XSZv3W+Q0xtdVpidJeNGBWnczlZrummt4xw3
|
||||
xs6zV+rGUDy59yDkKwBKjMMa42Mni7T9Fx8+EKUuhVK3PVQyajoyQqFwT1GORJNz
|
||||
c/eYQ6VYOCSC8OyZmsBM2p+0D4FF2/abwSPMmy0NgyFLCUFVc3OECpkCgYEA5OAm
|
||||
I3wt5s+clg18qS7BKR2DuOFWrzNVcHYXhjx8vOSWV033Oy3yvdUBAhu9A1LUqpwy
|
||||
Ma+unIgxmvmUMQEdyHQMcgBsVs10dR/g2xGjMLcwj6kn+xr3JVIZnbRT50YuPhf+
|
||||
ns1ScdhP6upo9I0/sRsIuN96Gb65JJx94gQ4k9MCgYBO5V6gA2aMQvZAFLUicgzT
|
||||
u/vGea+oYv7tQfaW0J8E/6PYwwaX93Y7Q3QNXCoCzJX5fsNnoFf36mIThGHGiHY6
|
||||
y5bZPPWFDI3hUMa1Hu/35XS85kYOP6sGJjf4kTLyirEcNKJUWH7CXY+00cwvTkOC
|
||||
S4Iz64Aas8AilIhRZ1m3eQKBgQCUW1s9azQRxgeZGFrzC3R340LL530aCeta/6FW
|
||||
CQVOJ9nv84DLYohTVqvVowdNDTb+9Epw/JDxtDJ7Y0YU0cVtdxPOHcocJgdUGHrX
|
||||
ZcJjRIt8w8g/s4X6MhKasBYm9s3owALzCuJjGzUKcDHiO2DKu1xXAb0SzRcTzUCn
|
||||
7daCswKBgQDOYPZ2JGmhibqKjjLFm0qzpcQ6RPvPK1/7g0NInmjPMebP0K6eSPx0
|
||||
9/49J6WTD++EajN7FhktUSYxukdWaCocAQJTDNYP0K88G4rtC2IYy5JFn9SWz5oh
|
||||
x//0u+zd/R/QRUzLOw4N72/Hu+UG6MNt5iDZFCtapRaKt6OvSBwy8w==
|
||||
-----END RSA PRIVATE KEY-----`;
|
||||
const pushEvent = require("./fixtures/webhook/push.json");
|
||||
const pushEvent = (
|
||||
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||
(event) => event.name === "push",
|
||||
)[0] as WebhookDefinition<"push">
|
||||
).examples[0];
|
||||
|
||||
describe("Server", () => {
|
||||
let server: Server;
|
||||
|
||||
let output: any[];
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
@ -45,9 +71,9 @@ describe("Server", () => {
|
|||
|
||||
// Error handler to avoid printing logs
|
||||
server.expressApp.use(
|
||||
(error: Error, req: Request, res: Response, next: NextFunction) => {
|
||||
(error: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
res.status(500).send(error.message);
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
|
@ -63,7 +89,60 @@ describe("Server", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("webhook handler (POST /)", () => {
|
||||
describe("webhook handler by providing webhookPath (POST /)", () => {
|
||||
it("should return 200 and run event handlers in app function", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
server = new Server({
|
||||
webhookPath: "/",
|
||||
Probot: Probot.defaults({
|
||||
appId,
|
||||
privateKey,
|
||||
secret: "secret",
|
||||
}),
|
||||
log: pino(streamLogsToOutput),
|
||||
port: await getPort(),
|
||||
});
|
||||
|
||||
await server.load((app) => {
|
||||
app.on("push", (event) => {
|
||||
expect(event.name).toEqual("push");
|
||||
});
|
||||
});
|
||||
|
||||
const dataString = JSON.stringify(pushEvent);
|
||||
|
||||
await request(server.expressApp)
|
||||
.post("/")
|
||||
.send(dataString)
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||
.set("x-github-delivery", "3sw4d5f6g7h8");
|
||||
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0].msg).toContain("POST / 200 -");
|
||||
});
|
||||
|
||||
test("respond with a friendly error when x-hub-signature-256 is missing", async () => {
|
||||
await server.load(() => {});
|
||||
|
||||
await request(server.expressApp)
|
||||
.post("/api/github/webhooks")
|
||||
.send(JSON.stringify(pushEvent))
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("content-type", "application/json")
|
||||
// Note: 'x-hub-signature-256' is missing
|
||||
.set("x-github-delivery", "3sw4d5f6g7h8")
|
||||
.expect(
|
||||
400,
|
||||
'{"error":"Required headers missing: x-hub-signature-256"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("webhook handler (POST /api/github/webhooks)", () => {
|
||||
it("should return 200 and run event handlers in app function", async () => {
|
||||
expect.assertions(3);
|
||||
|
||||
|
@ -86,28 +165,31 @@ describe("Server", () => {
|
|||
const dataString = JSON.stringify(pushEvent);
|
||||
|
||||
await request(server.expressApp)
|
||||
.post("/")
|
||||
.post("/api/github/webhooks")
|
||||
.send(dataString)
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||
.set("x-github-delivery", "3sw4d5f6g7h8");
|
||||
|
||||
expect(output.length).toEqual(1);
|
||||
expect(output[0].msg).toContain("POST / 200 -");
|
||||
expect(output[0].msg).toContain("POST /api/github/webhooks 200 -");
|
||||
});
|
||||
|
||||
test("respond with a friendly error when x-hub-signature-256 is missing", async () => {
|
||||
await server.load(() => {});
|
||||
|
||||
await request(server.expressApp)
|
||||
.post("/")
|
||||
.post("/api/github/webhooks")
|
||||
.send(JSON.stringify(pushEvent))
|
||||
.set("content-type", "application/json")
|
||||
.set("x-github-event", "push")
|
||||
.set("content-type", "application/json")
|
||||
// Note: 'x-hub-signature-256' is missing
|
||||
.set("x-github-delivery", "3sw4d5f6g7h8")
|
||||
.expect(
|
||||
400,
|
||||
'{"error":"Required headers missing: x-hub-signature-256"}'
|
||||
'{"error":"Required headers missing: x-hub-signature-256"}',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -121,30 +203,31 @@ describe("Server", () => {
|
|||
});
|
||||
|
||||
describe(".start() / .stop()", () => {
|
||||
it("should expect the correct error if port already in use", (next) => {
|
||||
expect.assertions(1);
|
||||
it("should expect the correct error if port already in use", () =>
|
||||
new Promise<void>((next) => {
|
||||
expect.assertions(1);
|
||||
|
||||
// block port 3001
|
||||
const http = require("http");
|
||||
const blockade = http.createServer().listen(3001, async () => {
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({ appId, privateKey }),
|
||||
log: pino(streamLogsToOutput),
|
||||
port: 3001,
|
||||
// block port 3001
|
||||
const http = require("http");
|
||||
const blockade = http.createServer().listen(3001, async () => {
|
||||
const server = new Server({
|
||||
Probot: Probot.defaults({ appId, privateKey }),
|
||||
log: pino(streamLogsToOutput),
|
||||
port: 3001,
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
} catch (error) {
|
||||
expect((error as Error).message).toEqual(
|
||||
"Port 3001 is already in use. You can define the PORT environment variable to use a different port.",
|
||||
);
|
||||
}
|
||||
|
||||
await server.stop();
|
||||
blockade.close(() => next());
|
||||
});
|
||||
|
||||
try {
|
||||
await server.start();
|
||||
} catch (error) {
|
||||
expect(error.message).toEqual(
|
||||
"Port 3001 is already in use. You can define the PORT environment variable to use a different port."
|
||||
);
|
||||
}
|
||||
|
||||
await server.stop();
|
||||
blockade.close(() => next());
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it("should listen to port when not in use", async () => {
|
||||
const testApp = new Server({
|
||||
|
@ -179,21 +262,38 @@ describe("Server", () => {
|
|||
describe("router", () => {
|
||||
it("prefixes paths with route name", () => {
|
||||
const router = server.router("/my-app");
|
||||
router.get("/foo", (req, res) => res.end("foo"));
|
||||
router.get("/foo", (_req, res) => res.end("foo"));
|
||||
|
||||
return request(server.expressApp).get("/my-app/foo").expect(200, "foo");
|
||||
});
|
||||
|
||||
it("allows routes with no path", () => {
|
||||
const router = server.router();
|
||||
router.get("/foo", (req, res) => res.end("foo"));
|
||||
router.get("/foo", (_req, res) => res.end("foo"));
|
||||
|
||||
return request(server.expressApp).get("/foo").expect(200, "foo");
|
||||
});
|
||||
|
||||
it("allows you to overwrite the root path", () => {
|
||||
it("allows you to overwrite the root path when webhookPath is not defined", () => {
|
||||
const log = pino(streamLogsToOutput);
|
||||
server = new Server({
|
||||
Probot: Probot.defaults({
|
||||
appId,
|
||||
privateKey,
|
||||
secret: "secret",
|
||||
log: log.child({ name: "probot" }),
|
||||
}),
|
||||
log: log.child({ name: "server" }),
|
||||
});
|
||||
|
||||
// Error handler to avoid printing logs
|
||||
server.expressApp.use(
|
||||
(error: Error, _req: Request, res: Response, _next: NextFunction) => {
|
||||
res.status(500).send(error.message);
|
||||
},
|
||||
);
|
||||
const router = server.router();
|
||||
router.get("/", (req, res) => res.end("foo"));
|
||||
router.get("/", (_req, res) => res.end("foo"));
|
||||
|
||||
return request(server.expressApp).get("/").expect(200, "foo");
|
||||
});
|
||||
|
@ -202,12 +302,12 @@ describe("Server", () => {
|
|||
["foo", "bar"].forEach((name) => {
|
||||
const router = server.router("/" + name);
|
||||
|
||||
router.use((req, res, next) => {
|
||||
router.use((_req, res, next) => {
|
||||
res.append("X-Test", name);
|
||||
next();
|
||||
});
|
||||
|
||||
router.get("/hello", (req, res) => res.end(name));
|
||||
router.get("/hello", (_req, res) => res.end(name));
|
||||
});
|
||||
|
||||
await request(server.expressApp)
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
import Stream from "stream";
|
||||
import Stream from "node:stream";
|
||||
|
||||
import express from "express";
|
||||
import request from "supertest";
|
||||
import pino from "pino";
|
||||
import { Options } from "pino-http";
|
||||
import { pino } from "pino";
|
||||
import type { Options } from "pino-http";
|
||||
import { describe, expect, test, beforeEach } from "vitest";
|
||||
|
||||
import { getLoggingMiddleware } from "../../src/server/logging-middleware";
|
||||
import { getLoggingMiddleware } from "../../src/server/logging-middleware.js";
|
||||
|
||||
describe("logging", () => {
|
||||
let server: express.Express;
|
||||
|
@ -13,7 +14,7 @@ describe("logging", () => {
|
|||
let options: Options;
|
||||
|
||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||
streamLogsToOutput._write = (object, encoding, done) => {
|
||||
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||
output.push(JSON.parse(object));
|
||||
done();
|
||||
};
|
||||
|
@ -22,11 +23,11 @@ describe("logging", () => {
|
|||
function applyMiddlewares() {
|
||||
server.use(express.json());
|
||||
server.use(getLoggingMiddleware(logger, options));
|
||||
server.get("/", (req, res) => {
|
||||
server.get("/", (_req, res) => {
|
||||
res.set("X-Test-Header", "testing");
|
||||
res.send("OK");
|
||||
});
|
||||
server.post("/", (req, res) => res.send("OK"));
|
||||
server.post("/", (_req, res) => res.send("OK"));
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -40,7 +41,7 @@ describe("logging", () => {
|
|||
return request(server)
|
||||
.get("/")
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect((_res) => {
|
||||
// logs id with request and response
|
||||
expect(output[0].req.id).toBeTruthy();
|
||||
expect(typeof output[0].responseTime).toEqual("number");
|
||||
|
@ -54,7 +55,7 @@ describe("logging", () => {
|
|||
method: "GET",
|
||||
remoteAddress: "::ffff:127.0.0.1",
|
||||
url: "/",
|
||||
})
|
||||
}),
|
||||
);
|
||||
|
||||
expect(output[0].res).toEqual(
|
||||
|
@ -62,7 +63,7 @@ describe("logging", () => {
|
|||
headers: expect.objectContaining({
|
||||
"x-test-header": "testing",
|
||||
}),
|
||||
})
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -73,7 +74,7 @@ describe("logging", () => {
|
|||
.get("/")
|
||||
.set("X-Request-ID", "42")
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect((_res) => {
|
||||
expect(output[0].req.id).toEqual("42");
|
||||
});
|
||||
});
|
||||
|
@ -84,7 +85,7 @@ describe("logging", () => {
|
|||
.get("/")
|
||||
.set("X-GitHub-Delivery", "a-b-c")
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect((_res) => {
|
||||
expect(output[0].req.id).toEqual("a-b-c");
|
||||
});
|
||||
});
|
||||
|
@ -92,14 +93,14 @@ describe("logging", () => {
|
|||
test("sets ignorePaths option to ignore logging", () => {
|
||||
options = {
|
||||
autoLogging: {
|
||||
ignorePaths: ["/"],
|
||||
ignore: (req) => ["/"].includes(req.url!),
|
||||
},
|
||||
};
|
||||
applyMiddlewares();
|
||||
return request(server)
|
||||
.get("/")
|
||||
.expect(200)
|
||||
.expect((res) => {
|
||||
.expect((_res) => {
|
||||
expect(output.length).toEqual(0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { RepositoryEditedEvent } from "@octokit/webhooks-types";
|
||||
import { expectType } from "tsd";
|
||||
import { Probot } from "../../src";
|
||||
import { Probot } from "../../src/index.js";
|
||||
|
||||
const app = new Probot({});
|
||||
|
||||
|
|
|
@ -0,0 +1,262 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`importView > only providing GH_HOST 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Import Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
|
||||
<body class=\\"bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full py-6\\">
|
||||
<a href=\\"/probot\\"><img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\"></a>
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h2>Use existing Github App</h2>
|
||||
<br>
|
||||
|
||||
<h3>Step 1:</h3>
|
||||
<p class=\\"d-block mt-2\\">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b></b>
|
||||
</p>
|
||||
<a class=\\"d-block mt-2\\" href=\\"https://github.com/settings/apps\\" target=\\"__blank\\" rel=\\"noreferrer\\">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<h3>Step 2:</h3>
|
||||
<p class=\\"mt-2\\">Fill out this form</p>
|
||||
<form onsubmit=\\"return onSubmit(event) || false\\">
|
||||
<label class=\\"d-block mt-2\\" for=\\"appId\\">App Id</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"text\\" required=\\"true\\" id=\\"appId\\" name=\\"appId\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"whs\\">Webhook secret (required!)</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"password\\" required=\\"true\\" id=\\"whs\\" name=\\"whs\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"pem\\">Private Key</label>
|
||||
<input class=\\"form-control width-full m-2\\" type=\\"file\\" accept=\\".pem\\" required=\\"true\\" id=\\"pem\\"
|
||||
name=\\"pem\\">
|
||||
<br>
|
||||
|
||||
<button class=\\"btn btn-outline m-2\\" type=\\"submit\\">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const idEl = document.getElementById('appId');
|
||||
const appId = idEl.value;
|
||||
|
||||
|
||||
const secretEl = document.getElementById('whs');
|
||||
const webhook_secret = secretEl.value;
|
||||
|
||||
const fileEl = document.getElementById('pem');
|
||||
const file = fileEl.files[0];
|
||||
|
||||
file.text().then((text) => fetch('', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ appId, pem: text, webhook_secret })
|
||||
})).then((r) => {
|
||||
if (r.ok) {
|
||||
location.replace('/probot/success');
|
||||
}
|
||||
}).catch((e) => alert(e));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`importView > providing "My App" as name 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Import My App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
|
||||
<body class=\\"bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full py-6\\">
|
||||
<a href=\\"/probot\\"><img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\"></a>
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h2>Use existing Github App</h2>
|
||||
<br>
|
||||
|
||||
<h3>Step 1:</h3>
|
||||
<p class=\\"d-block mt-2\\">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b></b>
|
||||
</p>
|
||||
<a class=\\"d-block mt-2\\" href=\\"https://github.com/settings/apps\\" target=\\"__blank\\" rel=\\"noreferrer\\">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<h3>Step 2:</h3>
|
||||
<p class=\\"mt-2\\">Fill out this form</p>
|
||||
<form onsubmit=\\"return onSubmit(event) || false\\">
|
||||
<label class=\\"d-block mt-2\\" for=\\"appId\\">App Id</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"text\\" required=\\"true\\" id=\\"appId\\" name=\\"appId\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"whs\\">Webhook secret (required!)</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"password\\" required=\\"true\\" id=\\"whs\\" name=\\"whs\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"pem\\">Private Key</label>
|
||||
<input class=\\"form-control width-full m-2\\" type=\\"file\\" accept=\\".pem\\" required=\\"true\\" id=\\"pem\\"
|
||||
name=\\"pem\\">
|
||||
<br>
|
||||
|
||||
<button class=\\"btn btn-outline m-2\\" type=\\"submit\\">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const idEl = document.getElementById('appId');
|
||||
const appId = idEl.value;
|
||||
|
||||
|
||||
const secretEl = document.getElementById('whs');
|
||||
const webhook_secret = secretEl.value;
|
||||
|
||||
const fileEl = document.getElementById('pem');
|
||||
const file = fileEl.files[0];
|
||||
|
||||
file.text().then((text) => fetch('', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ appId, pem: text, webhook_secret })
|
||||
})).then((r) => {
|
||||
if (r.ok) {
|
||||
location.replace('/probot/success');
|
||||
}
|
||||
}).catch((e) => alert(e));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`importView > providing a smee-url as WEBHOOK_PROXY_URL 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Import Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
|
||||
<body class=\\"bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full py-6\\">
|
||||
<a href=\\"/probot\\"><img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\"></a>
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h2>Use existing Github App</h2>
|
||||
<br>
|
||||
|
||||
<h3>Step 1:</h3>
|
||||
<p class=\\"d-block mt-2\\">
|
||||
Replace your app's Webhook URL with <br>
|
||||
<b>https://smee.io/1234</b>
|
||||
</p>
|
||||
<a class=\\"d-block mt-2\\" href=\\"https://github.com/settings/apps\\" target=\\"__blank\\" rel=\\"noreferrer\\">
|
||||
You can do it here
|
||||
</a>
|
||||
|
||||
<br>
|
||||
<h3>Step 2:</h3>
|
||||
<p class=\\"mt-2\\">Fill out this form</p>
|
||||
<form onsubmit=\\"return onSubmit(event) || false\\">
|
||||
<label class=\\"d-block mt-2\\" for=\\"appId\\">App Id</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"text\\" required=\\"true\\" id=\\"appId\\" name=\\"appId\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"whs\\">Webhook secret (required!)</label>
|
||||
<input class=\\"form-control width-full\\" type=\\"password\\" required=\\"true\\" id=\\"whs\\" name=\\"whs\\"><br>
|
||||
|
||||
<label class=\\"d-block mt-3\\" for=\\"pem\\">Private Key</label>
|
||||
<input class=\\"form-control width-full m-2\\" type=\\"file\\" accept=\\".pem\\" required=\\"true\\" id=\\"pem\\"
|
||||
name=\\"pem\\">
|
||||
<br>
|
||||
|
||||
<button class=\\"btn btn-outline m-2\\" type=\\"submit\\">Submit</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
function onSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
const idEl = document.getElementById('appId');
|
||||
const appId = idEl.value;
|
||||
|
||||
|
||||
const secretEl = document.getElementById('whs');
|
||||
const webhook_secret = secretEl.value;
|
||||
|
||||
const fileEl = document.getElementById('pem');
|
||||
const file = fileEl.files[0];
|
||||
|
||||
file.text().then((text) => fetch('', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({ appId, pem: text, webhook_secret })
|
||||
})).then((r) => {
|
||||
if (r.ok) {
|
||||
location.replace('/probot/success');
|
||||
}
|
||||
}).catch((e) => alert(e));
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
||||
</html>"
|
||||
`;
|
|
@ -0,0 +1,138 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`probotView > not providing parameters 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
</h1>
|
||||
|
||||
<p>This bot was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`probotView > providing description 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
</h1>
|
||||
|
||||
<p>My App with Probot</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`probotView > providing description 2`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
<span class=\\"Label Label--outline v-align-middle ml-2 text-gray-light\\">v1.0.0</span>
|
||||
</h1>
|
||||
|
||||
<p>This bot was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`probotView > providing name 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>My App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to My App
|
||||
</h1>
|
||||
|
||||
<p>This bot was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
|
@ -0,0 +1,193 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`setupView > provide also description 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
|
||||
</h1>
|
||||
|
||||
<p>Awesome App with Probot</p>
|
||||
|
||||
<div class=\\"text-left mt-6\\">
|
||||
<h2 class=\\"alt-h3 mb-2\\">Getting Started</h2>
|
||||
|
||||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action=\\"https://github.com/organizations/probot/settings/apps/new\\" method=\\"post\\" target=\\"_blank\\" class=\\"d-flex flex-items-center\\">
|
||||
<button class=\\"btn btn-outline\\" name=\\"manifest\\" id=\\"manifest\\" value='{\\"name\\":\\"My App\\"}' >Register GitHub App</button>
|
||||
<a href=\\"/probot/import\\" class=\\"ml-2\\">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`setupView > provide also name 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup My App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to My App
|
||||
|
||||
</h1>
|
||||
|
||||
<p>This app was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
|
||||
<div class=\\"text-left mt-6\\">
|
||||
<h2 class=\\"alt-h3 mb-2\\">Getting Started</h2>
|
||||
|
||||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action=\\"https://github.com/organizations/probot/settings/apps/new\\" method=\\"post\\" target=\\"_blank\\" class=\\"d-flex flex-items-center\\">
|
||||
<button class=\\"btn btn-outline\\" name=\\"manifest\\" id=\\"manifest\\" value='{\\"name\\":\\"My App\\"}' >Register GitHub App</button>
|
||||
<a href=\\"/probot/import\\" class=\\"ml-2\\">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`setupView > provide also version 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
<span class=\\"Label Label--outline v-align-middle ml-2 text-gray-light\\">v1.0.0</span>
|
||||
</h1>
|
||||
|
||||
<p>This app was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
|
||||
<div class=\\"text-left mt-6\\">
|
||||
<h2 class=\\"alt-h3 mb-2\\">Getting Started</h2>
|
||||
|
||||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action=\\"https://github.com/organizations/probot/settings/apps/new\\" method=\\"post\\" target=\\"_blank\\" class=\\"d-flex flex-items-center\\">
|
||||
<button class=\\"btn btn-outline\\" name=\\"manifest\\" id=\\"manifest\\" value='{\\"name\\":\\"My App\\"}' >Register GitHub App</button>
|
||||
<a href=\\"/probot/import\\" class=\\"ml-2\\">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`setupView > providing bare minimum 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<h1>
|
||||
Welcome to your Probot App
|
||||
|
||||
</h1>
|
||||
|
||||
<p>This app was built using <a href=\\"https://github.com/probot/probot\\">Probot</a>, a framework for building GitHub Apps.</p>
|
||||
|
||||
<div class=\\"text-left mt-6\\">
|
||||
<h2 class=\\"alt-h3 mb-2\\">Getting Started</h2>
|
||||
|
||||
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||
<br>
|
||||
|
||||
<form action=\\"https://github.com/organizations/probot/settings/apps/new\\" method=\\"post\\" target=\\"_blank\\" class=\\"d-flex flex-items-center\\">
|
||||
<button class=\\"btn btn-outline\\" name=\\"manifest\\" id=\\"manifest\\" value='{\\"name\\":\\"My App\\"}' >Register GitHub App</button>
|
||||
<a href=\\"/probot/import\\" class=\\"ml-2\\">or use an existing Github App</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
|
@ -0,0 +1,69 @@
|
|||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`setupView > not providing name 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup Your App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<div class=\\"text-center\\">
|
||||
<h1 class=\\"alt-h3 mb-2\\">Congrats! You have successfully installed your app!
|
||||
<br>
|
||||
Checkout <a href=\\"https://probot.github.io/docs/webhooks/\\">Receiving webhooks</a> and <a href=\\"https://probot.github.io/docs/github-api/\\">Interacting with GitHub</a> to learn more!</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
||||
|
||||
exports[`setupView > providing with name 1`] = `
|
||||
"<!DOCTYPE html>
|
||||
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||
<head>
|
||||
<meta charset=\\"UTF-8\\">
|
||||
<meta name=\\"viewport\\" content=\\"width=device-width, initial-scale=1.0\\">
|
||||
<meta http-equiv=\\"X-UA-Compatible\\" content=\\"ie=edge\\">
|
||||
<title>Setup My App | built with Probot</title>
|
||||
<link rel=\\"icon\\" href=\\"/probot/static/probot-head.png\\">
|
||||
<link rel=\\"stylesheet\\" href=\\"/probot/static/primer.css\\">
|
||||
</head>
|
||||
<body class=\\"height-full bg-gray-light\\">
|
||||
<div class=\\"d-flex flex-column flex-justify-center flex-items-center text-center height-full\\">
|
||||
<img src=\\"/probot/static/robot.svg\\" alt=\\"Probot Logo\\" width=\\"100\\" class=\\"mb-6\\">
|
||||
<div class=\\"box-shadow rounded-2 border p-6 bg-white\\">
|
||||
<div class=\\"text-center\\">
|
||||
<h1 class=\\"alt-h3 mb-2\\">Congrats! You have successfully installed your app!
|
||||
<br>
|
||||
Checkout <a href=\\"https://probot.github.io/docs/webhooks/\\">Receiving webhooks</a> and <a href=\\"https://probot.github.io/docs/github-api/\\">Interacting with GitHub</a> to learn more!</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class=\\"mt-4\\">
|
||||
<h4 class=\\"alt-h4 text-gray-light\\">Need help?</h4>
|
||||
<div class=\\"d-flex flex-justify-center mt-2\\">
|
||||
<a href=\\"https://probot.github.io/docs/\\" class=\\"btn btn-outline mr-2\\">Documentation</a>
|
||||
<a href=\\"https://github.com/probot/probot/discussions\\" class=\\"btn btn-outline\\">Discuss on GitHub</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>"
|
||||
`;
|
|
@ -0,0 +1,31 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { importView } from "../../src/views/import.js";
|
||||
|
||||
describe("importView", () => {
|
||||
test("only providing GH_HOST ", () => {
|
||||
expect(
|
||||
importView({
|
||||
GH_HOST: "https://github.com",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('providing "My App" as name ', () => {
|
||||
expect(
|
||||
importView({
|
||||
name: "My App",
|
||||
GH_HOST: "https://github.com",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("providing a smee-url as WEBHOOK_PROXY_URL ", () => {
|
||||
expect(
|
||||
importView({
|
||||
WEBHOOK_PROXY_URL: "https://smee.io/1234",
|
||||
GH_HOST: "https://github.com",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,33 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { probotView } from "../../src/views/probot.js";
|
||||
|
||||
describe("probotView", () => {
|
||||
test("not providing parameters", () => {
|
||||
expect(probotView({})).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("providing name", () => {
|
||||
expect(
|
||||
probotView({
|
||||
name: "My App",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("providing description", () => {
|
||||
expect(
|
||||
probotView({
|
||||
description: "My App with Probot",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("providing description", () => {
|
||||
expect(
|
||||
probotView({
|
||||
version: "1.0.0",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,48 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { setupView } from "../../src/views/setup.js";
|
||||
|
||||
describe("setupView", () => {
|
||||
test("providing bare minimum ", () => {
|
||||
expect(
|
||||
setupView({
|
||||
createAppUrl:
|
||||
"https://github.com/organizations/probot/settings/apps/new",
|
||||
manifest: JSON.stringify({ name: "My App" }),
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("provide also name", () => {
|
||||
expect(
|
||||
setupView({
|
||||
name: "My App",
|
||||
createAppUrl:
|
||||
"https://github.com/organizations/probot/settings/apps/new",
|
||||
manifest: JSON.stringify({ name: "My App" }),
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("provide also version", () => {
|
||||
expect(
|
||||
setupView({
|
||||
version: "1.0.0",
|
||||
createAppUrl:
|
||||
"https://github.com/organizations/probot/settings/apps/new",
|
||||
manifest: JSON.stringify({ name: "My App" }),
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test("provide also description", () => {
|
||||
expect(
|
||||
setupView({
|
||||
description: "Awesome App with Probot",
|
||||
createAppUrl:
|
||||
"https://github.com/organizations/probot/settings/apps/new",
|
||||
manifest: JSON.stringify({ name: "My App" }),
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,16 @@
|
|||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import { successView } from "../../src/views/success.js";
|
||||
|
||||
describe("setupView", () => {
|
||||
test("not providing name ", () => {
|
||||
expect(successView({})).toMatchSnapshot();
|
||||
});
|
||||
test("providing with name ", () => {
|
||||
expect(
|
||||
successView({
|
||||
name: "My App",
|
||||
}),
|
||||
).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -1,28 +1,29 @@
|
|||
import express, { Response } from "express";
|
||||
// tslint:disable-next-line:no-var-requires
|
||||
import { randomInt } from "node:crypto";
|
||||
import http from "node:http";
|
||||
import net from "node:net";
|
||||
|
||||
import express, { type Response } from "express";
|
||||
const sse: (
|
||||
req: express.Request,
|
||||
res: express.Response,
|
||||
next: express.NextFunction
|
||||
next: express.NextFunction,
|
||||
) => void = require("connect-sse")();
|
||||
import fetchMock from "fetch-mock";
|
||||
import EventSource from "eventsource";
|
||||
import http from "http";
|
||||
import net from "net";
|
||||
import nock from "nock";
|
||||
import { getLog } from "../src/helpers/get-log";
|
||||
import { createWebhookProxy } from "../src/helpers/webhook-proxy";
|
||||
import { describe, expect, afterEach, test, vi } from "vitest";
|
||||
import { getLog } from "../src/helpers/get-log.js";
|
||||
import { createWebhookProxy } from "../src/helpers/webhook-proxy.js";
|
||||
|
||||
const targetPort = 999999;
|
||||
let targetPort = 999999;
|
||||
|
||||
interface SSEResponse extends Response {
|
||||
json(body: any, status?: string): this;
|
||||
}
|
||||
|
||||
jest.setTimeout(10000);
|
||||
|
||||
describe("webhook-proxy", () => {
|
||||
// tslint:disable-next-line:one-variable-per-declaration
|
||||
let emit: SSEResponse["json"], proxy: EventSource, server: http.Server;
|
||||
let emit: SSEResponse["json"];
|
||||
let proxy: EventSource;
|
||||
let server: http.Server;
|
||||
|
||||
afterEach(() => {
|
||||
server && server.close();
|
||||
|
@ -30,59 +31,111 @@ describe("webhook-proxy", () => {
|
|||
});
|
||||
|
||||
describe("with a valid proxy server", () => {
|
||||
beforeEach((done) => {
|
||||
test("forwards events to server", async () => {
|
||||
let readyPromise = {
|
||||
promise: undefined,
|
||||
reject: undefined,
|
||||
resolve: undefined,
|
||||
} as {
|
||||
promise?: Promise<any>;
|
||||
resolve?: (value?: any) => any;
|
||||
reject?: (reason?: any) => any;
|
||||
};
|
||||
|
||||
readyPromise.promise = new Promise((resolve, reject) => {
|
||||
readyPromise.resolve = resolve;
|
||||
readyPromise.reject = reject;
|
||||
});
|
||||
|
||||
let finishedPromise = {
|
||||
promise: undefined,
|
||||
reject: undefined,
|
||||
resolve: undefined,
|
||||
} as {
|
||||
promise?: Promise<any>;
|
||||
resolve?: (value?: any) => any;
|
||||
reject?: (reason?: any) => any;
|
||||
};
|
||||
|
||||
finishedPromise.promise = new Promise((resolve, reject) => {
|
||||
finishedPromise.resolve = resolve;
|
||||
finishedPromise.reject = reject;
|
||||
});
|
||||
|
||||
const app = express();
|
||||
|
||||
app.get("/events", sse, (req, res: SSEResponse) => {
|
||||
app.get("/events", sse, (_req, res: SSEResponse) => {
|
||||
res.json({}, "ready");
|
||||
emit = res.json;
|
||||
});
|
||||
|
||||
server = app.listen(0, () => {
|
||||
const url = `http://127.0.0.1:${
|
||||
(server.address() as net.AddressInfo).port
|
||||
}/events`;
|
||||
proxy = createWebhookProxy({
|
||||
server = app.listen(0, async () => {
|
||||
targetPort = (server.address() as net.AddressInfo).port;
|
||||
const url = `http://127.0.0.1:${targetPort}/events`;
|
||||
|
||||
const fetch = fetchMock
|
||||
.sandbox()
|
||||
.postOnce(`http://localhost:${targetPort}/test`, {
|
||||
status: 200,
|
||||
then: () => {
|
||||
finishedPromise.resolve!();
|
||||
},
|
||||
});
|
||||
|
||||
proxy = (await createWebhookProxy({
|
||||
url,
|
||||
port: targetPort,
|
||||
path: "/test",
|
||||
logger: getLog({ level: "fatal" }),
|
||||
})!;
|
||||
fetch,
|
||||
})) as EventSource;
|
||||
|
||||
// Wait for proxy to be ready
|
||||
proxy.addEventListener("ready", () => done());
|
||||
proxy.addEventListener("ready", readyPromise.resolve!);
|
||||
});
|
||||
});
|
||||
|
||||
test("forwards events to server", (done) => {
|
||||
nock(`http://localhost:${targetPort}`)
|
||||
.post("/test")
|
||||
.reply(200, () => {
|
||||
done();
|
||||
});
|
||||
|
||||
const body = { action: "foo" };
|
||||
await readyPromise.promise;
|
||||
|
||||
emit({
|
||||
body,
|
||||
body: { action: "foo" },
|
||||
"x-github-event": "test",
|
||||
});
|
||||
|
||||
await finishedPromise.promise;
|
||||
});
|
||||
});
|
||||
|
||||
test("logs an error when the proxy server is not found", (done) => {
|
||||
const url = "http://bad.proxy/events";
|
||||
nock("http://bad.proxy").get("/events").reply(404);
|
||||
test("logs an error when the proxy server is not found", async () => {
|
||||
expect.assertions(2);
|
||||
|
||||
const log = getLog({ level: "fatal" }).child({});
|
||||
log.error = jest.fn();
|
||||
let finishedPromise = {
|
||||
promise: undefined,
|
||||
reject: undefined,
|
||||
resolve: undefined,
|
||||
} as {
|
||||
promise?: Promise<any>;
|
||||
resolve?: (value?: any) => any;
|
||||
reject?: (reason?: any) => any;
|
||||
};
|
||||
|
||||
proxy = createWebhookProxy({ url, logger: log })!;
|
||||
|
||||
proxy.addEventListener("error", (error: any) => {
|
||||
expect(error.status).toBe(404);
|
||||
expect(log.error).toHaveBeenCalledWith(error);
|
||||
done();
|
||||
finishedPromise.promise = new Promise((resolve, reject) => {
|
||||
finishedPromise.resolve = resolve;
|
||||
finishedPromise.reject = reject;
|
||||
});
|
||||
|
||||
const url = `http://bad.n${randomInt(1e10).toString(36)}.proxy/events`;
|
||||
|
||||
const logger = getLog({ level: "fatal" }).child({});
|
||||
logger.error = vi.fn() as any;
|
||||
|
||||
createWebhookProxy({ url, logger })!.then((proxy) => {
|
||||
(proxy as EventSource).addEventListener("error", (error: any) => {
|
||||
expect(error.message).toMatch(/^getaddrinfo ENOTFOUND/);
|
||||
expect(logger.error).toHaveBeenCalledWith(error);
|
||||
finishedPromise.resolve!();
|
||||
});
|
||||
});
|
||||
|
||||
await finishedPromise.promise;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
{
|
||||
"extends": "@tsconfig/node10/tsconfig.json",
|
||||
"extends": "@octokit/tsconfig",
|
||||
"compilerOptions": {
|
||||
"module": "Node16",
|
||||
"verbatimModuleSyntax": false,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUnusedLocals": true,
|
||||
|
@ -12,7 +14,10 @@
|
|||
"noImplicitAny": true,
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"resolveJsonModule": true
|
||||
"resolveJsonModule": true,
|
||||
"allowJs": true,
|
||||
"lib": ["es2023", "dom"],
|
||||
"moduleResolution": "node16"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"compileOnSave": false
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { defineConfig } from 'vitest/config'
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['test/**/*.test.ts'],
|
||||
coverage: {
|
||||
provider: "v8",
|
||||
},
|
||||
},
|
||||
})
|
Loading…
Reference in New Issue