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
|
name: Docs
|
||||||
"on":
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
@ -8,14 +8,18 @@ jobs:
|
||||||
name: docs
|
name: docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
- run: git fetch --depth=20 origin +refs/tags/*:refs/tags/*
|
- run: git fetch --depth=20 origin +refs/tags/*:refs/tags/*
|
||||||
- uses: actions/setup-node@v2
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 16
|
node-version: lts/*
|
||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- name: Install dependencies
|
||||||
- run: npm run build
|
run: npm ci
|
||||||
- run: ./script/publish-docs
|
- name: Build probot
|
||||||
|
run: npm run build
|
||||||
|
- name: Publish documentation
|
||||||
|
run: ./script/publish-docs
|
||||||
env:
|
env:
|
||||||
OCTOKITBOT_PAT: ${{ secrets.OCTOKITBOT_PAT }}
|
OCTOKITBOT_PAT: ${{ secrets.OCTOKITBOT_PAT }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: Release
|
name: Release
|
||||||
"on":
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
@ -11,14 +11,19 @@ jobs:
|
||||||
name: release
|
name: release
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Check out repository
|
||||||
- uses: actions/setup-node@v2
|
uses: actions/checkout@v4
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: lts/*
|
node-version: lts/*
|
||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- name: Install dependencies
|
||||||
- run: npm run build
|
run: npm ci
|
||||||
- run: npx semantic-release
|
- name: Build
|
||||||
|
run: npm run build
|
||||||
|
- name: Release
|
||||||
|
run: npx semantic-release
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
name: Test
|
name: Test
|
||||||
"on":
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
@ -7,43 +7,140 @@ name: Test
|
||||||
types:
|
types:
|
||||||
- opened
|
- opened
|
||||||
- synchronize
|
- synchronize
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
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:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
node-version:
|
node-version:
|
||||||
- 10
|
- 18
|
||||||
- 12
|
- 20
|
||||||
- 14
|
- 21
|
||||||
os:
|
os:
|
||||||
- ubuntu-latest
|
- ubuntu-latest
|
||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
|
fail-fast: false
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
- name: Use Node.js ${{ matrix.node-version }}
|
- name: Use Node.js ${{ matrix.node-version }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: ${{ matrix.node-version }}
|
node-version: ${{ matrix.node-version }}
|
||||||
cache: npm
|
cache: npm
|
||||||
- run: npm ci
|
- name: Install dependencies
|
||||||
- run: npm run build
|
run: npm ci
|
||||||
- run: npx jest
|
- 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:
|
test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
needs: test_matrix
|
needs:
|
||||||
|
- test-unit
|
||||||
|
- test-redis
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- run: exit 1
|
||||||
- uses: actions/setup-node@v2
|
if: ${{ needs.test-unit.result != 'success' || needs.test-redis.result != 'success' }}
|
||||||
|
- name: Check out repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Use Node.js LTS
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: lts/*
|
||||||
cache: npm
|
cache: npm
|
||||||
node-version: 16
|
- name: Install Dependencies
|
||||||
- run: npm ci
|
run: npm ci
|
||||||
- run: npm run lint
|
- name: Build
|
||||||
- run: npm run build
|
run: npm run build
|
||||||
- run: npx jest
|
- name: Test
|
||||||
- name: codecov
|
run: npm run test:coverage
|
||||||
|
- name: Update Codecov
|
||||||
run: npx codecov
|
run: npx codecov
|
||||||
env:
|
env:
|
||||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
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.
|
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
|
## 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:
|
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 |
|
| 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> |
|
| `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> |
|
| `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 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_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> |
|
| `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).
|
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:
|
Some less common environment variables are:
|
||||||
|
|
||||||
| Environment Variable | [Programmatic Argument](/docs/development/#run-probot-programmatically) | Description |
|
| 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_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> |
|
| `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). |
|
| `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_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` | `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_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` |
|
| `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> |
|
| `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` |
|
| `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. |
|
| `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: `/` |
|
| `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> |
|
| `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).
|
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
|
# 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:**
|
**Contents:**
|
||||||
|
|
||||||
|
@ -224,7 +224,7 @@ const app = require("./index.js");
|
||||||
module.exports = createNodeMiddleware(app, { probot: createProbot() });
|
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
|
```js
|
||||||
module.exports = createNodeMiddleware(app, {
|
module.exports = createNodeMiddleware(app, {
|
||||||
|
|
|
@ -53,7 +53,7 @@ module.exports = (app) => {
|
||||||
context.octokit.issues.createComment(
|
context.octokit.issues.createComment(
|
||||||
context.issue({
|
context.issue({
|
||||||
body: `There were ${edits} edits to issues in this thread.`,
|
body: `There were ${edits} edits to issues in this thread.`,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
|
@ -17,7 +17,7 @@ module.exports = (app) => {
|
||||||
response.data.issues.forEach((issue) => {
|
response.data.issues.forEach((issue) => {
|
||||||
context.log.info("Issue: %s", issue.title);
|
context.log.info("Issue: %s", issue.title);
|
||||||
});
|
});
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -33,7 +33,7 @@ module.exports = (app) => {
|
||||||
const allIssues = await context.octokit.paginate(
|
const allIssues = await context.octokit.paginate(
|
||||||
context.octokit.issues.list,
|
context.octokit.issues.list,
|
||||||
context.repo(),
|
context.repo(),
|
||||||
(response) => response.data
|
(response) => response.data,
|
||||||
);
|
);
|
||||||
console.log(allIssues);
|
console.log(allIssues);
|
||||||
});
|
});
|
||||||
|
@ -58,7 +58,7 @@ module.exports = (app) => {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -66,14 +66,14 @@ module.exports = (app) => {
|
||||||
|
|
||||||
## Async iterators
|
## 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
|
```js
|
||||||
module.exports = (app) => {
|
module.exports = (app) => {
|
||||||
app.on("issues.opened", async (context) => {
|
app.on("issues.opened", async (context) => {
|
||||||
for await (const response of octokit.paginate.iterator(
|
for await (const response of octokit.paginate.iterator(
|
||||||
context.octokit.issues.list,
|
context.octokit.issues.list,
|
||||||
context.repo()
|
context.repo(),
|
||||||
)) {
|
)) {
|
||||||
for (const issue of response.data) {
|
for (const issue of response.data) {
|
||||||
if (issue.body.includes("something")) {
|
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:
|
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.
|
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:
|
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",
|
"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": "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",
|
"lint:fix": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"docs/*.md\" *.md package.json tsconfig.json --end-of-line auto",
|
||||||
"pretest": "tsc --noEmit -p test",
|
"pretest": "npm run build && tsc --noEmit -p test",
|
||||||
"test": "jest",
|
"test": "vitest run",
|
||||||
"posttest": "npm run lint",
|
"test:coverage": "vitest run --coverage",
|
||||||
|
"test:dev": "vitest --ui --coverage",
|
||||||
"doc": "typedoc --options .typedoc.json"
|
"doc": "typedoc --options .typedoc.json"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
|
@ -35,92 +36,65 @@
|
||||||
"author": "Brandon Keepers",
|
"author": "Brandon Keepers",
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@octokit/core": "^3.2.4",
|
"@octokit/core": "^5.0.2",
|
||||||
"@octokit/plugin-enterprise-compatibility": "^1.2.8",
|
"@octokit/plugin-enterprise-compatibility": "^4.0.1",
|
||||||
"@octokit/plugin-paginate-rest": "^2.6.2",
|
"@octokit/plugin-paginate-rest": "^9.1.4",
|
||||||
"@octokit/plugin-rest-endpoint-methods": "^5.0.1",
|
"@octokit/plugin-rest-endpoint-methods": "^10.1.5",
|
||||||
"@octokit/plugin-retry": "^3.0.6",
|
"@octokit/plugin-retry": "^6.0.1",
|
||||||
"@octokit/plugin-throttling": "^3.3.4",
|
"@octokit/plugin-throttling": "^8.1.3",
|
||||||
"@octokit/types": "^8.0.0",
|
"@octokit/request": "^8.1.6",
|
||||||
"@octokit/webhooks": "^9.26.3",
|
"@octokit/types": "^12.3.0",
|
||||||
"@probot/get-private-key": "^1.1.0",
|
"@octokit/webhooks": "^12.0.10",
|
||||||
"@probot/octokit-plugin-config": "^1.0.0",
|
"@probot/get-private-key": "^1.1.2",
|
||||||
"@probot/pino": "^2.2.0",
|
"@probot/octokit-plugin-config": "^2.0.1",
|
||||||
"@types/express": "^4.17.9",
|
"@probot/pino": "^2.3.5",
|
||||||
"@types/ioredis": "^4.27.1",
|
"@types/express": "^4.17.21",
|
||||||
"@types/pino": "^6.3.4",
|
"commander": "^11.1.0",
|
||||||
"@types/pino-http": "^5.0.6",
|
"deepmerge": "^4.3.1",
|
||||||
"commander": "^6.2.0",
|
"dotenv": "^16.3.1",
|
||||||
"deepmerge": "^4.2.2",
|
|
||||||
"deprecation": "^2.3.1",
|
|
||||||
"dotenv": "^8.2.0",
|
|
||||||
"eventsource": "^2.0.2",
|
"eventsource": "^2.0.2",
|
||||||
"express": "^4.17.1",
|
"express": "^4.18.2",
|
||||||
"express-handlebars": "^6.0.3",
|
"ioredis": "^5.3.2",
|
||||||
"ioredis": "^4.27.8",
|
"js-yaml": "^4.1.0",
|
||||||
"js-yaml": "^3.14.1",
|
"lru-cache": "^10.0.3",
|
||||||
"lru-cache": "^6.0.0",
|
"octokit-auth-probot": "^2.0.0",
|
||||||
"octokit-auth-probot": "^1.2.2",
|
"pino": "^8.16.1",
|
||||||
"pino": "^6.7.0",
|
"pino-http": "^8.5.1",
|
||||||
"pino-http": "^5.3.0",
|
|
||||||
"pkg-conf": "^3.1.0",
|
"pkg-conf": "^3.1.0",
|
||||||
"resolve": "^1.19.0",
|
"resolve": "^1.22.8",
|
||||||
"semver": "^7.3.4",
|
"update-dotenv": "^1.1.1"
|
||||||
"update-dotenv": "^1.1.1",
|
|
||||||
"uuid": "^8.3.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@octokit/webhooks-examples": "^4.0.0",
|
"@octokit/tsconfig": "^2.0.0",
|
||||||
"@octokit/webhooks-methods": "^3.0.0",
|
"@octokit/webhooks-examples": "^7.3.1",
|
||||||
"@octokit/webhooks-types": "^4.0.0",
|
"@octokit/webhooks-methods": "^4.0.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@octokit/webhooks-types": "^7.3.1",
|
||||||
"@types/eventsource": "^1.1.5",
|
"@types/eventsource": "^1.1.15",
|
||||||
"@types/jest": "^26.0.18",
|
"@types/js-yaml": "^4.0.9",
|
||||||
"@types/js-yaml": "^3.12.5",
|
"@types/node": "^18.18.1",
|
||||||
"@types/jsonwebtoken": "^8.5.0",
|
"@types/resolve": "^1.20.5",
|
||||||
"@types/node": "^16.0.0",
|
"@types/supertest": "^2.0.16",
|
||||||
"@types/readable-stream": "^2.3.9",
|
"@vitest/coverage-v8": "^0.34.6",
|
||||||
"@types/resolve": "^1.17.1",
|
"@vitest/ui": "^0.34.6",
|
||||||
"@types/semver": "^7.3.4",
|
|
||||||
"@types/supertest": "^2.0.10",
|
|
||||||
"@types/uuid": "^8.3.0",
|
|
||||||
"body-parser": "^1.19.0",
|
|
||||||
"bottleneck": "^2.19.5",
|
"bottleneck": "^2.19.5",
|
||||||
"connect-sse": "^1.2.0",
|
"connect-sse": "^1.2.0",
|
||||||
"execa": "^5.0.0",
|
"execa": "^5.0.0",
|
||||||
|
"fetch-mock": "npm:@gr2m/fetch-mock@9.11.0-pull-request-644.1",
|
||||||
"get-port": "^5.1.1",
|
"get-port": "^5.1.1",
|
||||||
"got": "^11.8.0",
|
"prettier": "^3.0.3",
|
||||||
"jest": "^26.6.3",
|
"rimraf": "^5.0.5",
|
||||||
"nock": "^13.0.5",
|
"semantic-release": "^22.0.7",
|
||||||
"prettier": "^2.2.1",
|
|
||||||
"rimraf": "^3.0.2",
|
|
||||||
"semantic-release": "^19.0.3",
|
|
||||||
"semantic-release-plugin-update-version-in-files": "^1.1.0",
|
"semantic-release-plugin-update-version-in-files": "^1.1.0",
|
||||||
"smee-client": "^1.2.2",
|
"smee-client": "^2.0.0",
|
||||||
"supertest": "^6.0.1",
|
"sonic-boom": "^3.7.0",
|
||||||
"ts-jest": "^26.4.4",
|
"supertest": "^6.3.3",
|
||||||
"tsd": "^0.23.0",
|
"tsd": "^0.29.0",
|
||||||
"typedoc": "^0.22.10",
|
"typedoc": "^0.25.3",
|
||||||
"typescript": "^4.1.2"
|
"typescript": "^5.2.2",
|
||||||
|
"vitest": "^0.34.6"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=10.21"
|
"node": ">=18"
|
||||||
},
|
|
||||||
"jest": {
|
|
||||||
"testEnvironment": "node",
|
|
||||||
"coveragePathIgnorePatterns": [
|
|
||||||
"<rootDir>/lib/"
|
|
||||||
],
|
|
||||||
"moduleFileExtensions": [
|
|
||||||
"ts",
|
|
||||||
"js",
|
|
||||||
"json",
|
|
||||||
"node"
|
|
||||||
],
|
|
||||||
"preset": "ts-jest"
|
|
||||||
},
|
|
||||||
"tsd": {
|
|
||||||
"directory": "test/types"
|
|
||||||
},
|
},
|
||||||
"release": {
|
"release": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
import path from "path";
|
import { resolve } from "node:path";
|
||||||
import { ApplicationFunctionOptions, Probot } from "../index";
|
|
||||||
|
import type { ApplicationFunctionOptions, Probot } from "../index.js";
|
||||||
|
import { loadPackageJson } from "../helpers/load-package-json.js";
|
||||||
|
import { probotView } from "../views/probot.js";
|
||||||
|
|
||||||
export function defaultApp(
|
export function defaultApp(
|
||||||
app: Probot,
|
_app: Probot,
|
||||||
{ getRouter }: ApplicationFunctionOptions
|
{ getRouter, cwd = process.cwd() }: ApplicationFunctionOptions,
|
||||||
) {
|
) {
|
||||||
if (!getRouter) {
|
if (!getRouter) {
|
||||||
throw new Error("getRouter() is required for defaultApp");
|
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();
|
const router = getRouter();
|
||||||
|
|
||||||
router.get("/probot", (req, res) => {
|
router.get("/probot", (_req, res) => {
|
||||||
let pkg;
|
res.send(probotViewRendered);
|
||||||
try {
|
|
||||||
pkg = require(path.join(process.cwd(), "package.json"));
|
|
||||||
} catch (e) {
|
|
||||||
pkg = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render("probot.handlebars", pkg);
|
|
||||||
});
|
});
|
||||||
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 "node:child_process";
|
||||||
import { exec } from "child_process";
|
|
||||||
import { Request, Response } from "express";
|
import type { IncomingMessage, ServerResponse } from "http";
|
||||||
|
import { parse as parseQuery } from "querystring";
|
||||||
|
import express from "express";
|
||||||
import updateDotenv from "update-dotenv";
|
import updateDotenv from "update-dotenv";
|
||||||
|
|
||||||
import { Probot } from "../probot";
|
import { Probot } from "../probot.js";
|
||||||
import { ManifestCreation } from "../manifest-creation";
|
import { ManifestCreation } from "../manifest-creation.js";
|
||||||
import { getLoggingMiddleware } from "../server/logging-middleware";
|
import { getLoggingMiddleware } from "../server/logging-middleware.js";
|
||||||
import { ApplicationFunctionOptions } from "../types";
|
import type { ApplicationFunctionOptions } from "../types.js";
|
||||||
import { isProduction } from "../helpers/is-production";
|
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 = (
|
export const setupAppFactory = (
|
||||||
host: string | undefined,
|
host: string | undefined,
|
||||||
port: number | undefined
|
port: number | undefined,
|
||||||
) =>
|
) =>
|
||||||
async function setupApp(
|
async function setupApp(
|
||||||
app: Probot,
|
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 setup: ManifestCreation = new ManifestCreation();
|
||||||
|
const pkg = setup.pkg;
|
||||||
|
|
||||||
// If not on Glitch or Production, create a smee URL
|
// If not on Glitch or Production, create a smee URL
|
||||||
if (
|
if (
|
||||||
|
@ -31,75 +42,132 @@ export const setupAppFactory = (
|
||||||
await setup.createWebhookChannel();
|
await setup.createWebhookChannel();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!getRouter) {
|
|
||||||
throw new Error("getRouter is required to use the setup app");
|
|
||||||
}
|
|
||||||
|
|
||||||
const route = getRouter();
|
const route = getRouter();
|
||||||
|
|
||||||
route.use(getLoggingMiddleware(app.log));
|
route.use(getLoggingMiddleware(app.log));
|
||||||
|
|
||||||
printWelcomeMessage(app, host, port);
|
printWelcomeMessage(app, host, port);
|
||||||
|
|
||||||
route.get("/probot", async (req, res) => {
|
route.get("/probot", async (req: IncomingMessage, res: ServerResponse) => {
|
||||||
const baseUrl = getBaseUrl(req);
|
const baseUrl = getBaseUrl(req);
|
||||||
const pkg = setup.pkg;
|
|
||||||
const manifest = setup.getManifest(pkg, baseUrl);
|
const manifest = setup.getManifest(pkg, baseUrl);
|
||||||
const createAppUrl = setup.createAppUrl;
|
const createAppUrl = setup.createAppUrl;
|
||||||
// Pass the manifest to be POST'd
|
// 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) => {
|
route.get(
|
||||||
const { code } = req.query;
|
"/probot/setup",
|
||||||
const response = await setup.createAppFromCode(code);
|
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 (!code || typeof code !== "string" || code.length === 0) {
|
||||||
if (process.env.PROJECT_DOMAIN) {
|
res
|
||||||
exec("refresh", (error) => {
|
.writeHead(400, { "content-type": "text/plain" })
|
||||||
if (error) {
|
.end("code missing or invalid");
|
||||||
app.log.error(error);
|
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);
|
printRestartMessage(app);
|
||||||
}
|
},
|
||||||
|
);
|
||||||
|
|
||||||
res.redirect(`${response}/installations/new`);
|
const successViewRendered = successView({ name: pkg.name });
|
||||||
});
|
|
||||||
|
|
||||||
route.get("/probot/import", async (_req, res) => {
|
route.get(
|
||||||
const { WEBHOOK_PROXY_URL, GHE_HOST } = process.env;
|
"/probot/success",
|
||||||
const GH_HOST = `https://${GHE_HOST ?? "github.com"}`;
|
(_req: IncomingMessage, res: ServerResponse) => {
|
||||||
res.render("import.handlebars", { WEBHOOK_PROXY_URL, GH_HOST });
|
res
|
||||||
});
|
.writeHead(200, { "content-type": "text/html" })
|
||||||
|
.end(successViewRendered);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
route.post("/probot/import", bodyParser.json(), async (req, res) => {
|
route.get("/", (_req, res: ServerResponse) =>
|
||||||
const { appId, pem, webhook_secret } = req.body;
|
res
|
||||||
if (!appId || !pem || !webhook_secret) {
|
.writeHead(302, { "content-type": "text/plain", location: `/probot` })
|
||||||
res.status(400).send("appId and/or pem and/or webhook_secret missing");
|
.end(`Found. Redirecting to /probot`),
|
||||||
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"));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
function printWelcomeMessage(
|
function printWelcomeMessage(
|
||||||
app: Probot,
|
app: Probot,
|
||||||
host: string | undefined,
|
host: string | undefined,
|
||||||
port: number | undefined
|
port: number | undefined,
|
||||||
) {
|
) {
|
||||||
// use glitch env to get correct domain welcome message
|
// use glitch env to get correct domain welcome message
|
||||||
// https://glitch.com/help/project/
|
// https://glitch.com/help/project/
|
||||||
|
@ -127,11 +195,16 @@ function printRestartMessage(app: Probot) {
|
||||||
app.log.info("");
|
app.log.info("");
|
||||||
}
|
}
|
||||||
|
|
||||||
function getBaseUrl(req: Request): string {
|
function getBaseUrl(req: IncomingMessage): string {
|
||||||
const protocols = req.headers["x-forwarded-proto"] || req.protocol;
|
const protocols =
|
||||||
|
req.headers["x-forwarded-proto"] ||
|
||||||
|
// @ts-expect-error based on the functionality of express
|
||||||
|
req.socket?.encrypted
|
||||||
|
? "https"
|
||||||
|
: "http";
|
||||||
const protocol =
|
const protocol =
|
||||||
typeof protocols === "string" ? protocols.split(",")[0] : protocols[0];
|
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}`;
|
const baseUrl = `${protocol}://${host}`;
|
||||||
return baseUrl;
|
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.js";
|
||||||
|
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||||
import { getAuthenticatedOctokit } from "./octokit/get-authenticated-octokit";
|
import type { State } from "./types.js";
|
||||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
|
||||||
import { State } from "./types";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Authenticate and get a GitHub client that can be used to make API calls.
|
* 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
|
* @param id - ID of the installation, which can be extracted from
|
||||||
* `context.payload.installation.id`. If called without this parameter, the
|
* `context.payload.installation.id`. If called without this parameter, the
|
||||||
|
@ -32,10 +31,6 @@ import { State } from "./types";
|
||||||
export async function auth(
|
export async function auth(
|
||||||
state: State,
|
state: State,
|
||||||
installationId?: number,
|
installationId?: number,
|
||||||
log?: Logger
|
|
||||||
): Promise<InstanceType<typeof ProbotOctokit>> {
|
): Promise<InstanceType<typeof ProbotOctokit>> {
|
||||||
return getAuthenticatedOctokit(
|
return getAuthenticatedOctokit(Object.assign({}, state), installationId);
|
||||||
Object.assign({}, state, log ? { log } : null),
|
|
||||||
installationId
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
// Usage: probot receive -e push -p path/to/payload app.js
|
// 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 express, { Router } from "express";
|
||||||
|
import { config as dotenvConfig } from "dotenv";
|
||||||
|
dotenvConfig();
|
||||||
|
|
||||||
require("dotenv").config();
|
import { program } from "commander";
|
||||||
|
|
||||||
import path from "path";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
import program from "commander";
|
|
||||||
import { getPrivateKey } from "@probot/get-private-key";
|
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 { Probot, type ApplicationFunctionOptions } from "../index.js";
|
||||||
import { resolveAppFunction } from "../helpers/resolve-app-function";
|
import { resolveAppFunction } from "../helpers/resolve-app-function.js";
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
program
|
program
|
||||||
|
@ -19,48 +20,48 @@ async function main() {
|
||||||
.option(
|
.option(
|
||||||
"-e, --event <event-name>",
|
"-e, --event <event-name>",
|
||||||
"Event name",
|
"Event name",
|
||||||
process.env.GITHUB_EVENT_NAME
|
process.env.GITHUB_EVENT_NAME,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-p, --payload-path <payload-path>",
|
"-p, --payload-path <payload-path>",
|
||||||
"Path to the event payload",
|
"Path to the event payload",
|
||||||
process.env.GITHUB_EVENT_PATH
|
process.env.GITHUB_EVENT_PATH,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-t, --token <access-token>",
|
"-t, --token <access-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("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||||
.option(
|
.option(
|
||||||
"-P, --private-key <file>",
|
"-P, --private-key <file>",
|
||||||
"Path to private key file (.pem) for the GitHub App",
|
"Path to private key file (.pem) for the GitHub App",
|
||||||
process.env.PRIVATE_KEY_PATH
|
process.env.PRIVATE_KEY_PATH,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-L, --log-level <level>",
|
"-L, --log-level <level>",
|
||||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||||
process.env.LOG_LEVEL
|
process.env.LOG_LEVEL,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--log-format <format>",
|
"--log-format <format>",
|
||||||
'One of: "pretty", "json"',
|
'One of: "pretty", "json"',
|
||||||
process.env.LOG_LEVEL || "pretty"
|
process.env.LOG_LEVEL || "pretty",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--log-level-in-string",
|
"--log-level-in-string",
|
||||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
"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(
|
.option(
|
||||||
"--log-message-key",
|
"--log-message-key",
|
||||||
"Set to the string key for the 'message' in the log JSON object",
|
"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(
|
.option(
|
||||||
"--sentry-dsn <dsn>",
|
"--sentry-dsn <dsn>",
|
||||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||||
process.env.SENTRY_DSN
|
process.env.SENTRY_DSN,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--base-url <url>",
|
"--base-url <url>",
|
||||||
|
@ -69,38 +70,51 @@ async function main() {
|
||||||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||||
process.env.GHE_HOST
|
process.env.GHE_HOST
|
||||||
}/api/v3`
|
}/api/v3`
|
||||||
: "https://api.github.com"
|
: "https://api.github.com",
|
||||||
)
|
)
|
||||||
.parse(process.argv);
|
.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();
|
program.help();
|
||||||
}
|
}
|
||||||
|
|
||||||
const privateKey = getPrivateKey();
|
const privateKey = getPrivateKey();
|
||||||
if (!githubToken && (!program.app || !privateKey)) {
|
if (!githubToken && (!appId || !privateKey)) {
|
||||||
console.warn(
|
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({
|
const log = getLog({
|
||||||
level: program.logLevel,
|
level: logLevel,
|
||||||
logFormat: program.logFormat,
|
logFormat,
|
||||||
logLevelInString: program.logLevelInString,
|
logLevelInString,
|
||||||
logMessageKey: program.logMessageKey,
|
logMessageKey,
|
||||||
sentryDsn: program.sentryDsn,
|
sentryDsn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const probot = new Probot({
|
const probot = new Probot({
|
||||||
appId: program.app,
|
appId,
|
||||||
privateKey: String(privateKey),
|
privateKey: String(privateKey),
|
||||||
githubToken: githubToken,
|
githubToken: githubToken,
|
||||||
log,
|
log,
|
||||||
baseUrl: program.baseUrl,
|
baseUrl: baseUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const expressApp = express();
|
const expressApp = express();
|
||||||
|
@ -113,12 +127,12 @@ async function main() {
|
||||||
};
|
};
|
||||||
|
|
||||||
const appFn = await resolveAppFunction(
|
const appFn = await resolveAppFunction(
|
||||||
path.resolve(process.cwd(), program.args[0])
|
path.resolve(process.cwd(), program.args[0]),
|
||||||
);
|
);
|
||||||
await probot.load(appFn, options);
|
await probot.load(appFn, options);
|
||||||
|
|
||||||
probot.log.debug("Receiving event", program.event);
|
probot.log.debug("Receiving event", event);
|
||||||
probot.receive({ name: program.event, payload, id: uuidv4() }).catch(() => {
|
probot.receive({ name: event, payload, id: uuidv4() }).catch(() => {
|
||||||
// Process must exist non-zero to indicate that the action failed to run
|
// Process must exist non-zero to indicate that the action failed to run
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import { run } from "../";
|
import { run } from "../index.js";
|
||||||
|
|
||||||
run(process.argv);
|
run(process.argv);
|
||||||
|
|
|
@ -1,24 +1,30 @@
|
||||||
import semver from "semver";
|
import { resolve } from "node:path";
|
||||||
import program from "commander";
|
|
||||||
|
|
||||||
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)) {
|
dotenvConfig();
|
||||||
console.log(
|
|
||||||
`Node.js version ${pkg.engines.node} is required. You have ${process.version}.`
|
//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);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
program
|
program
|
||||||
.version(pkg.version)
|
.version(pkg.version || "0.0.0-dev")
|
||||||
.usage("<command> [options]")
|
.usage("<command> [options]")
|
||||||
.command("run", "run the bot")
|
.command("run", "run the bot")
|
||||||
.command("receive", "Receive a single event and payload")
|
.command("receive", "Receive a single event and payload")
|
||||||
.on("command:*", (cmd) => {
|
.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`);
|
console.error(`Invalid command: ${program.args.join(" ")}\n`);
|
||||||
program.outputHelp();
|
program.outputHelp();
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|
|
@ -1,65 +1,65 @@
|
||||||
import program from "commander";
|
import { program } from "commander";
|
||||||
import { getPrivateKey } from "@probot/get-private-key";
|
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(
|
export function readCliOptions(
|
||||||
argv: string[]
|
argv: string[],
|
||||||
): Options & PinoOptions & { args: string[] } {
|
): Options & PinoOptions & { args: string[] } {
|
||||||
program
|
program
|
||||||
.usage("[options] <apps...>")
|
.usage("[options] <apps...>")
|
||||||
.option(
|
.option(
|
||||||
"-p, --port <n>",
|
"-p, --port <n>",
|
||||||
"Port to start the server on",
|
"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("-H --host <host>", "Host to start the server on", process.env.HOST)
|
||||||
.option(
|
.option(
|
||||||
"-W, --webhook-proxy <url>",
|
"-W, --webhook-proxy <url>",
|
||||||
"URL of the webhook proxy service.`",
|
"URL of the webhook proxy service.`",
|
||||||
process.env.WEBHOOK_PROXY_URL
|
process.env.WEBHOOK_PROXY_URL,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-w, --webhook-path <path>",
|
"-w, --webhook-path <path>",
|
||||||
"URL path which receives webhooks. Ex: `/webhook`",
|
"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("-a, --app <id>", "ID of the GitHub App", process.env.APP_ID)
|
||||||
.option(
|
.option(
|
||||||
"-s, --secret <secret>",
|
"-s, --secret <secret>",
|
||||||
"Webhook secret of the GitHub App",
|
"Webhook secret of the GitHub App",
|
||||||
process.env.WEBHOOK_SECRET
|
process.env.WEBHOOK_SECRET,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-P, --private-key <file>",
|
"-P, --private-key <file>",
|
||||||
"Path to private key file (.pem) for the GitHub App",
|
"Path to private key file (.pem) for the GitHub App",
|
||||||
process.env.PRIVATE_KEY_PATH
|
process.env.PRIVATE_KEY_PATH,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"-L, --log-level <level>",
|
"-L, --log-level <level>",
|
||||||
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
'One of: "trace" | "debug" | "info" | "warn" | "error" | "fatal"',
|
||||||
process.env.LOG_LEVEL || "info"
|
process.env.LOG_LEVEL || "info",
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--log-format <format>",
|
"--log-format <format>",
|
||||||
'One of: "pretty", "json"',
|
'One of: "pretty", "json"',
|
||||||
process.env.LOG_FORMAT
|
process.env.LOG_FORMAT,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--log-level-in-string",
|
"--log-level-in-string",
|
||||||
"Set to log levels (trace, debug, info, ...) as words instead of numbers (10, 20, 30, ...)",
|
"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(
|
.option(
|
||||||
"--sentry-dsn <dsn>",
|
"--sentry-dsn <dsn>",
|
||||||
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
'Set to your Sentry DSN, e.g. "https://1234abcd@sentry.io/12345"',
|
||||||
process.env.SENTRY_DSN
|
process.env.SENTRY_DSN,
|
||||||
)
|
)
|
||||||
.option(
|
.option(
|
||||||
"--redis-url <url>",
|
"--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"',
|
'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(
|
.option(
|
||||||
"--base-url <url>",
|
"--base-url <url>",
|
||||||
|
@ -68,7 +68,7 @@ export function readCliOptions(
|
||||||
? `${process.env.GHE_PROTOCOL || "https"}://${
|
? `${process.env.GHE_PROTOCOL || "https"}://${
|
||||||
process.env.GHE_HOST
|
process.env.GHE_HOST
|
||||||
}/api/v3`
|
}/api/v3`
|
||||||
: "https://api.github.com"
|
: "https://api.github.com",
|
||||||
)
|
)
|
||||||
.parse(argv);
|
.parse(argv);
|
||||||
|
|
||||||
|
@ -77,12 +77,13 @@ export function readCliOptions(
|
||||||
privateKey: privateKeyPath,
|
privateKey: privateKeyPath,
|
||||||
redisUrl,
|
redisUrl,
|
||||||
...options
|
...options
|
||||||
} = program;
|
} = program.opts();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
privateKey: getPrivateKey({ filepath: privateKeyPath }) || undefined,
|
privateKey: getPrivateKey({ filepath: privateKeyPath }) || undefined,
|
||||||
appId,
|
appId,
|
||||||
redisConfig: redisUrl,
|
redisConfig: redisUrl,
|
||||||
|
args: program.args,
|
||||||
...options,
|
...options,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { getPrivateKey } from "@probot/get-private-key";
|
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(
|
export function readEnvOptions(env = process.env) {
|
||||||
env: Record<string, string | undefined> = process.env
|
|
||||||
) {
|
|
||||||
const privateKey = getPrivateKey({ env });
|
const privateKey = getPrivateKey({ env });
|
||||||
|
|
||||||
const logFormat =
|
const logFormat: PinoOptions["logFormat"] =
|
||||||
env.LOG_FORMAT || (env.NODE_ENV === "production" ? "json" : "pretty");
|
env.LOG_FORMAT && env.LOG_FORMAT.length !== 0
|
||||||
|
? env.LOG_FORMAT === "pretty"
|
||||||
|
? "pretty"
|
||||||
|
: "json"
|
||||||
|
: env.NODE_ENV === "production"
|
||||||
|
? "json"
|
||||||
|
: "pretty";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
args: [],
|
args: [],
|
||||||
|
@ -19,7 +23,7 @@ export function readEnvOptions(
|
||||||
webhookPath: env.WEBHOOK_PATH,
|
webhookPath: env.WEBHOOK_PATH,
|
||||||
webhookProxy: env.WEBHOOK_PROXY_URL,
|
webhookProxy: env.WEBHOOK_PROXY_URL,
|
||||||
logLevel: env.LOG_LEVEL as LogLevel,
|
logLevel: env.LOG_LEVEL as LogLevel,
|
||||||
logFormat: logFormat as PinoOptions["logFormat"],
|
logFormat: logFormat,
|
||||||
logLevelInString: env.LOG_LEVEL_IN_STRING === "true",
|
logLevelInString: env.LOG_LEVEL_IN_STRING === "true",
|
||||||
logMessageKey: env.LOG_MESSAGE_KEY,
|
logMessageKey: env.LOG_MESSAGE_KEY,
|
||||||
sentryDsn: env.SENTRY_DSN,
|
sentryDsn: env.SENTRY_DSN,
|
||||||
|
|
|
@ -1,14 +1,12 @@
|
||||||
import path from "path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
|
||||||
import merge from "deepmerge";
|
import merge from "deepmerge";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
EmitterWebhookEvent as WebhookEvent,
|
||||||
|
EmitterWebhookEventName as WebhookEvents,
|
||||||
|
} from "@octokit/webhooks";
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import type { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||||
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";
|
|
||||||
|
|
||||||
export type MergeOptions = merge.Options;
|
export type MergeOptions = merge.Options;
|
||||||
|
|
||||||
|
@ -31,12 +29,12 @@ type RepoIssueNumberType<T extends WebhookEvents> =
|
||||||
WebhookEvent<T>["payload"] extends { issue: { number: number } }
|
WebhookEvent<T>["payload"] extends { issue: { number: number } }
|
||||||
? number
|
? number
|
||||||
: never | WebhookEvent<T>["payload"] extends {
|
: never | WebhookEvent<T>["payload"] extends {
|
||||||
pull_request: { number: number };
|
pull_request: { number: number };
|
||||||
}
|
}
|
||||||
? number
|
? number
|
||||||
: never | WebhookEvent<T>["payload"] extends { number: number }
|
: never | WebhookEvent<T>["payload"] extends { number: number }
|
||||||
? number
|
? number
|
||||||
: never;
|
: never;
|
||||||
|
|
||||||
/** Context.repo return type */
|
/** Context.repo return type */
|
||||||
type RepoResultType<E extends WebhookEvents> = {
|
type RepoResultType<E extends WebhookEvents> = {
|
||||||
|
@ -65,20 +63,16 @@ export class Context<E extends WebhookEvents = WebhookEvents> {
|
||||||
public id: string;
|
public id: string;
|
||||||
public payload: WebhookEvent<E>["payload"];
|
public payload: WebhookEvent<E>["payload"];
|
||||||
|
|
||||||
public octokit: InstanceType<typeof ProbotOctokit>;
|
public octokit: ProbotOctokit;
|
||||||
public log: DeprecatedLogger;
|
public log: Logger;
|
||||||
|
|
||||||
constructor(
|
constructor(event: WebhookEvent<E>, octokit: ProbotOctokit, log: Logger) {
|
||||||
event: WebhookEvent<E>,
|
|
||||||
octokit: InstanceType<typeof ProbotOctokit>,
|
|
||||||
log: Logger
|
|
||||||
) {
|
|
||||||
this.name = event.name;
|
this.name = event.name;
|
||||||
this.id = event.id;
|
this.id = event.id;
|
||||||
this.payload = event.payload;
|
this.payload = event.payload;
|
||||||
|
|
||||||
this.octokit = octokit;
|
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 {
|
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;
|
const repo = this.payload.repository;
|
||||||
|
|
||||||
if (!repo) {
|
if (!repo) {
|
||||||
throw new Error(
|
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,
|
owner: repo.owner.login,
|
||||||
repo: repo.name,
|
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.
|
* @param object - Params to be merged with the issue params.
|
||||||
*/
|
*/
|
||||||
public issue<T>(
|
public issue<T>(
|
||||||
object?: T
|
object?: T,
|
||||||
): RepoResultType<E> & { issue_number: RepoIssueNumberType<E> } & T {
|
): RepoResultType<E> & { issue_number: RepoIssueNumberType<E> } & T {
|
||||||
return Object.assign(
|
return Object.assign(
|
||||||
{
|
{
|
||||||
issue_number:
|
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)
|
(this.payload.issue || this.payload.pull_request || this.payload)
|
||||||
.number,
|
.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.
|
* @param object - Params to be merged with the pull request params.
|
||||||
*/
|
*/
|
||||||
public pullRequest<T>(
|
public pullRequest<T>(
|
||||||
object?: T
|
object?: T,
|
||||||
): RepoResultType<E> & { pull_number: RepoIssueNumberType<E> } & T {
|
): RepoResultType<E> & { pull_number: RepoIssueNumberType<E> } & T {
|
||||||
const payload = this.payload;
|
const payload = this.payload;
|
||||||
return Object.assign(
|
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,
|
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>(
|
public async config<T>(
|
||||||
fileName: string,
|
fileName: string,
|
||||||
defaultConfig?: T,
|
defaultConfig?: T,
|
||||||
deepMergeOptions?: MergeOptions
|
deepMergeOptions?: MergeOptions,
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
const params = this.repo({
|
const params = this.repo({
|
||||||
path: path.posix.join(".github", fileName),
|
path: path.posix.join(".github", fileName),
|
||||||
defaults(configs: object[]) {
|
defaults(configs: object[]) {
|
||||||
const result = merge.all(
|
const result = merge.all(
|
||||||
[defaultConfig || {}, ...configs],
|
[defaultConfig || {}, ...configs],
|
||||||
deepMergeOptions
|
deepMergeOptions,
|
||||||
);
|
);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-expect-error
|
||||||
const { config, files } = await this.octokit.config.get(params);
|
const { config, files } = await this.octokit.config.get(params);
|
||||||
|
|
||||||
// if no default config is set, and no config files are found, return null
|
// 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { RequestListener } from "http";
|
import type { RequestListener } from "http";
|
||||||
import { createNodeMiddleware as createWebbhooksMiddleware } from "@octokit/webhooks";
|
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||||
|
|
||||||
import { ApplicationFunction } from "./types";
|
import type { ApplicationFunction, MiddlewareOptions } from "./types.js";
|
||||||
import { MiddlewareOptions } from "./types";
|
import { defaultWebhooksPath } from "./server/server.js";
|
||||||
|
import { createProbot } from "./create-probot.js";
|
||||||
|
|
||||||
export function createNodeMiddleware(
|
export function createNodeMiddleware(
|
||||||
appFn: ApplicationFunction,
|
appFn: ApplicationFunction,
|
||||||
{ probot, webhooksPath }: MiddlewareOptions
|
{ probot = createProbot(), webhooksPath } = {} as MiddlewareOptions,
|
||||||
): RequestListener {
|
): RequestListener {
|
||||||
probot.load(appFn);
|
probot.load(appFn);
|
||||||
|
|
||||||
return createWebbhooksMiddleware(probot.webhooks, {
|
return createWebhooksMiddleware(probot.webhooks, {
|
||||||
path: webhooksPath || "/",
|
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 { getPrivateKey } from "@probot/get-private-key";
|
||||||
|
|
||||||
import { getLog, GetLogOptions } from "./helpers/get-log";
|
import { getLog } from "./helpers/get-log.js";
|
||||||
import { Options } from "./types";
|
import type { Options } from "./types.js";
|
||||||
import { Probot } from "./probot";
|
import { Probot } from "./probot.js";
|
||||||
|
import { defaultWebhooksPath } from "./server/server.js";
|
||||||
|
|
||||||
type CreateProbotOptions = {
|
type CreateProbotOptions = {
|
||||||
overrides?: Options;
|
overrides?: Options;
|
||||||
defaults?: Options;
|
defaults?: Options;
|
||||||
env?: NodeJS.ProcessEnv;
|
env?: Partial<NodeJS.ProcessEnv>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const DEFAULTS = {
|
const DEFAULTS: Partial<NodeJS.ProcessEnv> = {
|
||||||
APP_ID: "",
|
APP_ID: "",
|
||||||
WEBHOOK_SECRET: "",
|
WEBHOOK_SECRET: "",
|
||||||
|
WEBHOOK_PATH: defaultWebhooksPath,
|
||||||
GHE_HOST: "",
|
GHE_HOST: "",
|
||||||
GHE_PROTOCOL: "",
|
GHE_PROTOCOL: "https",
|
||||||
LOG_FORMAT: "",
|
LOG_FORMAT: undefined,
|
||||||
LOG_LEVEL: "warn",
|
LOG_LEVEL: "warn",
|
||||||
LOG_LEVEL_IN_STRING: "",
|
LOG_LEVEL_IN_STRING: "false",
|
||||||
LOG_MESSAGE_KEY: "msg",
|
LOG_MESSAGE_KEY: "msg",
|
||||||
REDIS_URL: "",
|
REDIS_URL: "",
|
||||||
SENTRY_DSN: "",
|
SENTRY_DSN: "",
|
||||||
|
@ -47,6 +49,7 @@ export function createProbot({
|
||||||
privateKey: (privateKey && privateKey.toString()) || undefined,
|
privateKey: (privateKey && privateKey.toString()) || undefined,
|
||||||
secret: envWithDefaults.WEBHOOK_SECRET,
|
secret: envWithDefaults.WEBHOOK_SECRET,
|
||||||
redisConfig: envWithDefaults.REDIS_URL,
|
redisConfig: envWithDefaults.REDIS_URL,
|
||||||
|
webhookPath: envWithDefaults.WEBHOOK_PATH,
|
||||||
baseUrl: envWithDefaults.GHE_HOST
|
baseUrl: envWithDefaults.GHE_HOST
|
||||||
? `${envWithDefaults.GHE_PROTOCOL || "https"}://${
|
? `${envWithDefaults.GHE_PROTOCOL || "https"}://${
|
||||||
envWithDefaults.GHE_HOST
|
envWithDefaults.GHE_HOST
|
||||||
|
@ -60,15 +63,13 @@ export function createProbot({
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
const logOptions: GetLogOptions = {
|
const log = getLog({
|
||||||
level: probotOptions.logLevel,
|
level: probotOptions.logLevel,
|
||||||
logFormat: envWithDefaults.LOG_FORMAT as PinoOptions["logFormat"],
|
logFormat: envWithDefaults.LOG_FORMAT as PinoOptions["logFormat"],
|
||||||
logLevelInString: envWithDefaults.LOG_LEVEL_IN_STRING === "true",
|
logLevelInString: envWithDefaults.LOG_LEVEL_IN_STRING === "true",
|
||||||
logMessageKey: envWithDefaults.LOG_MESSAGE_KEY,
|
logMessageKey: envWithDefaults.LOG_MESSAGE_KEY,
|
||||||
sentryDsn: envWithDefaults.SENTRY_DSN,
|
sentryDsn: envWithDefaults.SENTRY_DSN,
|
||||||
};
|
}).child({ name: "server" });
|
||||||
|
|
||||||
const log = getLog(logOptions).child({ name: "server" });
|
|
||||||
|
|
||||||
return new Probot({
|
return new Probot({
|
||||||
log: log.child({ name: "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 type { Logger } from "pino";
|
||||||
import {
|
import type {
|
||||||
WebhookError,
|
WebhookError,
|
||||||
EmitterWebhookEvent as WebhookEvent,
|
EmitterWebhookEvent as WebhookEvent,
|
||||||
} from "@octokit/webhooks";
|
} from "@octokit/webhooks";
|
||||||
|
|
||||||
export function getErrorHandler(log: Logger) {
|
export function getErrorHandler(log: Logger) {
|
||||||
return (error: Error) => {
|
return (error: Error & { event?: WebhookEvent }) => {
|
||||||
const errors = (
|
const errors = (
|
||||||
error.name === "AggregateError" ? error : [error]
|
error.name === "AggregateError" ? error : [error]
|
||||||
) as WebhookError[];
|
) as WebhookError[];
|
||||||
|
|
||||||
const event = (error as any).event as WebhookEvent;
|
const event = error.event;
|
||||||
|
|
||||||
for (const error of errors) {
|
for (const error of errors) {
|
||||||
const errMessage = (error.message || "").toLowerCase();
|
const errMessage = (error.message || "").toLowerCase();
|
||||||
|
@ -18,7 +18,7 @@ export function getErrorHandler(log: Logger) {
|
||||||
if (errMessage.includes("x-hub-signature-256")) {
|
if (errMessage.includes("x-hub-signature-256")) {
|
||||||
log.error(
|
log.error(
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -26,7 +26,7 @@ export function getErrorHandler(log: Logger) {
|
||||||
if (errMessage.includes("pem") || errMessage.includes("json web token")) {
|
if (errMessage.includes("pem") || errMessage.includes("json web token")) {
|
||||||
log.error(
|
log.error(
|
||||||
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;
|
continue;
|
||||||
}
|
}
|
||||||
|
@ -34,7 +34,7 @@ export function getErrorHandler(log: Logger) {
|
||||||
log
|
log
|
||||||
.child({
|
.child({
|
||||||
name: "event",
|
name: "event",
|
||||||
id: event ? event.id : undefined,
|
id: event?.id,
|
||||||
})
|
})
|
||||||
.error(error);
|
.error(error);
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,10 @@
|
||||||
* app.log.fatal("Goodbye, cruel world!");
|
* app.log.fatal("Goodbye, cruel world!");
|
||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
import pino, { Logger, LoggerOptions } from "pino";
|
import { pino } from "pino";
|
||||||
import { getTransformStream, Options, LogLevel } from "@probot/pino";
|
import type { Logger, LoggerOptions } from "pino";
|
||||||
|
import { getTransformStream, type Options, type LogLevel } from "@probot/pino";
|
||||||
|
import { rebindLog } from "./rebind-log";
|
||||||
|
|
||||||
export type GetLogOptions = {
|
export type GetLogOptions = {
|
||||||
level?: LogLevel;
|
level?: LogLevel;
|
||||||
|
@ -31,9 +33,7 @@ export function getLog(options: GetLogOptions = {}): Logger {
|
||||||
messageKey: logMessageKey || "msg",
|
messageKey: logMessageKey || "msg",
|
||||||
};
|
};
|
||||||
const transform = getTransformStream(getTransformStreamOptions);
|
const transform = getTransformStream(getTransformStreamOptions);
|
||||||
// @ts-ignore TODO: check out what's wrong here
|
|
||||||
transform.pipe(pino.destination(1));
|
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 = {};
|
const defaultOptions: ResolveOptions = {};
|
||||||
|
|
||||||
export const resolveAppFunction = async (
|
export const resolveAppFunction = async (
|
||||||
appFnId: string,
|
appFnId: string,
|
||||||
opts?: ResolveOptions
|
opts?: ResolveOptions,
|
||||||
) => {
|
) => {
|
||||||
opts = opts || defaultOptions;
|
opts = opts || defaultOptions;
|
||||||
// These are mostly to ease testing
|
// These are mostly to ease testing
|
||||||
const basedir = opts.basedir || process.cwd();
|
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 appFnPath = resolver(appFnId, { basedir });
|
||||||
const mod = await import(appFnPath);
|
// On windows, an absolute path may start with a drive letter, e.g. C:/path/to/file.js
|
||||||
// Note: This needs "esModuleInterop" to be set to "true" in "tsconfig.json"
|
// This can be interpreted as a protocol, so ensure it's prefixed with file://
|
||||||
return mod.default;
|
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;
|
export type Resolver = (appFnId: string, opts: { basedir: string }) => string;
|
||||||
|
|
|
@ -2,28 +2,30 @@ import EventSource from "eventsource";
|
||||||
|
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
|
||||||
export const createWebhookProxy = (
|
export const createWebhookProxy = async (
|
||||||
opts: WebhookProxyOptions
|
opts: WebhookProxyOptions,
|
||||||
): EventSource | undefined => {
|
): Promise<EventSource | undefined> => {
|
||||||
try {
|
try {
|
||||||
const SmeeClient = require("smee-client");
|
const SmeeClient = (await import("smee-client")).default;
|
||||||
const smee = new SmeeClient({
|
const smee = new SmeeClient({
|
||||||
logger: opts.logger,
|
logger: opts.logger,
|
||||||
source: opts.url,
|
source: opts.url,
|
||||||
target: `http://localhost:${opts.port}${opts.path}`,
|
target: `http://localhost:${opts.port}${opts.path}`,
|
||||||
|
fetch: opts.fetch,
|
||||||
});
|
});
|
||||||
return smee.start();
|
return smee.start();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
opts.logger.warn(
|
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;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface WebhookProxyOptions {
|
export interface WebhookProxyOptions {
|
||||||
url?: string;
|
url: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
path?: string;
|
path?: string;
|
||||||
logger: Logger;
|
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";
|
export { Context } from "./context.js";
|
||||||
import {
|
|
||||||
|
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,
|
Options,
|
||||||
ApplicationFunction,
|
ApplicationFunction,
|
||||||
ApplicationFunctionOptions,
|
ApplicationFunctionOptions,
|
||||||
} from "./types";
|
} from "./types.js";
|
||||||
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";
|
|
||||||
|
|
||||||
export {
|
declare global {
|
||||||
Logger,
|
namespace NodeJS {
|
||||||
Context,
|
interface ProcessEnv {
|
||||||
ProbotOctokit,
|
/**
|
||||||
run,
|
* The App ID assigned to your GitHub App.
|
||||||
Probot,
|
* @example '1234'
|
||||||
Server,
|
*/
|
||||||
createNodeMiddleware,
|
APP_ID?: string;
|
||||||
createProbot,
|
|
||||||
};
|
|
||||||
|
|
||||||
/** 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 yaml from "js-yaml";
|
||||||
import path from "path";
|
|
||||||
import updateDotenv from "update-dotenv";
|
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 {
|
export class ManifestCreation {
|
||||||
get pkg() {
|
get pkg() {
|
||||||
let pkg: any;
|
return loadPackageJson();
|
||||||
try {
|
|
||||||
pkg = require(path.join(process.cwd(), "package.json"));
|
|
||||||
} catch (e) {
|
|
||||||
pkg = {};
|
|
||||||
}
|
|
||||||
return pkg;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createWebhookChannel() {
|
public async createWebhookChannel(): Promise<string | undefined> {
|
||||||
try {
|
try {
|
||||||
// tslint:disable:no-var-requires
|
const SmeeClient = (await import("smee-client")).default;
|
||||||
const SmeeClient = require("smee-client");
|
|
||||||
|
|
||||||
|
const WEBHOOK_PROXY_URL = await SmeeClient.createChannel();
|
||||||
await this.updateEnv({
|
await this.updateEnv({
|
||||||
WEBHOOK_PROXY_URL: await SmeeClient.createChannel(),
|
WEBHOOK_PROXY_URL,
|
||||||
});
|
});
|
||||||
|
return WEBHOOK_PROXY_URL;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Smee is not available, so we'll just move on
|
// 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.");
|
console.warn("Unable to connect to smee.io, try restarting your server.");
|
||||||
|
return void 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public getManifest(pkg: any, baseUrl: any) {
|
public getManifest(pkg: PackageJson, baseUrl: string) {
|
||||||
let manifest: any = {};
|
let manifest: Partial<Manifest> = {};
|
||||||
try {
|
try {
|
||||||
const file = fs.readFileSync(path.join(process.cwd(), "app.yml"), "utf8");
|
const file = fs.readFileSync(path.join(process.cwd(), "app.yml"), "utf8");
|
||||||
manifest = yaml.safeLoad(file);
|
manifest = yaml.load(file) as Manifest;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// App config does not exist, which is ok.
|
// App config does not exist, which is ok.
|
||||||
// @ts-ignore - in theory error can be anything
|
if ((error as Error & { code?: string }).code !== "ENOENT") {
|
||||||
if (error.code !== "ENOENT") {
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const generatedManifest = JSON.stringify(
|
const generatedManifest = JSON.stringify({
|
||||||
Object.assign(
|
description: manifest.description || pkg.description,
|
||||||
{
|
hook_attributes: {
|
||||||
description: manifest.description || pkg.description,
|
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`,
|
||||||
hook_attributes: {
|
},
|
||||||
url: process.env.WEBHOOK_PROXY_URL || `${baseUrl}/`,
|
name: process.env.PROJECT_DOMAIN || manifest.name || pkg.name,
|
||||||
},
|
public: manifest.public || true,
|
||||||
name: process.env.PROJECT_DOMAIN || manifest.name || pkg.name,
|
redirect_url: `${baseUrl}/probot/setup`,
|
||||||
public: manifest.public || true,
|
// TODO: add setup url
|
||||||
redirect_url: `${baseUrl}/probot/setup`,
|
// setup_url:`${baseUrl}/probot/success`,
|
||||||
// TODO: add setup url
|
url: manifest.url || pkg.homepage || pkg.repository,
|
||||||
// setup_url:`${baseUrl}/probot/success`,
|
version: "v1",
|
||||||
url: manifest.url || pkg.homepage || pkg.repository,
|
...manifest,
|
||||||
version: "v1",
|
});
|
||||||
},
|
|
||||||
manifest
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
return generatedManifest;
|
return generatedManifest;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createAppFromCode(code: any) {
|
public async createAppFromCode(code: string, probotOptions?: OctokitOptions) {
|
||||||
const octokit = new ProbotOctokit();
|
const octokit = new ProbotOctokit(probotOptions);
|
||||||
const options: any = {
|
const options: RequestParameters = {
|
||||||
|
...probotOptions,
|
||||||
code,
|
code,
|
||||||
mediaType: {
|
mediaType: {
|
||||||
previews: ["fury"], // needed for GHES 2.20 and older
|
previews: ["fury"], // needed for GHES 2.20 and older
|
||||||
|
@ -80,7 +76,7 @@ export class ManifestCreation {
|
||||||
};
|
};
|
||||||
const response = await octokit.request(
|
const response = await octokit.request(
|
||||||
"POST /app-manifests/:code/conversions",
|
"POST /app-manifests/:code/conversions",
|
||||||
options
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { id, client_id, client_secret, webhook_secret, pem } = response.data;
|
const { id, client_id, client_secret, webhook_secret, pem } = response.data;
|
||||||
|
@ -95,7 +91,7 @@ export class ManifestCreation {
|
||||||
return response.data.html_url;
|
return response.data.html_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async updateEnv(env: any) {
|
public async updateEnv(env: Env) {
|
||||||
// Needs to be public due to tests
|
// Needs to be public due to tests
|
||||||
return updateDotenv(env);
|
return updateDotenv(env);
|
||||||
}
|
}
|
||||||
|
@ -103,7 +99,7 @@ export class ManifestCreation {
|
||||||
get createAppUrl() {
|
get createAppUrl() {
|
||||||
const githubHost = process.env.GHE_HOST || `github.com`;
|
const githubHost = process.env.GHE_HOST || `github.com`;
|
||||||
return `${process.env.GHE_PROTOCOL || "https"}://${githubHost}${
|
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`;
|
}/settings/apps/new`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,17 @@
|
||||||
import { State } from "../types";
|
import type { State } from "../types.js";
|
||||||
import { ProbotOctokit } from "./probot-octokit";
|
import type { ProbotOctokit } from "./probot-octokit.js";
|
||||||
|
import type { OctokitOptions } from "../types.js";
|
||||||
|
import type { LogFn, Level } from "pino";
|
||||||
|
|
||||||
type FactoryOptions = {
|
type FactoryOptions = {
|
||||||
octokit: InstanceType<typeof ProbotOctokit>;
|
octokit: ProbotOctokit;
|
||||||
octokitOptions: ConstructorParameters<typeof ProbotOctokit> & {
|
octokitOptions: OctokitOptions;
|
||||||
throttle?: Record<string, unknown>;
|
|
||||||
auth?: Record<string, unknown>;
|
|
||||||
};
|
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
|
|
||||||
export async function getAuthenticatedOctokit(
|
export async function getAuthenticatedOctokit(
|
||||||
state: State,
|
state: State,
|
||||||
installationId?: number
|
installationId?: number,
|
||||||
) {
|
) {
|
||||||
const { log, octokit } = state;
|
const { log, octokit } = state;
|
||||||
|
|
||||||
|
@ -24,7 +23,9 @@ export async function getAuthenticatedOctokit(
|
||||||
factory: ({ octokit, octokitOptions, ...otherOptions }: FactoryOptions) => {
|
factory: ({ octokit, octokitOptions, ...otherOptions }: FactoryOptions) => {
|
||||||
const pinoLog = log.child({ name: "github" });
|
const pinoLog = log.child({ name: "github" });
|
||||||
|
|
||||||
const options = {
|
const options: ConstructorParameters<typeof ProbotOctokit>[0] & {
|
||||||
|
log: Record<Level, LogFn>;
|
||||||
|
} = {
|
||||||
...octokitOptions,
|
...octokitOptions,
|
||||||
log: {
|
log: {
|
||||||
fatal: pinoLog.fatal.bind(pinoLog),
|
fatal: pinoLog.fatal.bind(pinoLog),
|
||||||
|
@ -34,10 +35,12 @@ export async function getAuthenticatedOctokit(
|
||||||
debug: pinoLog.debug.bind(pinoLog),
|
debug: pinoLog.debug.bind(pinoLog),
|
||||||
trace: pinoLog.trace.bind(pinoLog),
|
trace: pinoLog.trace.bind(pinoLog),
|
||||||
},
|
},
|
||||||
throttle: {
|
throttle: octokitOptions.throttle?.enabled
|
||||||
...octokitOptions.throttle,
|
? {
|
||||||
id: installationId,
|
...octokitOptions.throttle,
|
||||||
},
|
id: String(installationId),
|
||||||
|
}
|
||||||
|
: { enabled: false },
|
||||||
auth: {
|
auth: {
|
||||||
...octokitOptions.auth,
|
...octokitOptions.auth,
|
||||||
otherOptions,
|
otherOptions,
|
||||||
|
@ -49,5 +52,5 @@ export async function getAuthenticatedOctokit(
|
||||||
|
|
||||||
return new Octokit(options);
|
return new Octokit(options);
|
||||||
},
|
},
|
||||||
}) as Promise<InstanceType<typeof ProbotOctokit>>;
|
}) as Promise<ProbotOctokit>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,38 @@
|
||||||
import Bottleneck from "bottleneck";
|
import Bottleneck from "bottleneck";
|
||||||
import Redis from "ioredis";
|
import { Redis, type RedisOptions } from "ioredis";
|
||||||
import { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import type { ThrottlingOptions } from "@octokit/plugin-throttling";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
log: Logger;
|
log: Logger;
|
||||||
redisConfig?: Redis.RedisOptions | string;
|
redisConfig?: RedisOptions | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getOctokitThrottleOptions(options: Options) {
|
export function getOctokitThrottleOptions(options: Options) {
|
||||||
let { log, redisConfig } = 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({
|
const connection = new Bottleneck.IORedisConnection({
|
||||||
client: getRedisClient(options),
|
client: getRedisClient(options),
|
||||||
|
@ -19,12 +41,12 @@ export function getOctokitThrottleOptions(options: Options) {
|
||||||
log.error(Object.assign(error, { source: "bottleneck" }));
|
log.error(Object.assign(error, { source: "bottleneck" }));
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
throttlingOptions.Bottleneck = Bottleneck;
|
||||||
Bottleneck,
|
throttlingOptions.connection = connection;
|
||||||
connection,
|
|
||||||
};
|
return throttlingOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRedisClient({ log, redisConfig }: Options): Redis.Redis | void {
|
function getRedisClient({ redisConfig }: Options): Redis | void {
|
||||||
if (redisConfig) return new Redis(redisConfig as Redis.RedisOptions);
|
if (redisConfig) return new Redis(redisConfig as RedisOptions);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
import LRUCache from "lru-cache";
|
import type { LRUCache } from "lru-cache";
|
||||||
import { ProbotOctokit } from "./probot-octokit";
|
import { ProbotOctokit } from "./probot-octokit.js";
|
||||||
import Redis from "ioredis";
|
import type { RedisOptions } from "ioredis";
|
||||||
|
import { request } from "@octokit/request";
|
||||||
|
|
||||||
import { getOctokitThrottleOptions } from "./get-octokit-throttle-options";
|
import { getOctokitThrottleOptions } from "./get-octokit-throttle-options.js";
|
||||||
import { aliasLog } from "../helpers/alias-log";
|
|
||||||
|
|
||||||
import type { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
|
import type { RequestRequestOptions } from "@octokit/types";
|
||||||
|
import type { OctokitOptions } from "../types.js";
|
||||||
|
|
||||||
type Options = {
|
type Options = {
|
||||||
cache: LRUCache<number, string>;
|
cache: LRUCache<number, string>;
|
||||||
|
@ -14,8 +16,10 @@ type Options = {
|
||||||
githubToken?: string;
|
githubToken?: string;
|
||||||
appId?: number;
|
appId?: number;
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
redisConfig?: Redis.RedisOptions | string;
|
redisConfig?: RedisOptions | string;
|
||||||
|
webhookPath?: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
request?: RequestRequestOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -32,11 +36,21 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
||||||
const authOptions = options.githubToken
|
const authOptions = options.githubToken
|
||||||
? {
|
? {
|
||||||
token: options.githubToken,
|
token: options.githubToken,
|
||||||
|
request: request.defaults({
|
||||||
|
request: {
|
||||||
|
fetch: options.request?.fetch,
|
||||||
|
},
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
cache: options.cache,
|
cache: options.cache,
|
||||||
appId: options.appId,
|
appId: options.appId,
|
||||||
privateKey: options.privateKey,
|
privateKey: options.privateKey,
|
||||||
|
request: request.defaults({
|
||||||
|
request: {
|
||||||
|
fetch: options.request?.fetch,
|
||||||
|
},
|
||||||
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const octokitThrottleOptions = getOctokitThrottleOptions({
|
const octokitThrottleOptions = getOctokitThrottleOptions({
|
||||||
|
@ -44,10 +58,10 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
||||||
redisConfig: options.redisConfig,
|
redisConfig: options.redisConfig,
|
||||||
});
|
});
|
||||||
|
|
||||||
let defaultOptions: any = {
|
let defaultOptions: Partial<OctokitOptions> = {
|
||||||
auth: authOptions,
|
auth: authOptions,
|
||||||
log: options.log.child
|
log: options.log.child
|
||||||
? aliasLog(options.log.child({ name: "octokit" }))
|
? options.log.child({ name: "octokit" })
|
||||||
: options.log,
|
: options.log,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -59,7 +73,7 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
||||||
defaultOptions.throttle = octokitThrottleOptions;
|
defaultOptions.throttle = octokitThrottleOptions;
|
||||||
}
|
}
|
||||||
|
|
||||||
return options.Octokit.defaults((instanceOptions: any) => {
|
return options.Octokit.defaults((instanceOptions: OctokitOptions) => {
|
||||||
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
||||||
auth: instanceOptions.auth
|
auth: instanceOptions.auth
|
||||||
? Object.assign({}, defaultOptions.auth, instanceOptions.auth)
|
? Object.assign({}, defaultOptions.auth, instanceOptions.auth)
|
||||||
|
@ -70,7 +84,7 @@ export function getProbotOctokitWithDefaults(options: Options) {
|
||||||
options.throttle = Object.assign(
|
options.throttle = Object.assign(
|
||||||
{},
|
{},
|
||||||
defaultOptions.throttle,
|
defaultOptions.throttle,
|
||||||
instanceOptions.throttle
|
instanceOptions.throttle,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,18 +1,13 @@
|
||||||
import { Webhooks } from "@octokit/webhooks";
|
import { Webhooks } from "@octokit/webhooks";
|
||||||
|
|
||||||
import { State } from "../types";
|
import type { State } from "../types.js";
|
||||||
import { getErrorHandler } from "../helpers/get-error-handler";
|
import { getErrorHandler } from "../helpers/get-error-handler.js";
|
||||||
import { webhookTransform } from "./octokit-webhooks-transform";
|
import { webhookTransform } from "./octokit-webhooks-transform.js";
|
||||||
|
|
||||||
// import { Context } from "../context";
|
|
||||||
|
|
||||||
export function getWebhooks(state: State) {
|
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({
|
const webhooks = new Webhooks({
|
||||||
secret: state.webhooks.secret!,
|
secret: state.webhooks.secret!,
|
||||||
transform: webhookTransform.bind(null, state),
|
transform: (hook) => webhookTransform(state, hook),
|
||||||
});
|
});
|
||||||
webhooks.onError(getErrorHandler(state.log));
|
webhooks.onError(getErrorHandler(state.log));
|
||||||
return webhooks;
|
return webhooks;
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
// tslint:disable-next-line
|
|
||||||
import type { Octokit } from "@octokit/core";
|
import type { Octokit } from "@octokit/core";
|
||||||
|
|
||||||
export function probotRequestLogging(octokit: Octokit) {
|
export function probotRequestLogging(octokit: Octokit) {
|
||||||
|
@ -20,7 +19,7 @@ export function probotRequestLogging(octokit: Octokit) {
|
||||||
octokit.request.endpoint.parse(options);
|
octokit.request.endpoint.parse(options);
|
||||||
const msg = `GitHub request: ${method} ${url} - ${result.status}`;
|
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);
|
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 { Context } from "../context.js";
|
||||||
import { State } from "../types";
|
import type { State } from "../types.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Probot's transform option, which extends the `event` object that is passed
|
* Probot's transform option, which extends the `event` object that is passed
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Octokit } from "@octokit/core";
|
import { Octokit } from "@octokit/core";
|
||||||
import { enterpriseCompatibility } from "@octokit/plugin-enterprise-compatibility";
|
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 { paginateRest } from "@octokit/plugin-paginate-rest";
|
||||||
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
import { legacyRestEndpointMethods } from "@octokit/plugin-rest-endpoint-methods";
|
||||||
import { retry } from "@octokit/plugin-retry";
|
import { retry } from "@octokit/plugin-retry";
|
||||||
|
@ -8,29 +8,30 @@ import { throttling } from "@octokit/plugin-throttling";
|
||||||
import { config } from "@probot/octokit-plugin-config";
|
import { config } from "@probot/octokit-plugin-config";
|
||||||
import { createProbotAuth } from "octokit-auth-probot";
|
import { createProbotAuth } from "octokit-auth-probot";
|
||||||
|
|
||||||
import { probotRequestLogging } from "./octokit-plugin-probot-request-logging";
|
import { probotRequestLogging } from "./octokit-plugin-probot-request-logging.js";
|
||||||
import { VERSION } from "../version";
|
import { VERSION } from "../version.js";
|
||||||
|
|
||||||
const defaultOptions = {
|
const defaultOptions = {
|
||||||
authStrategy: createProbotAuth,
|
authStrategy: createProbotAuth,
|
||||||
throttle: {
|
throttle: {
|
||||||
onAbuseLimit: (
|
enabled: true,
|
||||||
|
onSecondaryRateLimit: (
|
||||||
retryAfter: number,
|
retryAfter: number,
|
||||||
options: RequestOptions,
|
options: RequestOptions,
|
||||||
octokit: Octokit
|
octokit: Octokit,
|
||||||
) => {
|
) => {
|
||||||
octokit.log.warn(
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
onRateLimit: (
|
onRateLimit: (
|
||||||
retryAfter: number,
|
retryAfter: number,
|
||||||
options: RequestOptions,
|
options: RequestOptions,
|
||||||
octokit: Octokit
|
octokit: Octokit,
|
||||||
) => {
|
) => {
|
||||||
octokit.log.warn(
|
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;
|
return true;
|
||||||
},
|
},
|
||||||
|
@ -45,14 +46,18 @@ export const ProbotOctokit = Octokit.plugin(
|
||||||
legacyRestEndpointMethods,
|
legacyRestEndpointMethods,
|
||||||
enterpriseCompatibility,
|
enterpriseCompatibility,
|
||||||
probotRequestLogging,
|
probotRequestLogging,
|
||||||
config
|
config,
|
||||||
).defaults((instanceOptions: any) => {
|
).defaults((instanceOptions: any) => {
|
||||||
// merge throttle options deeply
|
// merge throttle options deeply
|
||||||
const options = Object.assign({}, defaultOptions, instanceOptions, {
|
const options = {
|
||||||
throttle: instanceOptions.throttle
|
...defaultOptions,
|
||||||
? Object.assign({}, defaultOptions.throttle, instanceOptions.throttle)
|
...instanceOptions,
|
||||||
: defaultOptions.throttle,
|
...{
|
||||||
});
|
throttle: { ...defaultOptions.throttle, ...instanceOptions?.throttle },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
return options;
|
return options;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type ProbotOctokit = InstanceType<typeof ProbotOctokit>;
|
||||||
|
|
|
@ -1,28 +1,28 @@
|
||||||
import LRUCache from "lru-cache";
|
import { LRUCache } from "lru-cache";
|
||||||
import { Logger } from "pino";
|
import type { Logger } from "pino";
|
||||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||||
|
|
||||||
import { aliasLog } from "./helpers/alias-log";
|
import { auth } from "./auth.js";
|
||||||
import { auth } from "./auth";
|
import { getLog } from "./helpers/get-log.js";
|
||||||
import { getLog } from "./helpers/get-log";
|
import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-defaults.js";
|
||||||
import { getProbotOctokitWithDefaults } from "./octokit/get-probot-octokit-with-defaults";
|
import { getWebhooks } from "./octokit/get-webhooks.js";
|
||||||
import { getWebhooks } from "./octokit/get-webhooks";
|
import { ProbotOctokit } from "./octokit/probot-octokit.js";
|
||||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
import { VERSION } from "./version.js";
|
||||||
import { VERSION } from "./version";
|
import type {
|
||||||
import {
|
|
||||||
ApplicationFunction,
|
ApplicationFunction,
|
||||||
ApplicationFunctionOptions,
|
ApplicationFunctionOptions,
|
||||||
DeprecatedLogger,
|
|
||||||
Options,
|
Options,
|
||||||
ProbotWebhooks,
|
ProbotWebhooks,
|
||||||
State,
|
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 {
|
export class Probot {
|
||||||
static version = VERSION;
|
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 {
|
const ProbotWithDefaults = class extends this {
|
||||||
constructor(...args: any[]) {
|
constructor(...args: any[]) {
|
||||||
const options = args[0] || {};
|
const options = args[0] || {};
|
||||||
|
@ -34,15 +34,16 @@ export class Probot {
|
||||||
}
|
}
|
||||||
|
|
||||||
public webhooks: ProbotWebhooks;
|
public webhooks: ProbotWebhooks;
|
||||||
public log: DeprecatedLogger;
|
public webhookPath: string;
|
||||||
|
public log: Logger;
|
||||||
public version: String;
|
public version: String;
|
||||||
public on: ProbotWebhooks["on"];
|
public on: ProbotWebhooks["on"];
|
||||||
public onAny: ProbotWebhooks["onAny"];
|
public onAny: ProbotWebhooks["onAny"];
|
||||||
public onError: ProbotWebhooks["onError"];
|
public onError: ProbotWebhooks["onError"];
|
||||||
public auth: (
|
public auth: (
|
||||||
installationId?: number,
|
installationId?: number,
|
||||||
log?: Logger
|
log?: Logger,
|
||||||
) => Promise<InstanceType<typeof ProbotOctokit>>;
|
) => Promise<ProbotOctokit>;
|
||||||
|
|
||||||
private state: State;
|
private state: State;
|
||||||
|
|
||||||
|
@ -52,14 +53,16 @@ export class Probot {
|
||||||
let level = options.logLevel;
|
let level = options.logLevel;
|
||||||
const logMessageKey = options.logMessageKey;
|
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`
|
// TODO: support redis backend for access token cache if `options.redisConfig`
|
||||||
const cache = new LRUCache<number, string>({
|
const cache = new LRUCache<number, string>({
|
||||||
// cache max. 15000 tokens, that will use less than 10mb memory
|
// cache max. 15000 tokens, that will use less than 10mb memory
|
||||||
max: 15000,
|
max: 15000,
|
||||||
// Cache for 1 minute less than GitHub expiry
|
// Cache for 1 minute less than GitHub expiry
|
||||||
maxAge: 1000 * 60 * 59,
|
ttl: 1000 * 60 * 59,
|
||||||
});
|
});
|
||||||
|
|
||||||
const Octokit = getProbotOctokitWithDefaults({
|
const Octokit = getProbotOctokitWithDefaults({
|
||||||
|
@ -68,16 +71,25 @@ export class Probot {
|
||||||
appId: Number(options.appId),
|
appId: Number(options.appId),
|
||||||
privateKey: options.privateKey,
|
privateKey: options.privateKey,
|
||||||
cache,
|
cache,
|
||||||
log: this.log,
|
log: rebindLog(this.log),
|
||||||
redisConfig: options.redisConfig,
|
redisConfig: options.redisConfig,
|
||||||
baseUrl: options.baseUrl,
|
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 = {
|
this.state = {
|
||||||
cache,
|
cache,
|
||||||
githubToken: options.githubToken,
|
githubToken: options.githubToken,
|
||||||
log: this.log,
|
log: rebindLog(this.log),
|
||||||
Octokit,
|
Octokit,
|
||||||
octokit,
|
octokit,
|
||||||
webhooks: {
|
webhooks: {
|
||||||
|
@ -87,11 +99,14 @@ export class Probot {
|
||||||
privateKey: options.privateKey,
|
privateKey: options.privateKey,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
port: options.port,
|
port: options.port,
|
||||||
|
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||||
|
request: options.request,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.auth = auth.bind(null, this.state);
|
this.auth = auth.bind(null, this.state);
|
||||||
|
|
||||||
this.webhooks = getWebhooks(this.state);
|
this.webhooks = getWebhooks(this.state);
|
||||||
|
this.webhookPath = this.state.webhookPath;
|
||||||
|
|
||||||
this.on = this.webhooks.on;
|
this.on = this.webhooks.on;
|
||||||
this.onAny = this.webhooks.onAny;
|
this.onAny = this.webhooks.onAny;
|
||||||
|
@ -107,7 +122,7 @@ export class Probot {
|
||||||
|
|
||||||
public async load(
|
public async load(
|
||||||
appFn: ApplicationFunction | ApplicationFunction[],
|
appFn: ApplicationFunction | ApplicationFunction[],
|
||||||
options: ApplicationFunctionOptions = {}
|
options: ApplicationFunctionOptions = {},
|
||||||
) {
|
) {
|
||||||
if (Array.isArray(appFn)) {
|
if (Array.isArray(appFn)) {
|
||||||
for (const fn of appFn) {
|
for (const fn of appFn) {
|
||||||
|
|
39
src/run.ts
39
src/run.ts
|
@ -1,18 +1,19 @@
|
||||||
import pkgConf from "pkg-conf";
|
import pkgConf from "pkg-conf";
|
||||||
|
|
||||||
import { ApplicationFunction, Options, ServerOptions } from "./types";
|
import type { ApplicationFunction, Options, ServerOptions } from "./types.js";
|
||||||
import { Probot } from "./index";
|
import { Probot } from "./index.js";
|
||||||
import { setupAppFactory } from "./apps/setup";
|
import { setupAppFactory } from "./apps/setup.js";
|
||||||
import { getLog, GetLogOptions } from "./helpers/get-log";
|
import { getLog } from "./helpers/get-log.js";
|
||||||
import { readCliOptions } from "./bin/read-cli-options";
|
import { readCliOptions } from "./bin/read-cli-options.js";
|
||||||
import { readEnvOptions } from "./bin/read-env-options";
|
import { readEnvOptions } from "./bin/read-env-options.js";
|
||||||
import { Server } from "./server/server";
|
import { Server } from "./server/server.js";
|
||||||
import { defaultApp } from "./apps/default";
|
import { defaultApp } from "./apps/default.js";
|
||||||
import { resolveAppFunction } from "./helpers/resolve-app-function";
|
import { resolveAppFunction } from "./helpers/resolve-app-function.js";
|
||||||
import { isProduction } from "./helpers/is-production";
|
import { isProduction } from "./helpers/is-production.js";
|
||||||
|
import { config as dotenvConfig } from "dotenv";
|
||||||
|
|
||||||
type AdditionalOptions = {
|
type AdditionalOptions = {
|
||||||
env: Record<string, string | undefined>;
|
env: NodeJS.ProcessEnv;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -21,9 +22,9 @@ type AdditionalOptions = {
|
||||||
*/
|
*/
|
||||||
export async function run(
|
export async function run(
|
||||||
appFnOrArgv: ApplicationFunction | string[],
|
appFnOrArgv: ApplicationFunction | string[],
|
||||||
additionalOptions?: AdditionalOptions
|
additionalOptions?: AdditionalOptions,
|
||||||
) {
|
) {
|
||||||
require("dotenv").config();
|
dotenvConfig();
|
||||||
|
|
||||||
const envOptions = readEnvOptions(additionalOptions?.env);
|
const envOptions = readEnvOptions(additionalOptions?.env);
|
||||||
const cliOptions = Array.isArray(appFnOrArgv)
|
const cliOptions = Array.isArray(appFnOrArgv)
|
||||||
|
@ -55,15 +56,13 @@ export async function run(
|
||||||
args,
|
args,
|
||||||
} = { ...envOptions, ...cliOptions };
|
} = { ...envOptions, ...cliOptions };
|
||||||
|
|
||||||
const logOptions: GetLogOptions = {
|
const log = getLog({
|
||||||
level,
|
level,
|
||||||
logFormat,
|
logFormat,
|
||||||
logLevelInString,
|
logLevelInString,
|
||||||
logMessageKey,
|
logMessageKey,
|
||||||
sentryDsn,
|
sentryDsn,
|
||||||
};
|
});
|
||||||
|
|
||||||
const log = getLog(logOptions);
|
|
||||||
|
|
||||||
const probotOptions: Options = {
|
const probotOptions: Options = {
|
||||||
appId,
|
appId,
|
||||||
|
@ -90,12 +89,12 @@ export async function run(
|
||||||
if (!appId) {
|
if (!appId) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"App ID is missing, and is required to run in production mode. " +
|
"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) {
|
} else if (!privateKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Certificate is missing, and is required to run in production mode. " +
|
"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)) {
|
if (Array.isArray(appFnOrArgv)) {
|
||||||
const pkg = await pkgConf("probot");
|
const pkg = await pkgConf("probot");
|
||||||
|
|
||||||
const combinedApps: ApplicationFunction = async (app) => {
|
const combinedApps: ApplicationFunction = async (_app) => {
|
||||||
await server.load(defaultApp);
|
await server.load(defaultApp);
|
||||||
|
|
||||||
if (Array.isArray(pkg.apps)) {
|
if (Array.isArray(pkg.apps)) {
|
||||||
|
|
|
@ -1,19 +1,21 @@
|
||||||
import pinoHttp from "pino-http";
|
import { randomUUID as uuidv4 } from "node:crypto";
|
||||||
import type { Logger } from "pino";
|
|
||||||
import { v4 as uuidv4 } from "uuid";
|
|
||||||
|
|
||||||
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({
|
return pinoHttp({
|
||||||
...options,
|
...options,
|
||||||
logger: logger.child({ name: "http" }),
|
logger: logger.child({ name: "http" }),
|
||||||
customSuccessMessage(res) {
|
customSuccessMessage(_req, res) {
|
||||||
const responseTime = Date.now() - res[pinoHttp.startTime];
|
const responseTime = Date.now() - res[startTime];
|
||||||
// @ts-ignore
|
|
||||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||||
},
|
},
|
||||||
customErrorMessage(err, res) {
|
customErrorMessage(_err, res) {
|
||||||
const responseTime = Date.now() - res[pinoHttp.startTime];
|
const responseTime = Date.now() - res[startTime];
|
||||||
// @ts-ignore
|
|
||||||
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
return `${res.req.method} ${res.req.url} ${res.statusCode} - ${responseTime}ms`;
|
||||||
},
|
},
|
||||||
genReqId: (req) =>
|
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 express, { Router, type Application } from "express";
|
||||||
import { join } from "path";
|
import type { Logger } from "pino";
|
||||||
import { Logger } from "pino";
|
|
||||||
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
import { createNodeMiddleware as createWebhooksMiddleware } from "@octokit/webhooks";
|
||||||
|
|
||||||
import { getLog } from "../helpers/get-log";
|
import { getLoggingMiddleware } from "./logging-middleware.js";
|
||||||
import { getLoggingMiddleware } from "./logging-middleware";
|
import { createWebhookProxy } from "../helpers/webhook-proxy.js";
|
||||||
import { createWebhookProxy } from "../helpers/webhook-proxy";
|
import { VERSION } from "../version.js";
|
||||||
import { VERSION } from "../version";
|
import type { ApplicationFunction, ServerOptions } from "../types.js";
|
||||||
import { ApplicationFunction, ServerOptions } from "../types";
|
import type { Probot } from "../index.js";
|
||||||
import { Probot } from "../";
|
import type EventSource from "eventsource";
|
||||||
import { engine } from "express-handlebars";
|
import { rebindLog } from "../helpers/rebind-log.js";
|
||||||
import EventSource from "eventsource";
|
|
||||||
|
// the default path as defined in @octokit/webhooks
|
||||||
|
export const defaultWebhooksPath = "/api/github/webhooks";
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
|
cwd?: string;
|
||||||
httpServer?: HttpServer;
|
httpServer?: HttpServer;
|
||||||
port?: number;
|
port?: number;
|
||||||
host?: string;
|
host?: string;
|
||||||
|
@ -35,69 +38,66 @@ export class Server {
|
||||||
|
|
||||||
constructor(options: ServerOptions = {} as ServerOptions) {
|
constructor(options: ServerOptions = {} as ServerOptions) {
|
||||||
this.expressApp = express();
|
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 = {
|
this.state = {
|
||||||
|
cwd: options.cwd || process.cwd(),
|
||||||
port: options.port,
|
port: options.port,
|
||||||
host: options.host,
|
host: options.host,
|
||||||
webhookPath: options.webhookPath || "/",
|
webhookPath: options.webhookPath || defaultWebhooksPath,
|
||||||
webhookProxy: options.webhookProxy,
|
webhookProxy: options.webhookProxy,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.expressApp.use(getLoggingMiddleware(this.log, options.loggingOptions));
|
this.expressApp.use(getLoggingMiddleware(this.log, options.loggingOptions));
|
||||||
this.expressApp.use(
|
this.expressApp.use(
|
||||||
"/probot/static/",
|
"/probot/static/",
|
||||||
express.static(join(__dirname, "..", "..", "static"))
|
express.static(join(__dirname, "..", "..", "static")),
|
||||||
);
|
);
|
||||||
this.expressApp.use(
|
this.expressApp.use(
|
||||||
this.state.webhookPath,
|
|
||||||
createWebhooksMiddleware(this.probotApp.webhooks, {
|
createWebhooksMiddleware(this.probotApp.webhooks, {
|
||||||
path: "/",
|
path: this.state.webhookPath,
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.expressApp.engine(
|
this.expressApp.get("/ping", (_req, res) => res.end("PONG"));
|
||||||
"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"));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async load(appFn: ApplicationFunction) {
|
public async load(appFn: ApplicationFunction) {
|
||||||
await appFn(this.probotApp, {
|
await appFn(this.probotApp, {
|
||||||
|
cwd: this.state.cwd,
|
||||||
getRouter: (path) => this.router(path),
|
getRouter: (path) => this.router(path),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async start() {
|
public async start() {
|
||||||
this.log.info(
|
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 port = this.state.port || 3000;
|
||||||
const { host, webhookPath, webhookProxy } = this.state;
|
const { host, webhookPath, webhookProxy } = this.state;
|
||||||
const printableHost = host ?? "localhost";
|
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(
|
const server = this.expressApp.listen(
|
||||||
port,
|
port,
|
||||||
...((host ? [host] : []) as any),
|
...((host ? [host] : []) as any),
|
||||||
() => {
|
async () => {
|
||||||
if (webhookProxy) {
|
if (webhookProxy) {
|
||||||
this.state.eventSource = createWebhookProxy({
|
this.state.eventSource = await createWebhookProxy({
|
||||||
logger: this.log,
|
logger: this.log,
|
||||||
path: webhookPath,
|
path: webhookPath,
|
||||||
port: port,
|
port: port,
|
||||||
url: webhookProxy,
|
url: webhookProxy,
|
||||||
}) as EventSource;
|
});
|
||||||
}
|
}
|
||||||
this.log.info(`Listening on http://${printableHost}:${port}`);
|
this.log.info(`Listening on http://${printableHost}:${port}`);
|
||||||
resolve(server);
|
resolve(server);
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
server.on("error", (error: NodeJS.ErrnoException) => {
|
server.on("error", (error: NodeJS.ErrnoException) => {
|
||||||
|
@ -110,7 +110,7 @@ export class Server {
|
||||||
this.log.error(error);
|
this.log.error(error);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
})) as HttpServer;
|
});
|
||||||
|
|
||||||
return this.state.httpServer;
|
return this.state.httpServer;
|
||||||
}
|
}
|
||||||
|
|
134
src/types.ts
134
src/types.ts
|
@ -1,17 +1,18 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import {
|
import type {
|
||||||
EmitterWebhookEvent as WebhookEvent,
|
EmitterWebhookEvent as WebhookEvent,
|
||||||
Webhooks,
|
Webhooks,
|
||||||
} from "@octokit/webhooks";
|
} from "@octokit/webhooks";
|
||||||
import LRUCache from "lru-cache";
|
import type { LRUCache } from "lru-cache";
|
||||||
import Redis from "ioredis";
|
import type { RedisOptions } from "ioredis";
|
||||||
import { Options as LoggingOptions } from "pino-http";
|
import type { Options as LoggingOptions } from "pino-http";
|
||||||
|
|
||||||
import { Probot } from "./index";
|
import { Probot } from "./index.js";
|
||||||
import { Context } from "./context";
|
import { Context } from "./context.js";
|
||||||
import { ProbotOctokit } from "./octokit/probot-octokit";
|
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 {
|
export interface Options {
|
||||||
privateKey?: string;
|
privateKey?: string;
|
||||||
|
@ -20,13 +21,15 @@ export interface Options {
|
||||||
|
|
||||||
Octokit?: typeof ProbotOctokit;
|
Octokit?: typeof ProbotOctokit;
|
||||||
log?: Logger;
|
log?: Logger;
|
||||||
redisConfig?: Redis.RedisOptions | string;
|
redisConfig?: RedisOptions | string;
|
||||||
secret?: string;
|
secret?: string;
|
||||||
logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
logLevel?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
|
||||||
logMessageKey?: string;
|
logMessageKey?: string;
|
||||||
port?: number;
|
port?: number;
|
||||||
host?: string;
|
host?: string;
|
||||||
baseUrl?: string;
|
baseUrl?: string;
|
||||||
|
request?: RequestRequestOptions;
|
||||||
|
webhookPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type State = {
|
export type State = {
|
||||||
|
@ -35,7 +38,7 @@ export type State = {
|
||||||
githubToken?: string;
|
githubToken?: string;
|
||||||
log: Logger;
|
log: Logger;
|
||||||
Octokit: typeof ProbotOctokit;
|
Octokit: typeof ProbotOctokit;
|
||||||
octokit: InstanceType<typeof ProbotOctokit>;
|
octokit: ProbotOctokit;
|
||||||
cache?: LRUCache<number, string>;
|
cache?: LRUCache<number, string>;
|
||||||
webhooks: {
|
webhooks: {
|
||||||
secret?: string;
|
secret?: string;
|
||||||
|
@ -43,23 +46,31 @@ export type State = {
|
||||||
port?: number;
|
port?: number;
|
||||||
host?: string;
|
host?: string;
|
||||||
baseUrl?: 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>;
|
type SimplifiedObject = Omit<Context, keyof WebhookEvent>;
|
||||||
export type ProbotWebhooks = Webhooks<SimplifiedObject>;
|
export type ProbotWebhooks = Webhooks<SimplifiedObject>;
|
||||||
|
|
||||||
export type DeprecatedLogger = LogFn & Logger;
|
|
||||||
|
|
||||||
export type ApplicationFunctionOptions = {
|
export type ApplicationFunctionOptions = {
|
||||||
getRouter?: (path?: string) => express.Router;
|
getRouter?: (path?: string) => express.Router;
|
||||||
|
cwd?: string;
|
||||||
[key: string]: unknown;
|
[key: string]: unknown;
|
||||||
};
|
};
|
||||||
export type ApplicationFunction = (
|
export type ApplicationFunction = (
|
||||||
app: Probot,
|
app: Probot,
|
||||||
options: ApplicationFunctionOptions
|
options: ApplicationFunctionOptions,
|
||||||
) => void | Promise<void>;
|
) => void | Promise<void>;
|
||||||
|
|
||||||
export type ServerOptions = {
|
export type ServerOptions = {
|
||||||
|
cwd?: string;
|
||||||
log?: Logger;
|
log?: Logger;
|
||||||
port?: number;
|
port?: number;
|
||||||
host?: string;
|
host?: string;
|
||||||
|
@ -67,6 +78,7 @@ export type ServerOptions = {
|
||||||
webhookProxy?: string;
|
webhookProxy?: string;
|
||||||
Probot: typeof Probot;
|
Probot: typeof Probot;
|
||||||
loggingOptions?: LoggingOptions;
|
loggingOptions?: LoggingOptions;
|
||||||
|
request?: RequestRequestOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type MiddlewareOptions = {
|
export type MiddlewareOptions = {
|
||||||
|
@ -74,3 +86,99 @@ export type MiddlewareOptions = {
|
||||||
webhooksPath?: string;
|
webhooksPath?: string;
|
||||||
[key: string]: unknown;
|
[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">
|
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<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="icon" href="/probot/static/probot-head.png">
|
||||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -20,9 +29,9 @@
|
||||||
<h3>Step 1:</h3>
|
<h3>Step 1:</h3>
|
||||||
<p class="d-block mt-2">
|
<p class="d-block mt-2">
|
||||||
Replace your app's Webhook URL with <br>
|
Replace your app's Webhook URL with <br>
|
||||||
<b>{{ WEBHOOK_PROXY_URL }}</b>
|
<b>${WEBHOOK_PROXY_URL}</b>
|
||||||
</p>
|
</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
|
You can do it here
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
@ -49,7 +58,7 @@
|
||||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||||
<div class="d-flex flex-justify-center mt-2">
|
<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.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -81,4 +90,5 @@
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</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">
|
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<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="icon" href="/probot/static/probot-head.png">
|
||||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -13,26 +22,28 @@
|
||||||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
<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="box-shadow rounded-2 border p-6 bg-white">
|
||||||
<h1>
|
<h1>
|
||||||
Welcome to {{#if name }}{{ name }}{{else}}your Probot App{{/if}}
|
Welcome to ${name || "your Probot App"}
|
||||||
{{#if version}}
|
${
|
||||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ version }}</span>
|
version
|
||||||
{{/if}}
|
? ` <span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>\n`
|
||||||
</h1>
|
: ""
|
||||||
|
} </h1>
|
||||||
|
|
||||||
{{#if description }}
|
<p>${
|
||||||
<p>{{ description }}</p>
|
description
|
||||||
{{else}}
|
? description
|
||||||
<p>This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
: 'This bot was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||||
{{/if}}
|
}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-4">
|
<div class="mt-4">
|
||||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||||
<div class="d-flex flex-justify-center mt-2">
|
<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.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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>
|
<!DOCTYPE html>
|
||||||
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<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="icon" href="/probot/static/probot-head.png">
|
||||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -13,17 +27,19 @@
|
||||||
<img src="/probot/static/robot.svg" alt="Probot Logo" width="100" class="mb-6">
|
<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="box-shadow rounded-2 border p-6 bg-white">
|
||||||
<h1>
|
<h1>
|
||||||
Welcome to {{#if pkg.name }}{{ pkg.name }}{{else}}your Probot App{{/if}}
|
Welcome to ${name || "your Probot App"}
|
||||||
{{#if version}}
|
${
|
||||||
<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v{{ pkg.version }}</span>
|
version
|
||||||
{{/if}}
|
? `<span class="Label Label--outline v-align-middle ml-2 text-gray-light">v${version}</span>`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{{#if pkg.description }}
|
<p>${
|
||||||
<p>{{ pkg.description }}</p>
|
description
|
||||||
{{else}}
|
? description
|
||||||
<p>This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.</p>
|
: 'This app was built using <a href="https://github.com/probot/probot">Probot</a>, a framework for building GitHub Apps.'
|
||||||
{{/if}}
|
}</p>
|
||||||
|
|
||||||
<div class="text-left mt-6">
|
<div class="text-left mt-6">
|
||||||
<h2 class="alt-h3 mb-2">Getting Started</h2>
|
<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>
|
<p>To start building a GitHub App, you'll need to register a new app on GitHub.</p>
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
<form action="{{ createAppUrl }}" method="post" target="_blank" class="d-flex flex-items-center">
|
<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>
|
<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>
|
<a href="/probot/import" class="ml-2">or use an existing Github App</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
@ -42,9 +58,10 @@
|
||||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||||
<div class="d-flex flex-justify-center mt-2">
|
<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.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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">
|
<html lang="en" class="height-full" data-color-mode="auto" data-light-theme="light" data-dark-theme="dark">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
<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="icon" href="/probot/static/probot-head.png">
|
||||||
<link rel="stylesheet" href="/probot/static/primer.css">
|
<link rel="stylesheet" href="/probot/static/primer.css">
|
||||||
</head>
|
</head>
|
||||||
|
@ -23,9 +24,10 @@
|
||||||
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
<h4 class="alt-h4 text-gray-light">Need help?</h4>
|
||||||
<div class="d-flex flex-justify-center mt-2">
|
<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.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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</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`] = `
|
exports[`Setup app > GET /probot/import > renders importView 1`] = `
|
||||||
Array [
|
"<!DOCTYPE html>
|
||||||
Array [
|
<html lang=\\"en\\" class=\\"height-full\\" data-color-mode=\\"auto\\" data-light-theme=\\"light\\" data-dark-theme=\\"dark\\">
|
||||||
Object {
|
|
||||||
|
<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",
|
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
|
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
"APP_ID": "id",
|
"APP_ID": "id",
|
||||||
"GITHUB_CLIENT_ID": "Iv1.8a61f9b3a7aba766",
|
"GITHUB_CLIENT_ID": "Iv1.8a61f9b3a7aba766",
|
||||||
"GITHUB_CLIENT_SECRET": "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
"GITHUB_CLIENT_SECRET": "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
||||||
|
@ -19,15 +113,55 @@ Array [
|
||||||
]
|
]
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`Setup app POST /probot/import updates .env 1`] = `
|
exports[`Setup app > GET /probot/setup > throws a 400 Error if code is an empty string 1`] = `"code missing or invalid"`;
|
||||||
Array [
|
|
||||||
Array [
|
exports[`Setup app > GET /probot/setup > throws a 400 Error if code is not provided 1`] = `"code missing or invalid"`;
|
||||||
Object {
|
|
||||||
|
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",
|
"WEBHOOK_PROXY_URL": "mocked proxy URL",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
Array [
|
[
|
||||||
Object {
|
{
|
||||||
"APP_ID": "foo",
|
"APP_ID": "foo",
|
||||||
"PRIVATE_KEY": "\\"bar\\"",
|
"PRIVATE_KEY": "\\"bar\\"",
|
||||||
"WEBHOOK_SECRET": "baz",
|
"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 request from "supertest";
|
||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { Probot, Server } from "../../src";
|
import { Probot, Server } from "../../src/index.js";
|
||||||
import { defaultApp } from "../../src/apps/default";
|
import { defaultApp } from "../../src/apps/default.js";
|
||||||
|
|
||||||
describe("default app", () => {
|
describe("default app", () => {
|
||||||
let server: Server;
|
let output = [];
|
||||||
let output: any;
|
|
||||||
|
|
||||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||||
streamLogsToOutput._write = (object, encoding, done) => {
|
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||||
output.push(JSON.parse(object));
|
output.push(JSON.parse(object));
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(async () => {
|
async function instantiateServer(cwd = process.cwd()) {
|
||||||
output = [];
|
output = [];
|
||||||
server = new Server({
|
const server = new Server({
|
||||||
Probot: Probot.defaults({
|
Probot: Probot.defaults({
|
||||||
appId: 1,
|
appId: 1,
|
||||||
privateKey: "private key",
|
privateKey: "private key",
|
||||||
}),
|
}),
|
||||||
log: pino(streamLogsToOutput),
|
log: pino(streamLogsToOutput),
|
||||||
|
cwd,
|
||||||
});
|
});
|
||||||
|
|
||||||
await server.load(defaultApp);
|
await server.load(defaultApp);
|
||||||
});
|
return server;
|
||||||
|
}
|
||||||
|
|
||||||
describe("GET /probot", () => {
|
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);
|
return request(server.expressApp).get("/probot").expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("get info from package.json", () => {
|
describe("get info from package.json", () => {
|
||||||
let cwd: string;
|
|
||||||
beforeEach(() => {
|
|
||||||
cwd = process.cwd();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("returns the correct HTML with values", async () => {
|
it("returns the correct HTML with values", async () => {
|
||||||
|
const server = await instantiateServer();
|
||||||
const actual = await request(server.expressApp)
|
const actual = await request(server.expressApp)
|
||||||
.get("/probot")
|
.get("/probot")
|
||||||
.expect(200);
|
.expect(200);
|
||||||
expect(actual.text).toMatch("Welcome to probot");
|
expect(actual.text).toMatch("Welcome to probot");
|
||||||
expect(actual.text).toMatch("A framework for building GitHub Apps");
|
expect(actual.text).toMatch("A framework for building GitHub Apps");
|
||||||
expect(actual.text).toMatch(/v\d+\.\d+\.\d+/);
|
expect(actual.text).toMatch(/v\d+\.\d+\.\d+/);
|
||||||
|
expect(actual.text).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns the correct HTML without values", async () => {
|
it("returns the correct HTML without values", async () => {
|
||||||
process.chdir(__dirname);
|
const server = await instantiateServer(__dirname);
|
||||||
const actual = await request(server.expressApp)
|
const actual = await request(server.expressApp)
|
||||||
.get("/probot")
|
.get("/probot")
|
||||||
.expect(200);
|
.expect(200);
|
||||||
expect(actual.text).toMatch("Welcome to your Probot App");
|
expect(actual.text).toMatch("Welcome to your Probot App");
|
||||||
});
|
expect(actual.text).toMatchSnapshot();
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
process.chdir(cwd);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Redirect does not work because webhooks middleware is using root path
|
||||||
describe("GET /", () => {
|
describe("GET /", () => {
|
||||||
it("redirects to /probot", () => {
|
it("redirects to /probot", async () => {
|
||||||
return request(server.expressApp)
|
const server = await instantiateServer(__dirname);
|
||||||
|
await request(server.expressApp)
|
||||||
.get("/")
|
.get("/")
|
||||||
.expect(302)
|
.expect(302)
|
||||||
.expect("location", "/probot");
|
.expect("location", "/probot");
|
||||||
|
|
|
@ -1,15 +1,26 @@
|
||||||
const createChannel = jest.fn().mockResolvedValue("mocked proxy URL");
|
import { Stream } from "node:stream";
|
||||||
const updateDotenv = jest.fn().mockResolvedValue({});
|
|
||||||
jest.mock("smee-client", () => ({ createChannel }));
|
|
||||||
jest.mock("update-dotenv", () => updateDotenv);
|
|
||||||
|
|
||||||
import nock from "nock";
|
import fetchMock from "fetch-mock";
|
||||||
import { Stream } from "stream";
|
import { pino } from "pino";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import pino from "pino";
|
import { beforeEach, afterEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
import { Probot, Server } from "../../src";
|
import { Probot, Server } from "../../src/index.js";
|
||||||
import { setupAppFactory } from "../../src/apps/setup";
|
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", () => {
|
describe("Setup app", () => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
|
@ -37,7 +48,7 @@ describe("Setup app", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("logs", () => {
|
describe("logs", () => {
|
||||||
|
@ -91,31 +102,98 @@ describe("Setup app", () => {
|
||||||
|
|
||||||
describe("GET /probot/setup", () => {
|
describe("GET /probot/setup", () => {
|
||||||
it("returns a redirect", async () => {
|
it("returns a redirect", async () => {
|
||||||
nock("https://api.github.com")
|
const fetch = fetchMock
|
||||||
.post("/app-manifests/123/conversions")
|
.sandbox()
|
||||||
.reply(201, {
|
.postOnce("https://api.github.com/app-manifests/123/conversions", {
|
||||||
html_url: "/apps/my-app",
|
status: 201,
|
||||||
id: "id",
|
body: {
|
||||||
pem: "pem",
|
html_url: "/apps/my-app",
|
||||||
webhook_secret: "webhook_secret",
|
id: "id",
|
||||||
client_id: "Iv1.8a61f9b3a7aba766",
|
pem: "pem",
|
||||||
client_secret: "1726be1638095a19edd134c77bde3aa2ece1e5d8",
|
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")
|
.get("/probot/setup")
|
||||||
.query({ code: "123" })
|
.query({ code: "123" })
|
||||||
.expect(302)
|
.expect(302)
|
||||||
.expect("Location", "/apps/my-app/installations/new");
|
.expect("Location", "/apps/my-app/installations/new");
|
||||||
|
|
||||||
expect(createChannel).toHaveBeenCalledTimes(1);
|
expect(setupResponse.text).toMatchSnapshot();
|
||||||
expect(updateDotenv.mock.calls).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", () => {
|
describe("GET /probot/import", () => {
|
||||||
it("renders import.handlebars", async () => {
|
it("renders importView", async () => {
|
||||||
await request(server.expressApp).get("/probot/import").expect(200);
|
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(200)
|
||||||
.expect("");
|
.expect("");
|
||||||
|
|
||||||
expect(updateDotenv.mock.calls).toMatchSnapshot();
|
expect(mocks.updateDotenv.mock.calls).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("400 when keys are missing", async () => {
|
it("400 when keys are missing", async () => {
|
||||||
|
@ -144,19 +222,25 @@ describe("Setup app", () => {
|
||||||
webhook_secret: "baz",
|
webhook_secret: "baz",
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(server.expressApp)
|
const importResponse = await request(server.expressApp)
|
||||||
.post("/probot/import")
|
.post("/probot/import")
|
||||||
.set("content-type", "application/json")
|
.set("content-type", "application/json")
|
||||||
.send(body)
|
.send(body)
|
||||||
.expect(400);
|
.expect(400);
|
||||||
|
|
||||||
|
expect(importResponse.text).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("GET /probot/success", () => {
|
describe("GET /probot/success", () => {
|
||||||
it("returns a 200 response", async () => {
|
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 fs from "node:fs";
|
||||||
import path = require("path");
|
import path from "node:path";
|
||||||
|
|
||||||
import { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
import type { EmitterWebhookEvent as WebhookEvent } from "@octokit/webhooks";
|
||||||
import WebhookExamples, { WebhookDefinition } from "@octokit/webhooks-examples";
|
import WebhookExamples from "@octokit/webhooks-examples";
|
||||||
import nock from "nock";
|
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 { Context } from "../src/index.js";
|
||||||
import { ProbotOctokit } from "../src/octokit/probot-octokit";
|
import { ProbotOctokit } from "../src/octokit/probot-octokit.js";
|
||||||
import { PushEvent } from "@octokit/webhooks-types";
|
import type { PushEvent } from "@octokit/webhooks-types";
|
||||||
|
|
||||||
const pushEventPayload = (
|
const pushEventPayload = (
|
||||||
WebhookExamples.filter(
|
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||||
(event) => event.name === "push"
|
(event) => event.name === "push",
|
||||||
)[0] as WebhookDefinition<"push">
|
)[0] as WebhookDefinition<"push">
|
||||||
).examples[0];
|
).examples[0];
|
||||||
const issuesEventPayload = (
|
const issuesEventPayload = (
|
||||||
WebhookExamples.filter(
|
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||||
(event) => event.name === "issues"
|
(event) => event.name === "issues",
|
||||||
)[0] as WebhookDefinition<"issues">
|
)[0] as WebhookDefinition<"issues">
|
||||||
).examples[0];
|
).examples[0];
|
||||||
const pullRequestEventPayload = (
|
const pullRequestEventPayload = (
|
||||||
WebhookExamples.filter(
|
(WebhookExamples as unknown as WebhookDefinition[]).filter(
|
||||||
(event) => event.name === "pull_request"
|
(event) => event.name === "pull_request",
|
||||||
)[0] as WebhookDefinition<"pull_request">
|
)[0] as WebhookDefinition<"pull_request">
|
||||||
).examples[0];
|
).examples[0] as WebhookEvent<"pull_request">["payload"];
|
||||||
|
|
||||||
describe("Context", () => {
|
describe("Context", () => {
|
||||||
let event: WebhookEvent<"push"> = {
|
let event: WebhookEvent<"push"> = {
|
||||||
|
@ -34,7 +36,7 @@ describe("Context", () => {
|
||||||
let context: Context<"push"> = new Context<"push">(
|
let context: Context<"push"> = new Context<"push">(
|
||||||
event,
|
event,
|
||||||
{} as any,
|
{} as any,
|
||||||
{} as any
|
{} as any,
|
||||||
);
|
);
|
||||||
|
|
||||||
it("inherits the payload", () => {
|
it("inherits the payload", () => {
|
||||||
|
@ -97,8 +99,8 @@ describe("Context", () => {
|
||||||
try {
|
try {
|
||||||
context.repo();
|
context.repo();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(e.message).toMatch(
|
expect((e as Error).message).toMatch(
|
||||||
"context.repo() is not supported for this webhook event."
|
"context.repo() is not supported for this webhook event.",
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -179,15 +181,15 @@ describe("Context", () => {
|
||||||
owner: "muahaha",
|
owner: "muahaha",
|
||||||
pull_number: 5,
|
pull_number: 5,
|
||||||
repo: "Hello-World",
|
repo: "Hello-World",
|
||||||
}
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("config", () => {
|
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);
|
const configPath = path.join(__dirname, "fixtures", "config", fileName);
|
||||||
return fs.readFileSync(configPath, { encoding: "utf8" });
|
return fs.readFileSync(configPath, { encoding: "utf8" });
|
||||||
}
|
}
|
||||||
|
@ -202,9 +204,21 @@ describe("Context", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("gets a valid configuration", async () => {
|
it("gets a valid configuration", async () => {
|
||||||
const mock = nock("https://api.github.com")
|
const fetch = fetchMock
|
||||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
.sandbox()
|
||||||
.reply(200, nockConfigResponseDataFile("basic.yml"));
|
.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");
|
const config = await context.config("test-file.yml");
|
||||||
expect(config).toEqual({
|
expect(config).toEqual({
|
||||||
|
@ -212,39 +226,67 @@ describe("Context", () => {
|
||||||
baz: 11,
|
baz: 11,
|
||||||
foo: 5,
|
foo: 5,
|
||||||
});
|
});
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns null when the file and base repository are missing", async () => {
|
it("returns null when the file and base repository are missing", async () => {
|
||||||
const mock = nock("https://api.github.com")
|
const NOT_FOUND_RESPONSE = {
|
||||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
status: 404,
|
||||||
.reply(404)
|
body: {
|
||||||
.get("/repos/Codertocat/.github/contents/.github%2Ftest-file.yml")
|
message: "Not Found",
|
||||||
.reply(404);
|
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(await context.config("test-file.yml")).toBe(null);
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("accepts deepmerge options", async () => {
|
it("accepts deepmerge options", async () => {
|
||||||
const mock = nock("https://api.github.com")
|
const fetch = fetchMock
|
||||||
.get("/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml")
|
.sandbox()
|
||||||
.reply(
|
.getOnce(
|
||||||
200,
|
"https://api.github.com/repos/Codertocat/Hello-World/contents/.github%2Ftest-file.yml",
|
||||||
"foo:\n - name: master\n shouldChange: changed\n_extends: .github"
|
"foo:\n - name: master\n shouldChange: changed\n_extends: .github",
|
||||||
)
|
)
|
||||||
.get("/repos/Codertocat/.github/contents/.github%2Ftest-file.yml")
|
.getOnce(
|
||||||
.reply(
|
"https://api.github.com/repos/Codertocat/.github/contents/.github%2Ftest-file.yml",
|
||||||
200,
|
"foo:\n - name: develop\n - name: master\n shouldChange: should",
|
||||||
"foo:\n - name: develop\n - name: master\n shouldChange: should"
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const customMerge = jest.fn(
|
const octokit = new ProbotOctokit({
|
||||||
(_target: any[], _source: any[], _options: any): any[] => []
|
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 });
|
await context.config("test-file.yml", {}, { arrayMerge: customMerge });
|
||||||
expect(customMerge).toHaveBeenCalled();
|
expect(customMerge).toHaveBeenCalled();
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,31 +1,56 @@
|
||||||
import { createServer, IncomingMessage, ServerResponse } from "http";
|
import { createServer, IncomingMessage, ServerResponse } from "node:http";
|
||||||
import Stream from "stream";
|
import Stream from "node:stream";
|
||||||
|
|
||||||
import pino from "pino";
|
import { pino } from "pino";
|
||||||
import getPort from "get-port";
|
import getPort from "get-port";
|
||||||
import got from "got";
|
|
||||||
import { sign } from "@octokit/webhooks-methods";
|
import { sign } from "@octokit/webhooks-methods";
|
||||||
|
import { describe, expect, test, beforeEach } from "vitest";
|
||||||
|
|
||||||
import { createNodeMiddleware, createProbot, Probot } from "../src";
|
import { createNodeMiddleware, createProbot, Probot } from "../src/index.js";
|
||||||
import { ApplicationFunction } from "../src/types";
|
import type { ApplicationFunction } from "../src/types.js";
|
||||||
|
import WebhookExamples, {
|
||||||
|
type WebhookDefinition,
|
||||||
|
} from "@octokit/webhooks-examples";
|
||||||
|
|
||||||
const APP_ID = "1";
|
const APP_ID = "1";
|
||||||
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
const PRIVATE_KEY = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
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-----`;
|
-----END RSA PRIVATE KEY-----`;
|
||||||
const WEBHOOK_SECRET = "secret";
|
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", () => {
|
describe("createNodeMiddleware", () => {
|
||||||
let output: any[];
|
let output: any[];
|
||||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||||
streamLogsToOutput._write = (object, encoding, done) => {
|
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||||
output.push(JSON.parse(object));
|
output.push(JSON.parse(object));
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
@ -61,7 +86,8 @@ describe("createNodeMiddleware", () => {
|
||||||
|
|
||||||
const body = JSON.stringify(pushEvent);
|
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: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-github-event": "push",
|
"x-github-event": "push",
|
||||||
|
@ -76,6 +102,94 @@ describe("createNodeMiddleware", () => {
|
||||||
server.close();
|
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 () => {
|
test("loads app only once", async () => {
|
||||||
let counter = 0;
|
let counter = 0;
|
||||||
const appFn = () => {
|
const appFn = () => {
|
||||||
|
@ -90,11 +204,11 @@ describe("createNodeMiddleware", () => {
|
||||||
|
|
||||||
middleware(
|
middleware(
|
||||||
{} as IncomingMessage,
|
{} as IncomingMessage,
|
||||||
{ end() {}, writeHead() {} } as unknown as ServerResponse
|
{ end() {}, writeHead() {} } as unknown as ServerResponse,
|
||||||
);
|
);
|
||||||
middleware(
|
middleware(
|
||||||
{} as IncomingMessage,
|
{} as IncomingMessage,
|
||||||
{ end() {}, writeHead() {} } as unknown as ServerResponse
|
{ end() {}, writeHead() {} } as unknown as ServerResponse,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(counter).toEqual(1);
|
expect(counter).toEqual(1);
|
||||||
|
|
|
@ -1,20 +1,62 @@
|
||||||
import { createProbot, Probot } from "../src";
|
import { createProbot, Probot } from "../src/index.js";
|
||||||
import { captureLogOutput } from "./helpers/capture-log-output";
|
import { captureLogOutput } from "./helpers/capture-log-output.js";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
const env = {
|
const env = {
|
||||||
APP_ID: "1",
|
APP_ID: "1",
|
||||||
PRIVATE_KEY: `-----BEGIN RSA PRIVATE KEY-----
|
PRIVATE_KEY: `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
MIIJKAIBAAKCAgEAu0E+tR6wfOAJZ4lASzRUmvorCgbI5nQyvZl3WLu6ko2pcEnq
|
||||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
1t1/W/Yaovt9W8eMFVfoFXKhsHOAM5dFlktxOlcaUQiRYSO7fBbZYVNYoawnCRqD
|
||||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
HKQ1oKC6B23EKfW5NH8NLaI/+QJFG7fpr0P4HkHghLsOe7rIUDt7EjRsSSRhM2+Y
|
||||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
sFmRsnj0PWESWyI5exdKys0Mw25CmGsA27ltmebgHFYQ4ac+z0Esbjujcxec5wtn
|
||||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
oAMT6jIBjEPHYTy0Cbe/wDN0cZkg6QyNC09lMnUx8vP1gwAVP20VXfjdFHZ8cR80
|
||||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
ungLmBG0SWgVglqv52C5Gad2hEDsWyi28/XZ9/mNGatZJ1SSmA6+TSuSlrs/Dm0K
|
||||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
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-----`,
|
-----END RSA PRIVATE KEY-----`,
|
||||||
WEBHOOK_SECRET: "secret",
|
WEBHOOK_SECRET: "secret",
|
||||||
};
|
};
|
||||||
// tslint:disable:no-empty
|
|
||||||
describe("createProbot", () => {
|
describe("createProbot", () => {
|
||||||
test("createProbot()", () => {
|
test("createProbot()", () => {
|
||||||
const probot = createProbot({ env });
|
const probot = createProbot({ env });
|
||||||
|
@ -41,6 +83,20 @@ describe("createProbot", () => {
|
||||||
expect(probot.log.level).toEqual("trace");
|
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", () => {
|
test("env, overrides", () => {
|
||||||
const probot = createProbot({
|
const probot = createProbot({
|
||||||
env: {
|
env: {
|
||||||
|
@ -65,36 +121,34 @@ describe("createProbot", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("env, logger message key", async () => {
|
test("env, logger message key", async () => {
|
||||||
const outputData = await captureLogOutput(() => {
|
const probot = createProbot({
|
||||||
const probot = createProbot({
|
env: {
|
||||||
env: {
|
...env,
|
||||||
...env,
|
LOG_LEVEL: "info",
|
||||||
LOG_LEVEL: "info",
|
LOG_FORMAT: "json",
|
||||||
LOG_FORMAT: "json",
|
LOG_MESSAGE_KEY: "myMessage",
|
||||||
LOG_MESSAGE_KEY: "myMessage",
|
},
|
||||||
},
|
defaults: { logLevel: "trace" },
|
||||||
defaults: { logLevel: "trace" },
|
|
||||||
});
|
|
||||||
|
|
||||||
probot.log.info("Ciao");
|
|
||||||
});
|
});
|
||||||
|
const outputData = await captureLogOutput(() => {
|
||||||
|
probot.log.info("Ciao");
|
||||||
|
}, probot.log);
|
||||||
expect(JSON.parse(outputData).myMessage).toEqual("Ciao");
|
expect(JSON.parse(outputData).myMessage).toEqual("Ciao");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("env, octokit logger set", async () => {
|
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 outputData = await captureLogOutput(async () => {
|
||||||
const probot = createProbot({
|
|
||||||
env: {
|
|
||||||
...env,
|
|
||||||
LOG_LEVEL: "info",
|
|
||||||
LOG_FORMAT: "json",
|
|
||||||
LOG_MESSAGE_KEY: "myMessage",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const octokit = await probot.auth();
|
const octokit = await probot.auth();
|
||||||
octokit.log.info("Ciao");
|
octokit.log.info("Ciao");
|
||||||
});
|
}, probot.log);
|
||||||
expect(JSON.parse(outputData)).toMatchObject({
|
expect(JSON.parse(outputData)).toMatchObject({
|
||||||
myMessage: "Ciao",
|
myMessage: "Ciao",
|
||||||
name: "octokit",
|
name: "octokit",
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
import { describe, it } from "vitest";
|
||||||
|
|
||||||
describe("Deprecations", () => {
|
describe("Deprecations", () => {
|
||||||
it("no deprecations exists at this point", () => {});
|
it("no deprecations exists at this point", () => {});
|
||||||
});
|
});
|
||||||
|
|
|
@ -2,22 +2,20 @@ import execa from "execa";
|
||||||
import getPort from "get-port";
|
import getPort from "get-port";
|
||||||
|
|
||||||
import { sign } from "@octokit/webhooks-methods";
|
import { sign } from "@octokit/webhooks-methods";
|
||||||
import bodyParser from "body-parser";
|
|
||||||
import express from "express";
|
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.
|
* 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.
|
* 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
|
* to verify they are what we expect
|
||||||
*/
|
*/
|
||||||
describe("end-to-end-tests", () => {
|
describe("end-to-end-tests", () => {
|
||||||
let server: any;
|
let server: any;
|
||||||
let probotProcess: any;
|
let probotProcess: execa.ExecaChildProcess<string> | null;
|
||||||
let probotPort: number;
|
let probotPort: number;
|
||||||
let mockServerPort: number;
|
let mockServerPort: number;
|
||||||
|
|
||||||
|
@ -34,7 +32,7 @@ describe("end-to-end-tests", () => {
|
||||||
|
|
||||||
it("hello-world app", async () => {
|
it("hello-world app", async () => {
|
||||||
const app = express();
|
const app = express();
|
||||||
const httpMock = jest
|
const httpMock = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockImplementationOnce((req, res) => {
|
.mockImplementationOnce((req, res) => {
|
||||||
expect(req.method).toEqual("POST");
|
expect(req.method).toEqual("POST");
|
||||||
|
@ -52,37 +50,39 @@ describe("end-to-end-tests", () => {
|
||||||
.mockImplementationOnce((req, res) => {
|
.mockImplementationOnce((req, res) => {
|
||||||
expect(req.method).toEqual("POST");
|
expect(req.method).toEqual("POST");
|
||||||
expect(req.path).toEqual(
|
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!" });
|
expect(req.body).toStrictEqual({ body: "Hello World!" });
|
||||||
|
|
||||||
res.status(201).json({});
|
res.status(201).json({});
|
||||||
});
|
});
|
||||||
|
|
||||||
// tslint:disable-next-line
|
app.use(express.json());
|
||||||
app.use(bodyParser.json());
|
|
||||||
app.use("/api/v3", httpMock);
|
app.use("/api/v3", httpMock);
|
||||||
server = app.listen(mockServerPort);
|
server = app.listen(mockServerPort);
|
||||||
|
|
||||||
probotProcess = execa(
|
probotProcess = execa(
|
||||||
"bin/probot.js",
|
"node",
|
||||||
["run", "./test/e2e/hello-world.js"],
|
["bin/probot.js", "run", "./test/e2e/hello-world.cjs"],
|
||||||
{
|
{
|
||||||
env: {
|
env: {
|
||||||
APP_ID: "1",
|
APP_ID: "1",
|
||||||
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-----`,
|
||||||
"-----BEGIN RSA PRIVATE KEY-----\nMIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY\nFl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo\n/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY\nwQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv\nA1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq\nNKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U\nr1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=\n-----END RSA PRIVATE KEY-----",
|
|
||||||
WEBHOOK_SECRET: "test",
|
WEBHOOK_SECRET: "test",
|
||||||
PORT: String(probotPort),
|
PORT: String(probotPort),
|
||||||
GHE_HOST: `127.0.0.1:${mockServerPort}`,
|
GHE_HOST: `127.0.0.1:${mockServerPort}`,
|
||||||
GHE_PROTOCOL: "http",
|
GHE_PROTOCOL: "http",
|
||||||
LOG_LEVEL: "trace",
|
LOG_LEVEL: "trace",
|
||||||
|
WEBHOOK_PATH: "/",
|
||||||
},
|
},
|
||||||
}
|
stdio: "inherit",
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
// give probot a moment to start
|
// give probot a moment to start
|
||||||
await new Promise((resolve) => setTimeout(resolve, 3000));
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
||||||
|
// the probot process should be successfully started
|
||||||
|
expect(probotProcess.exitCode).toBeNull();
|
||||||
|
|
||||||
// send webhook event request
|
// send webhook event request
|
||||||
const body = JSON.stringify({
|
const body = JSON.stringify({
|
||||||
|
@ -102,7 +102,8 @@ describe("end-to-end-tests", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await got.post(`http://127.0.0.1:${probotPort}`, {
|
await fetch(`http://127.0.0.1:${probotPort}/`, {
|
||||||
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
"content-type": "application/json",
|
"content-type": "application/json",
|
||||||
"x-github-event": "issues",
|
"x-github-event": "issues",
|
||||||
|
@ -113,7 +114,9 @@ describe("end-to-end-tests", () => {
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
probotProcess.cancel();
|
probotProcess.cancel();
|
||||||
console.log((await probotProcess).stdout);
|
const awaitedProcess = await probotProcess;
|
||||||
|
console.log(awaitedProcess.stdout);
|
||||||
|
console.log(awaitedProcess.stderr);
|
||||||
}
|
}
|
||||||
|
|
||||||
expect(httpMock).toHaveBeenCalledTimes(2);
|
expect(httpMock).toHaveBeenCalledTimes(2);
|
||||||
|
|
|
@ -13,9 +13,7 @@ module.exports = (app) => {
|
||||||
const params = context.issue({ body: "Hello World!" });
|
const params = context.issue({ body: "Hello World!" });
|
||||||
|
|
||||||
// Post a comment on the issue
|
// Post a comment on the issue
|
||||||
await context.octokit.issues.createComment(params).then(
|
await context.octokit.issues.createComment(params);
|
||||||
() => console.log("issue comment created"),
|
console.log("issue comment created");
|
||||||
(error) => console.log("not ok", error)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
|
@ -0,0 +1 @@
|
||||||
|
{
|
|
@ -0,0 +1 @@
|
||||||
|
"null"
|
|
@ -1,2 +1 @@
|
||||||
// tslint:disable-next-line:no-empty
|
export default () => {};
|
||||||
export = () => {};
|
|
||||||
|
|
|
@ -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 = "";
|
let outputData = "";
|
||||||
|
|
||||||
const sbWrite = SonicBoom.prototype.write;
|
const stdoutSpy: SpyInstance = vi.spyOn(
|
||||||
SonicBoom.prototype.write = function (data) {
|
// @ts-expect-error
|
||||||
|
log[pinoSymbols.streamSym],
|
||||||
|
"write",
|
||||||
|
);
|
||||||
|
stdoutSpy.mockImplementation((data) => {
|
||||||
outputData += data;
|
outputData += data;
|
||||||
};
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await action();
|
await action();
|
||||||
|
|
||||||
return outputData;
|
return outputData;
|
||||||
} finally {
|
} 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", () => {
|
describe("isProduction", () => {
|
||||||
it("returns true if the NODE_ENV is set to production", () => {
|
it("returns true if the NODE_ENV is set to production", () => {
|
||||||
|
@ -11,6 +12,6 @@ describe("isProduction", () => {
|
||||||
(value) => {
|
(value) => {
|
||||||
process.env.NODE_ENV = value;
|
process.env.NODE_ENV = value;
|
||||||
expect(isProduction()).toBe(false);
|
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 fs from "node:fs";
|
||||||
import nock from "nock";
|
import path from "node:path";
|
||||||
import pkg from "../package.json";
|
import fetchMock from "fetch-mock";
|
||||||
import { ManifestCreation } from "../src/manifest-creation";
|
import { describe, expect, test, beforeEach, afterEach, vi } from "vitest";
|
||||||
import response from "./fixtures/setup/response.json";
|
|
||||||
|
import { ManifestCreation } from "../src/manifest-creation.js";
|
||||||
|
import { loadPackageJson } from "../src/helpers/load-package-json.js";
|
||||||
|
|
||||||
describe("ManifestCreation", () => {
|
describe("ManifestCreation", () => {
|
||||||
let setup: ManifestCreation;
|
let setup: ManifestCreation;
|
||||||
|
|
||||||
|
const pkg = loadPackageJson();
|
||||||
|
const response = JSON.parse(
|
||||||
|
fs.readFileSync(
|
||||||
|
path.resolve(process.cwd(), "./test/fixtures/setup/response.json"),
|
||||||
|
"utf8",
|
||||||
|
),
|
||||||
|
);
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setup = new ManifestCreation();
|
setup = new ManifestCreation();
|
||||||
});
|
});
|
||||||
|
@ -17,10 +26,10 @@ describe("ManifestCreation", () => {
|
||||||
delete process.env.PROJECT_DOMAIN;
|
delete process.env.PROJECT_DOMAIN;
|
||||||
delete process.env.WEBHOOK_PROXY_URL;
|
delete process.env.WEBHOOK_PROXY_URL;
|
||||||
|
|
||||||
setup.updateEnv = jest.fn();
|
setup.updateEnv = vi.fn();
|
||||||
|
|
||||||
const SmeeClient: typeof import("smee-client") = require("smee-client");
|
const SmeeClient: typeof import("smee-client") = require("smee-client");
|
||||||
SmeeClient.createChannel = jest
|
SmeeClient.createChannel = vi
|
||||||
.fn()
|
.fn()
|
||||||
.mockReturnValue("https://smee.io/1234abc");
|
.mockReturnValue("https://smee.io/1234abc");
|
||||||
});
|
});
|
||||||
|
@ -52,21 +61,21 @@ describe("ManifestCreation", () => {
|
||||||
|
|
||||||
test("creates an app url", () => {
|
test("creates an app url", () => {
|
||||||
expect(setup.createAppUrl).toEqual(
|
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", () => {
|
test("creates an app url when github org is set", () => {
|
||||||
process.env.GH_ORG = "testorg";
|
process.env.GH_ORG = "testorg";
|
||||||
expect(setup.createAppUrl).toEqual(
|
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", () => {
|
test("creates an app url when github host env is set", () => {
|
||||||
process.env.GHE_HOST = "hiimbex.github.com";
|
process.env.GHE_HOST = "hiimbex.github.com";
|
||||||
expect(setup.createAppUrl).toEqual(
|
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.GHE_HOST = "hiimbex.github.com";
|
||||||
process.env.GH_ORG = "testorg";
|
process.env.GH_ORG = "testorg";
|
||||||
expect(setup.createAppUrl).toEqual(
|
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_HOST = "hiimbex.github.com";
|
||||||
process.env.GHE_PROTOCOL = "http";
|
process.env.GHE_PROTOCOL = "http";
|
||||||
expect(setup.createAppUrl).toEqual(
|
expect(setup.createAppUrl).toEqual(
|
||||||
"http://hiimbex.github.com/settings/apps/new"
|
"http://hiimbex.github.com/settings/apps/new",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createAppFromCode", () => {
|
describe("createAppFromCode", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
setup.updateEnv = jest.fn();
|
setup.updateEnv = vi.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
|
@ -100,11 +109,18 @@ describe("ManifestCreation", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creates an app from a code", async () => {
|
test("creates an app from a code", async () => {
|
||||||
nock("https://api.github.com")
|
const fetch = fetchMock
|
||||||
.post("/app-manifests/123abc/conversions")
|
.sandbox()
|
||||||
.reply(200, response);
|
.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(createdApp).toEqual("https://github.com/apps/testerino0000000");
|
||||||
// expect dotenv to be called with id, webhook_secret, pem
|
// expect dotenv to be called with id, webhook_secret, pem
|
||||||
expect(setup.updateEnv).toHaveBeenCalledWith({
|
expect(setup.updateEnv).toHaveBeenCalledWith({
|
||||||
|
@ -118,11 +134,21 @@ describe("ManifestCreation", () => {
|
||||||
test("creates an app from a code when github host env is set", async () => {
|
test("creates an app from a code when github host env is set", async () => {
|
||||||
process.env.GHE_HOST = "swinton.github.com";
|
process.env.GHE_HOST = "swinton.github.com";
|
||||||
|
|
||||||
nock("https://swinton.github.com")
|
const fetch = fetchMock
|
||||||
.post("/api/v3/app-manifests/123abc/conversions")
|
.sandbox()
|
||||||
.reply(200, response);
|
.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(createdApp).toEqual("https://github.com/apps/testerino0000000");
|
||||||
// expect dotenv to be called with id, webhook_secret, pem
|
// expect dotenv to be called with id, webhook_secret, pem
|
||||||
expect(setup.updateEnv).toHaveBeenCalledWith({
|
expect(setup.updateEnv).toHaveBeenCalledWith({
|
||||||
|
@ -136,24 +162,24 @@ describe("ManifestCreation", () => {
|
||||||
|
|
||||||
describe("getManifest", () => {
|
describe("getManifest", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("creates an app from a code", () => {
|
test("creates an app from a code", () => {
|
||||||
// checks that getManifest returns a JSON.stringified manifest
|
// checks that getManifest returns a JSON.stringified manifest
|
||||||
expect(setup.getManifest(pkg, "localhost://3000")).toEqual(
|
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", () => {
|
test("creates an app from a code with overrided values from app.yml", () => {
|
||||||
const appYaml =
|
const appYaml =
|
||||||
"name: cool-app\ndescription: A description for a cool app";
|
"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
|
// checks that getManifest returns the correct JSON.stringified manifest
|
||||||
expect(setup.getManifest(pkg, "localhost://3000")).toEqual(
|
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 fetchMock from "fetch-mock";
|
||||||
import { ProbotOctokit } from "../src/octokit/probot-octokit";
|
import { ProbotOctokit } from "../src/octokit/probot-octokit.js";
|
||||||
|
import type { RequestError } from "@octokit/types";
|
||||||
type Options = ConstructorParameters<typeof ProbotOctokit>[0];
|
import type { OctokitOptions } from "../src/types.js";
|
||||||
|
import { describe, expect, test, vi, it } from "vitest";
|
||||||
|
|
||||||
describe("ProbotOctokit", () => {
|
describe("ProbotOctokit", () => {
|
||||||
let octokit: InstanceType<typeof ProbotOctokit>;
|
const defaultOptions: OctokitOptions = {
|
||||||
|
|
||||||
const defaultOptions: Options = {
|
|
||||||
retry: {
|
retry: {
|
||||||
// disable retries to test error states
|
// disable retries to test error states
|
||||||
enabled: false,
|
enabled: false,
|
||||||
|
@ -17,177 +16,455 @@ describe("ProbotOctokit", () => {
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
octokit = new ProbotOctokit(defaultOptions);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("works without options", async () => {
|
test("works without options", async () => {
|
||||||
octokit = new ProbotOctokit();
|
const fetch = fetchMock
|
||||||
const user = { login: "ohai" };
|
.sandbox()
|
||||||
|
.getOnce("https://api.github.com/user", '{"login": "ohai"}');
|
||||||
|
|
||||||
nock("https://api.github.com").get("/user").reply(200, user);
|
const octokit = new ProbotOctokit({
|
||||||
expect((await octokit.users.getAuthenticated({})).data).toEqual(user);
|
...defaultOptions,
|
||||||
|
request: {
|
||||||
|
fetch,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect((await octokit.users.getAuthenticated({})).data).toEqual(
|
||||||
|
'{"login": "ohai"}',
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("logs request errors", async () => {
|
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 {
|
try {
|
||||||
await octokit.request("/");
|
await octokit.request("/");
|
||||||
throw new Error("should throw");
|
throw new Error("should throw");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
expect(error.status).toBe(500);
|
expect((error as RequestError).status).toBe(500);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with retry enabled", () => {
|
test("with retry enabled retries failed requests", async () => {
|
||||||
beforeEach(() => {
|
let callCount = 0;
|
||||||
const options: Options = {
|
|
||||||
...defaultOptions,
|
|
||||||
retry: {
|
|
||||||
enabled: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
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 () => {
|
if (callCount++ === 0) {
|
||||||
nock("https://api.github.com").get("/").once().reply(500, {});
|
return Promise.reject({});
|
||||||
|
|
||||||
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}`,
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
|
||||||
.get("/")
|
return Promise.resolve({
|
||||||
.reply(200, {});
|
status: 200,
|
||||||
|
headers: new Headers(),
|
||||||
const { status } = await octokit.request("/");
|
text: () => Promise.resolve("{}"),
|
||||||
expect(status).toBe(200);
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
test("retries requests when hitting the abuse limiter", async () => {
|
const response = await octokit.request("/");
|
||||||
nock("https://api.github.com").get("/").once().reply(403, {
|
expect(response.status).toBe(200);
|
||||||
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);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("paginate", () => {
|
test("with throttling enabled retries requests when being rate limited", async () => {
|
||||||
// Prepare an array of issue objects
|
let callCount = 0;
|
||||||
const issues = new Array(5).fill(0).map((_, i, arr) => {
|
|
||||||
return {
|
const octokit = new ProbotOctokit({
|
||||||
id: i,
|
...defaultOptions,
|
||||||
number: i,
|
throttle: {
|
||||||
title: `Issue number ${i}`,
|
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(() => {
|
const { status } = await octokit.request("/");
|
||||||
nock("https://api.github.com")
|
expect(status).toBe(200);
|
||||||
.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"',
|
test("with throttling enabled using default onPrimaryRateLimit", async () => {
|
||||||
})
|
expect.assertions(14);
|
||||||
.get("/repositories/123/issues?per_page=1&page=2")
|
let callCount = 0;
|
||||||
.reply(200, [issues[1]], {
|
|
||||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=3>; rel="next"',
|
const octokit = new ProbotOctokit({
|
||||||
})
|
...defaultOptions,
|
||||||
.get("/repositories/123/issues?per_page=1&page=3")
|
// @ts-expect-error just need to mock the warn method
|
||||||
.reply(200, [issues[2]], {
|
log: {
|
||||||
link: '<https://api.github.com/repositories/123/issues?per_page=1&page=4>; rel="next"',
|
warn(message) {
|
||||||
})
|
expect(message).toEqual(
|
||||||
.get("/repositories/123/issues?per_page=1&page=4")
|
'Rate limit hit with "GET /", retrying in 1 seconds.',
|
||||||
.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")
|
// @ts-expect-error
|
||||||
.reply(200, [issues[4]], {
|
throttle: {
|
||||||
link: "",
|
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 { status } = await octokit.request("/");
|
||||||
const spy = jest.fn();
|
expect(status).toBe(200);
|
||||||
const res = await octokit.paginate(
|
});
|
||||||
octokit.issues.listForRepo.endpoint.merge({
|
|
||||||
owner: "JasonEtco",
|
test("with throttling enabled retries requests when hitting the secondary rate limiter", async () => {
|
||||||
repo: "pizza",
|
let callCount = 0;
|
||||||
per_page: 1,
|
|
||||||
}),
|
const octokit = new ProbotOctokit({
|
||||||
spy
|
...defaultOptions,
|
||||||
);
|
throttle: {
|
||||||
expect(Array.isArray(res)).toBeTruthy();
|
enabled: true,
|
||||||
expect(res.length).toBe(5);
|
fallbackSecondaryRateRetryAfter: 1,
|
||||||
expect(spy).toHaveBeenCalledTimes(5);
|
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 response = await octokit.request("/");
|
||||||
const spy = jest.fn((response, done) => {
|
expect(response.status).toBe(200);
|
||||||
if (response.data[0].id === 2) done();
|
});
|
||||||
}) as any;
|
|
||||||
const res = await octokit.paginate(
|
test("with throttling enabled using default onSecondaryRateLimit", async () => {
|
||||||
octokit.issues.listForRepo.endpoint.merge({
|
expect.assertions(14);
|
||||||
owner: "JasonEtco",
|
let callCount = 0;
|
||||||
repo: "pizza",
|
|
||||||
per_page: 1,
|
const octokit = new ProbotOctokit({
|
||||||
}),
|
...defaultOptions,
|
||||||
spy
|
// @ts-expect-error just need to mock the warn method
|
||||||
);
|
log: {
|
||||||
expect(res.length).toBe(3);
|
warn(message) {
|
||||||
expect(spy).toHaveBeenCalledTimes(3);
|
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 response = await octokit.request("/");
|
||||||
const res = await octokit.paginate(
|
expect(response.status).toBe(200);
|
||||||
octokit.issues.listForRepo.endpoint.merge({
|
});
|
||||||
owner: "JasonEtco",
|
|
||||||
repo: "pizza",
|
// Prepare an array of issue objects
|
||||||
per_page: 1,
|
const issues = new Array(5).fill(0).map((_, i) => {
|
||||||
})
|
return {
|
||||||
);
|
id: i,
|
||||||
expect(res).toEqual(issues);
|
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,
|
||||||
EmitterWebhookEvent as WebhookEvent,
|
EmitterWebhookEvent as WebhookEvent,
|
||||||
} from "@octokit/webhooks";
|
} from "@octokit/webhooks";
|
||||||
import Bottleneck from "bottleneck";
|
import Bottleneck from "bottleneck";
|
||||||
import nock from "nock";
|
import fetchMock from "fetch-mock";
|
||||||
import pino from "pino";
|
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 webhookExamples, {
|
||||||
import { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types";
|
type WebhookDefinition,
|
||||||
|
} from "@octokit/webhooks-examples";
|
||||||
|
import type { EmitterWebhookEventName } from "@octokit/webhooks/dist-types/types.js";
|
||||||
|
|
||||||
const appId = 1;
|
const appId = 1;
|
||||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
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-----`;
|
-----END RSA PRIVATE KEY-----`;
|
||||||
|
|
||||||
const getPayloadExamples = <TName extends EmitterWebhookEventName>(
|
const getPayloadExamples = <TName extends EmitterWebhookEventName>(
|
||||||
name: TName
|
name: TName,
|
||||||
) => {
|
) => {
|
||||||
return webhookExamples.filter((event) => event.name === name.split(".")[0])[0]
|
return (webhookExamples as unknown as WebhookDefinition[]).filter(
|
||||||
.examples as EmitterWebhookEvent<TName>["payload"][];
|
(event) => event.name === name.split(".")[0],
|
||||||
|
)[0].examples as EmitterWebhookEvent<TName>["payload"][];
|
||||||
};
|
};
|
||||||
const getPayloadExample = <TName extends EmitterWebhookEventName>(
|
const getPayloadExample = <TName extends EmitterWebhookEventName>(
|
||||||
name: TName
|
name: TName,
|
||||||
) => {
|
) => {
|
||||||
const examples = getPayloadExamples<TName>(name);
|
const examples = getPayloadExamples<TName>(name);
|
||||||
if (name.includes(".")) {
|
if (name.includes(".")) {
|
||||||
|
@ -43,7 +65,6 @@ const getPayloadExample = <TName extends EmitterWebhookEventName>(
|
||||||
}
|
}
|
||||||
return examples[0];
|
return examples[0];
|
||||||
};
|
};
|
||||||
// tslint:disable:no-empty
|
|
||||||
describe("Probot", () => {
|
describe("Probot", () => {
|
||||||
let probot: Probot;
|
let probot: Probot;
|
||||||
let event: WebhookEvent<
|
let event: WebhookEvent<
|
||||||
|
@ -52,7 +73,7 @@ describe("Probot", () => {
|
||||||
let output: any;
|
let output: any;
|
||||||
|
|
||||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||||
streamLogsToOutput._write = (object, encoding, done) => {
|
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||||
output.push(JSON.parse(object));
|
output.push(JSON.parse(object));
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
@ -69,22 +90,26 @@ describe("Probot", () => {
|
||||||
|
|
||||||
describe(".defaults()", () => {
|
describe(".defaults()", () => {
|
||||||
test("sets default options for constructor", async () => {
|
test("sets default options for constructor", async () => {
|
||||||
const mock = nock("https://api.github.com").get("/app").reply(200, {
|
const fetch = fetchMock.sandbox().getOnce("https://api.github.com/app", {
|
||||||
id: 1,
|
status: 200,
|
||||||
|
body: {
|
||||||
|
id: 1,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const MyProbot = Probot.defaults({ appId, privateKey });
|
const MyProbot = Probot.defaults({ appId, privateKey });
|
||||||
const probot = new MyProbot();
|
const probot = new MyProbot({
|
||||||
|
request: { fetch },
|
||||||
|
});
|
||||||
const octokit = await probot.auth();
|
const octokit = await probot.auth();
|
||||||
await octokit.apps.getAuthenticated();
|
await octokit.apps.getAuthenticated();
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("constructor", () => {
|
describe("constructor", () => {
|
||||||
it("no options", () => {
|
it("no options", () => {
|
||||||
expect(() => new Probot()).toThrow(
|
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)}`", () => {
|
it("shouldn't overwrite `options.throttle` passed to `{Octokit: ProbotOctokit.defaults(options)}`", () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
const MyOctokit = ProbotOctokit.plugin((octokit, options) => {
|
const MyOctokit = ProbotOctokit.plugin((_octokit, options) => {
|
||||||
expect(options.throttle.enabled).toEqual(false);
|
expect(options.throttle?.enabled).toEqual(true);
|
||||||
}).defaults({
|
}).defaults({
|
||||||
appId,
|
appId,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
@ -133,7 +158,7 @@ describe("Probot", () => {
|
||||||
it("responds with the correct error if webhook secret does not match", async () => {
|
it("responds with the correct error if webhook secret does not match", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
probot.log.error = jest.fn();
|
probot.log.error = vi.fn() as LogFn;
|
||||||
probot.webhooks.on("push", () => {
|
probot.webhooks.on("push", () => {
|
||||||
throw new Error("X-Hub-Signature-256 does not match blob signature");
|
throw new Error("X-Hub-Signature-256 does not match blob signature");
|
||||||
});
|
});
|
||||||
|
@ -141,16 +166,14 @@ describe("Probot", () => {
|
||||||
try {
|
try {
|
||||||
await probot.webhooks.receive(event);
|
await probot.webhooks.receive(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
|
||||||
).toMatchSnapshot();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("responds with the correct error if webhook secret is not found", async () => {
|
it("responds with the correct error if webhook secret is not found", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
probot.log.error = jest.fn();
|
probot.log.error = vi.fn() as LogFn;
|
||||||
probot.webhooks.on("push", () => {
|
probot.webhooks.on("push", () => {
|
||||||
throw new Error("No X-Hub-Signature-256 found on request");
|
throw new Error("No X-Hub-Signature-256 found on request");
|
||||||
});
|
});
|
||||||
|
@ -158,66 +181,58 @@ describe("Probot", () => {
|
||||||
try {
|
try {
|
||||||
await probot.webhooks.receive(event);
|
await probot.webhooks.receive(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
|
||||||
).toMatchSnapshot();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("responds with the correct error if webhook secret is wrong", async () => {
|
it("responds with the correct error if webhook secret is wrong", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
probot.log.error = jest.fn();
|
probot.log.error = vi.fn() as LogFn;
|
||||||
probot.webhooks.on("push", () => {
|
probot.webhooks.on("push", () => {
|
||||||
throw Error(
|
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 {
|
try {
|
||||||
await probot.webhooks.receive(event);
|
await probot.webhooks.receive(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
|
||||||
).toMatchSnapshot();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("responds with the correct error if the PEM file is missing", async () => {
|
it("responds with the correct error if the PEM file is missing", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
probot.log.error = jest.fn();
|
probot.log.error = vi.fn() as LogFn;
|
||||||
probot.webhooks.onAny(() => {
|
probot.webhooks.onAny(() => {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"error:0906D06C:PEM routines:PEM_read_bio:no start line"
|
"error:0906D06C:PEM routines:PEM_read_bio:no start line",
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await probot.webhooks.receive(event);
|
await probot.webhooks.receive(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
|
||||||
).toMatchSnapshot();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("responds with the correct error if the jwt could not be decoded", async () => {
|
it("responds with the correct error if the jwt could not be decoded", async () => {
|
||||||
expect.assertions(1);
|
expect.assertions(1);
|
||||||
|
|
||||||
probot.log.error = jest.fn();
|
probot.log.error = vi.fn() as LogFn;
|
||||||
probot.webhooks.onAny(() => {
|
probot.webhooks.onAny(() => {
|
||||||
throw new Error(
|
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 {
|
try {
|
||||||
await probot.webhooks.receive(event);
|
await probot.webhooks.receive(event);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
expect(
|
expect((probot.log.error as Mock).mock.calls[0][1]).toMatchSnapshot();
|
||||||
(probot.log.error as jest.Mock).mock.calls[0][1]
|
|
||||||
).toMatchSnapshot();
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -227,7 +242,7 @@ describe("Probot", () => {
|
||||||
const appFn = async (app: Probot) => {
|
const appFn = async (app: Probot) => {
|
||||||
const octokit = await app.auth();
|
const octokit = await app.auth();
|
||||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
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 appFn = async (app: Probot) => {
|
||||||
const octokit = await app.auth();
|
const octokit = await app.auth();
|
||||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
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 appFn = async (app: Probot) => {
|
||||||
const octokit = await app.auth();
|
const octokit = await app.auth();
|
||||||
expect(octokit.request.endpoint.DEFAULTS.baseUrl).toEqual(
|
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", () => {
|
describe.skipIf(process.env.REDIS_URL === undefined)(
|
||||||
it("sets throttle options", async () => {
|
"options.redisConfig as string",
|
||||||
expect.assertions(2);
|
() => {
|
||||||
|
it("sets throttle options", async () => {
|
||||||
|
expect.assertions(2);
|
||||||
|
|
||||||
probot = new Probot({
|
probot = new Probot({
|
||||||
githubToken: "faketoken",
|
githubToken: "faketoken",
|
||||||
redisConfig: "test",
|
redisConfig: process.env.REDIS_URL,
|
||||||
Octokit: ProbotOctokit.plugin((octokit, options) => {
|
Octokit: ProbotOctokit.plugin((_octokit, options) => {
|
||||||
expect(options.throttle.Bottleneck).toBe(Bottleneck);
|
expect(options.throttle?.Bottleneck).toBe(Bottleneck);
|
||||||
expect(options.throttle.connection).toBeInstanceOf(
|
expect(options.throttle?.connection).toBeInstanceOf(
|
||||||
Bottleneck.IORedisConnection
|
Bottleneck.IORedisConnection,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
describe.skip("redis configuration object", () => {
|
describe.skipIf(process.env.REDIS_URL === undefined)(
|
||||||
it("sets throttle options", async () => {
|
"redis configuration object",
|
||||||
expect.assertions(2);
|
() => {
|
||||||
const redisConfig = {
|
it("sets throttle options", async () => {
|
||||||
host: "test",
|
expect.assertions(2);
|
||||||
};
|
const redisConfig = {
|
||||||
|
host: process.env.REDIS_URL,
|
||||||
|
};
|
||||||
|
|
||||||
probot = new Probot({
|
probot = new Probot({
|
||||||
githubToken: "faketoken",
|
githubToken: "faketoken",
|
||||||
redisConfig,
|
redisConfig,
|
||||||
Octokit: ProbotOctokit.plugin((octokit, options) => {
|
Octokit: ProbotOctokit.plugin((_octokit, options) => {
|
||||||
expect(options.throttle.Bottleneck).toBe(Bottleneck);
|
expect(options.throttle?.Bottleneck).toBe(Bottleneck);
|
||||||
expect(options.throttle.connection).toBeInstanceOf(
|
expect(options.throttle?.connection).toBeInstanceOf(
|
||||||
Bottleneck.IORedisConnection
|
Bottleneck.IORedisConnection,
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
},
|
||||||
});
|
);
|
||||||
|
|
||||||
describe("on", () => {
|
describe("on", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -325,7 +346,7 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on("pull_request", spy);
|
probot.on("pull_request", spy);
|
||||||
|
|
||||||
expect(spy).toHaveBeenCalledTimes(0);
|
expect(spy).toHaveBeenCalledTimes(0);
|
||||||
|
@ -341,9 +362,15 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on("pull_request.opened", spy);
|
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);
|
await probot.receive(event);
|
||||||
expect(spy).toHaveBeenCalled();
|
expect(spy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
@ -354,7 +381,7 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on("pull_request.closed", spy);
|
probot.on("pull_request.closed", spy);
|
||||||
|
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
|
@ -367,7 +394,7 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.onAny(spy);
|
probot.onAny(spy);
|
||||||
|
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
|
@ -380,13 +407,19 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const event: WebhookEvent<"pull_request.opened"> = {
|
||||||
|
id: "123-456",
|
||||||
|
name: "pull_request",
|
||||||
|
payload: getPayloadExample("pull_request.opened"),
|
||||||
|
};
|
||||||
|
|
||||||
const event2: WebhookEvent<"issues.opened"> = {
|
const event2: WebhookEvent<"issues.opened"> = {
|
||||||
id: "123",
|
id: "123",
|
||||||
name: "issues",
|
name: "issues",
|
||||||
payload: getPayloadExample("issues.opened"),
|
payload: getPayloadExample("issues.opened"),
|
||||||
};
|
};
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on(["pull_request.opened", "issues.opened"], spy);
|
probot.on(["pull_request.opened", "issues.opened"], spy);
|
||||||
|
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
|
@ -401,7 +434,7 @@ describe("Probot", () => {
|
||||||
log: pino(streamLogsToOutput),
|
log: pino(streamLogsToOutput),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handler = jest.fn().mockImplementation((context) => {
|
const handler = vi.fn().mockImplementation((context) => {
|
||||||
expect(context.log.info).toBeDefined();
|
expect(context.log.info).toBeDefined();
|
||||||
context.log.info("testing");
|
context.log.info("testing");
|
||||||
|
|
||||||
|
@ -409,7 +442,7 @@ describe("Probot", () => {
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
id: context.id,
|
id: context.id,
|
||||||
msg: "testing",
|
msg: "testing",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -419,9 +452,40 @@ describe("Probot", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an authenticated client for installation.created", async () => {
|
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({
|
const probot = new Probot({
|
||||||
appId,
|
appId,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
request: {
|
||||||
|
fetch,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
|
@ -431,32 +495,35 @@ describe("Probot", () => {
|
||||||
};
|
};
|
||||||
event.payload.installation.id = 1;
|
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) => {
|
probot.on("installation.created", async (context) => {
|
||||||
await context.octokit.request("/");
|
await context.octokit.request("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
|
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an unauthenticated client for installation.deleted", async () => {
|
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({
|
const probot = new Probot({
|
||||||
appId,
|
appId,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
request: {
|
||||||
|
fetch,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
|
@ -466,46 +533,50 @@ describe("Probot", () => {
|
||||||
};
|
};
|
||||||
event.payload.installation.id = 1;
|
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) => {
|
probot.on("installation.deleted", async (context) => {
|
||||||
await context.octokit.request("/");
|
await context.octokit.request("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
await probot.receive(event).catch(console.log);
|
await probot.receive(event).catch(console.log);
|
||||||
|
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("returns an authenticated client for events without an installation", async () => {
|
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({
|
const probot = new Probot({
|
||||||
appId,
|
appId,
|
||||||
privateKey,
|
privateKey,
|
||||||
|
request: {
|
||||||
|
fetch,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
event = {
|
event = {
|
||||||
id: "123-456",
|
id: "123-456",
|
||||||
name: "check_run",
|
name: "check_run",
|
||||||
payload: getPayloadExamples("check_run").filter(
|
payload: getPayloadExamples("check_run").filter(
|
||||||
(event) => typeof event.installation === "undefined"
|
(event) => typeof event.installation === "undefined",
|
||||||
)[0],
|
)[0],
|
||||||
};
|
};
|
||||||
|
|
||||||
const mock = nock("https://api.github.com")
|
|
||||||
.get("/")
|
|
||||||
.matchHeader("authorization", (value) => value === undefined)
|
|
||||||
.reply(200, {});
|
|
||||||
|
|
||||||
probot.on("check_run", async (context) => {
|
probot.on("check_run", async (context) => {
|
||||||
await context.octokit.request("/");
|
await context.octokit.request("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
await probot.receive(event).catch(console.log);
|
await probot.receive(event).catch(console.log);
|
||||||
|
|
||||||
expect(mock.activeMocks()).toStrictEqual([]);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -524,7 +595,7 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on("pull_request", spy);
|
probot.on("pull_request", spy);
|
||||||
|
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
|
@ -538,7 +609,7 @@ describe("Probot", () => {
|
||||||
privateKey,
|
privateKey,
|
||||||
});
|
});
|
||||||
|
|
||||||
const spy = jest.fn();
|
const spy = vi.fn();
|
||||||
probot.on("pull_request", () => {
|
probot.on("pull_request", () => {
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
@ -568,7 +639,7 @@ describe("Probot", () => {
|
||||||
await probot.receive(event);
|
await probot.receive(event);
|
||||||
throw new Error("expected error to be raised from app");
|
throw new Error("expected error to be raised from app");
|
||||||
} catch (error) {
|
} 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(
|
const stubTranspiledAppFnPath = require.resolve(
|
||||||
"./fixtures/plugin/stub-typescript-transpiled-plugin"
|
"./fixtures/plugin/stub-typescript-transpiled-plugin.ts",
|
||||||
);
|
);
|
||||||
const basedir = process.cwd();
|
const basedir = process.cwd();
|
||||||
|
|
||||||
describe("resolver", () => {
|
describe("resolver", () => {
|
||||||
it("loads the module at the resolved path", async () => {
|
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 });
|
const module = await resolveAppFunction("foo", { resolver: stubResolver });
|
||||||
expect(module).toBe(require(stubAppFnPath));
|
expect(module).toBeInstanceOf(Function);
|
||||||
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("loads module transpiled from TypeScript (https://github.com/probot/probot/issues/1447)", async () => {
|
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 });
|
const module = await resolveAppFunction("foo", { resolver: stubResolver });
|
||||||
expect(module).toBe(require(stubTranspiledAppFnPath).default);
|
expect(module).toBeInstanceOf(Function);
|
||||||
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
expect(stubResolver).toHaveBeenCalledWith("foo", { basedir });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import path = require("path");
|
import path from "node:path";
|
||||||
|
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { sign } from "@octokit/webhooks-methods";
|
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", () => {
|
describe("run", () => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
let env: NodeJS.ProcessEnv;
|
let env: NodeJS.ProcessEnv;
|
||||||
|
@ -18,7 +21,7 @@ describe("run", () => {
|
||||||
PRIVATE_KEY_PATH: path.join(
|
PRIVATE_KEY_PATH: path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
"fixtures",
|
"fixtures",
|
||||||
"test-private-key.pem"
|
"test-private-key.pem",
|
||||||
),
|
),
|
||||||
WEBHOOK_PROXY_URL: "https://smee.io/EfHXC9BFfGAxbM6J",
|
WEBHOOK_PROXY_URL: "https://smee.io/EfHXC9BFfGAxbM6J",
|
||||||
WEBHOOK_SECRET: "secret",
|
WEBHOOK_SECRET: "secret",
|
||||||
|
@ -34,7 +37,7 @@ describe("run", () => {
|
||||||
() => {
|
() => {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
},
|
},
|
||||||
{ env }
|
{ env },
|
||||||
);
|
);
|
||||||
expect(initialized).toBeTruthy();
|
expect(initialized).toBeTruthy();
|
||||||
await server.stop();
|
await server.stop();
|
||||||
|
@ -58,10 +61,10 @@ describe("run", () => {
|
||||||
|
|
||||||
return new Promise(async (resolve) => {
|
return new Promise(async (resolve) => {
|
||||||
server = await run(
|
server = await run(
|
||||||
(app: Probot) => {
|
(_app: Probot) => {
|
||||||
initialized = true;
|
initialized = true;
|
||||||
},
|
},
|
||||||
{ env }
|
{ env },
|
||||||
);
|
);
|
||||||
expect(initialized).toBeFalsy();
|
expect(initialized).toBeFalsy();
|
||||||
await server.stop();
|
await server.stop();
|
||||||
|
@ -71,33 +74,39 @@ describe("run", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("defaults to JSON logs if NODE_ENV is set to 'production'", async () => {
|
it("defaults to JSON logs if NODE_ENV is set to 'production'", async () => {
|
||||||
const outputData = await captureLogOutput(async () => {
|
let outputData = "";
|
||||||
env.NODE_ENV = "production";
|
env.NODE_ENV = "production";
|
||||||
|
|
||||||
server = await run(
|
server = await run(
|
||||||
(app) => {
|
async (app) => {
|
||||||
|
outputData = await captureLogOutput(async () => {
|
||||||
app.log.fatal("test");
|
app.log.fatal("test");
|
||||||
},
|
}, app.log);
|
||||||
{ env }
|
},
|
||||||
);
|
{ env },
|
||||||
await server.stop();
|
);
|
||||||
});
|
await server.stop();
|
||||||
|
|
||||||
expect(outputData).toMatch(/"msg":"test"/);
|
expect(outputData).toMatch(/"msg":"test"/);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("webhooks", () => {
|
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 });
|
server = await run(() => {}, { env });
|
||||||
|
|
||||||
const dataString = JSON.stringify(pushEvent);
|
const dataString = JSON.stringify(pushEvent);
|
||||||
|
|
||||||
await request(server.expressApp)
|
await request(server.expressApp)
|
||||||
.post("/")
|
.post("/api/github/webhooks")
|
||||||
.send(dataString)
|
.send(dataString)
|
||||||
|
.set("content-type", "application/json")
|
||||||
.set("x-github-event", "push")
|
.set("x-github-event", "push")
|
||||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||||
.set("x-github-delivery", "123")
|
.set("x-github-delivery", "123")
|
||||||
|
@ -120,6 +129,7 @@ describe("run", () => {
|
||||||
await request(server.expressApp)
|
await request(server.expressApp)
|
||||||
.post("/custom-webhook")
|
.post("/custom-webhook")
|
||||||
.send(dataString)
|
.send(dataString)
|
||||||
|
.set("content-type", "application/json")
|
||||||
.set("x-github-event", "push")
|
.set("x-github-event", "push")
|
||||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||||
.set("x-github-delivery", "123")
|
.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 request from "supertest";
|
||||||
import pino from "pino";
|
import { pino } from "pino";
|
||||||
import { sign } from "@octokit/webhooks-methods";
|
import { sign } from "@octokit/webhooks-methods";
|
||||||
import getPort from "get-port";
|
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 appId = 1;
|
||||||
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
MIIBOQIBAAJBAIILhiN9IFpaE0pUXsesuuoaj6eeDiAqCiE49WB1tMB8ZMhC37kY
|
MIIEpAIBAAKCAQEA1c7+9z5Pad7OejecsQ0bu3aozN3tihPmljnnudb9G3HECdnH
|
||||||
Fl52NUYbUxb7JEf6pH5H9vqw1Wp69u78XeUCAwEAAQJAb88urnaXiXdmnIK71tuo
|
lWu2/a1gB9JW5TBQ+AVpum9Okx7KfqkfBKL9mcHgSL0yWMdjMfNOqNtrQqKlN4kE
|
||||||
/TyHBKt9I6Rhfzz0o9Gv7coL7a537FVDvV5UCARXHJMF41tKwj+zlt9EEUw7a1HY
|
p6RD++7sGbzbfZ9arwrlD/HSDAWGdGGJTSOBM6pHehyLmSC3DJoR/CTu0vTGTWXQ
|
||||||
wQIhAL4F/VHWSPHeTgXYf4EaX2OlpSOk/n7lsFtL/6bWRzRVAiEArzJs2vopJitv
|
rO64Z8tyXQPtVPb/YXrcUhbBp8i72b9Xky0fD6PkEebOy0Ip58XVAn2UPNlNOSPS
|
||||||
A1yBjz3q2nX+zthk+GLXrJQkYOnIk1ECIHfeFV8TWm5gej1LxZquBTA5pINoqDVq
|
ye+Qjtius0Md4Nie4+X8kwVI2Qjk3dSm0sw/720KJkdVDmrayeljtKBx6AtNQsSX
|
||||||
NKZSuZEHqGEFAiB6EDrxkovq8SYGhIQsJeqkTMO8n94xhMRZlFmIQDokEQIgAq5U
|
gzQbeMmiqFFkwrG1+zx6E7H7jqIQ9B6bvWKXGwIDAQABAoIBAD8kBBPL6PPhAqUB
|
||||||
r1UQNnUExRh7ZT0kFbMfO9jKYZVlQdCL9Dn93vo=
|
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-----`;
|
-----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", () => {
|
describe("Server", () => {
|
||||||
let server: Server;
|
let server: Server;
|
||||||
|
|
||||||
let output: any[];
|
let output: any[];
|
||||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||||
streamLogsToOutput._write = (object, encoding, done) => {
|
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||||
output.push(JSON.parse(object));
|
output.push(JSON.parse(object));
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
@ -45,9 +71,9 @@ describe("Server", () => {
|
||||||
|
|
||||||
// Error handler to avoid printing logs
|
// Error handler to avoid printing logs
|
||||||
server.expressApp.use(
|
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);
|
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 () => {
|
it("should return 200 and run event handlers in app function", async () => {
|
||||||
expect.assertions(3);
|
expect.assertions(3);
|
||||||
|
|
||||||
|
@ -86,28 +165,31 @@ describe("Server", () => {
|
||||||
const dataString = JSON.stringify(pushEvent);
|
const dataString = JSON.stringify(pushEvent);
|
||||||
|
|
||||||
await request(server.expressApp)
|
await request(server.expressApp)
|
||||||
.post("/")
|
.post("/api/github/webhooks")
|
||||||
.send(dataString)
|
.send(dataString)
|
||||||
|
.set("content-type", "application/json")
|
||||||
.set("x-github-event", "push")
|
.set("x-github-event", "push")
|
||||||
.set("x-hub-signature-256", await sign("secret", dataString))
|
.set("x-hub-signature-256", await sign("secret", dataString))
|
||||||
.set("x-github-delivery", "3sw4d5f6g7h8");
|
.set("x-github-delivery", "3sw4d5f6g7h8");
|
||||||
|
|
||||||
expect(output.length).toEqual(1);
|
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 () => {
|
test("respond with a friendly error when x-hub-signature-256 is missing", async () => {
|
||||||
await server.load(() => {});
|
await server.load(() => {});
|
||||||
|
|
||||||
await request(server.expressApp)
|
await request(server.expressApp)
|
||||||
.post("/")
|
.post("/api/github/webhooks")
|
||||||
.send(JSON.stringify(pushEvent))
|
.send(JSON.stringify(pushEvent))
|
||||||
|
.set("content-type", "application/json")
|
||||||
.set("x-github-event", "push")
|
.set("x-github-event", "push")
|
||||||
|
.set("content-type", "application/json")
|
||||||
// Note: 'x-hub-signature-256' is missing
|
// Note: 'x-hub-signature-256' is missing
|
||||||
.set("x-github-delivery", "3sw4d5f6g7h8")
|
.set("x-github-delivery", "3sw4d5f6g7h8")
|
||||||
.expect(
|
.expect(
|
||||||
400,
|
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()", () => {
|
describe(".start() / .stop()", () => {
|
||||||
it("should expect the correct error if port already in use", (next) => {
|
it("should expect the correct error if port already in use", () =>
|
||||||
expect.assertions(1);
|
new Promise<void>((next) => {
|
||||||
|
expect.assertions(1);
|
||||||
|
|
||||||
// block port 3001
|
// block port 3001
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
const blockade = http.createServer().listen(3001, async () => {
|
const blockade = http.createServer().listen(3001, async () => {
|
||||||
const server = new Server({
|
const server = new Server({
|
||||||
Probot: Probot.defaults({ appId, privateKey }),
|
Probot: Probot.defaults({ appId, privateKey }),
|
||||||
log: pino(streamLogsToOutput),
|
log: pino(streamLogsToOutput),
|
||||||
port: 3001,
|
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 () => {
|
it("should listen to port when not in use", async () => {
|
||||||
const testApp = new Server({
|
const testApp = new Server({
|
||||||
|
@ -179,21 +262,38 @@ describe("Server", () => {
|
||||||
describe("router", () => {
|
describe("router", () => {
|
||||||
it("prefixes paths with route name", () => {
|
it("prefixes paths with route name", () => {
|
||||||
const router = server.router("/my-app");
|
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");
|
return request(server.expressApp).get("/my-app/foo").expect(200, "foo");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("allows routes with no path", () => {
|
it("allows routes with no path", () => {
|
||||||
const router = server.router();
|
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");
|
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();
|
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");
|
return request(server.expressApp).get("/").expect(200, "foo");
|
||||||
});
|
});
|
||||||
|
@ -202,12 +302,12 @@ describe("Server", () => {
|
||||||
["foo", "bar"].forEach((name) => {
|
["foo", "bar"].forEach((name) => {
|
||||||
const router = server.router("/" + name);
|
const router = server.router("/" + name);
|
||||||
|
|
||||||
router.use((req, res, next) => {
|
router.use((_req, res, next) => {
|
||||||
res.append("X-Test", name);
|
res.append("X-Test", name);
|
||||||
next();
|
next();
|
||||||
});
|
});
|
||||||
|
|
||||||
router.get("/hello", (req, res) => res.end(name));
|
router.get("/hello", (_req, res) => res.end(name));
|
||||||
});
|
});
|
||||||
|
|
||||||
await request(server.expressApp)
|
await request(server.expressApp)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
import Stream from "stream";
|
import Stream from "node:stream";
|
||||||
|
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import pino from "pino";
|
import { pino } from "pino";
|
||||||
import { Options } from "pino-http";
|
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", () => {
|
describe("logging", () => {
|
||||||
let server: express.Express;
|
let server: express.Express;
|
||||||
|
@ -13,7 +14,7 @@ describe("logging", () => {
|
||||||
let options: Options;
|
let options: Options;
|
||||||
|
|
||||||
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
const streamLogsToOutput = new Stream.Writable({ objectMode: true });
|
||||||
streamLogsToOutput._write = (object, encoding, done) => {
|
streamLogsToOutput._write = (object, _encoding, done) => {
|
||||||
output.push(JSON.parse(object));
|
output.push(JSON.parse(object));
|
||||||
done();
|
done();
|
||||||
};
|
};
|
||||||
|
@ -22,11 +23,11 @@ describe("logging", () => {
|
||||||
function applyMiddlewares() {
|
function applyMiddlewares() {
|
||||||
server.use(express.json());
|
server.use(express.json());
|
||||||
server.use(getLoggingMiddleware(logger, options));
|
server.use(getLoggingMiddleware(logger, options));
|
||||||
server.get("/", (req, res) => {
|
server.get("/", (_req, res) => {
|
||||||
res.set("X-Test-Header", "testing");
|
res.set("X-Test-Header", "testing");
|
||||||
res.send("OK");
|
res.send("OK");
|
||||||
});
|
});
|
||||||
server.post("/", (req, res) => res.send("OK"));
|
server.post("/", (_req, res) => res.send("OK"));
|
||||||
}
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -40,7 +41,7 @@ describe("logging", () => {
|
||||||
return request(server)
|
return request(server)
|
||||||
.get("/")
|
.get("/")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((_res) => {
|
||||||
// logs id with request and response
|
// logs id with request and response
|
||||||
expect(output[0].req.id).toBeTruthy();
|
expect(output[0].req.id).toBeTruthy();
|
||||||
expect(typeof output[0].responseTime).toEqual("number");
|
expect(typeof output[0].responseTime).toEqual("number");
|
||||||
|
@ -54,7 +55,7 @@ describe("logging", () => {
|
||||||
method: "GET",
|
method: "GET",
|
||||||
remoteAddress: "::ffff:127.0.0.1",
|
remoteAddress: "::ffff:127.0.0.1",
|
||||||
url: "/",
|
url: "/",
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(output[0].res).toEqual(
|
expect(output[0].res).toEqual(
|
||||||
|
@ -62,7 +63,7 @@ describe("logging", () => {
|
||||||
headers: expect.objectContaining({
|
headers: expect.objectContaining({
|
||||||
"x-test-header": "testing",
|
"x-test-header": "testing",
|
||||||
}),
|
}),
|
||||||
})
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -73,7 +74,7 @@ describe("logging", () => {
|
||||||
.get("/")
|
.get("/")
|
||||||
.set("X-Request-ID", "42")
|
.set("X-Request-ID", "42")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((_res) => {
|
||||||
expect(output[0].req.id).toEqual("42");
|
expect(output[0].req.id).toEqual("42");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -84,7 +85,7 @@ describe("logging", () => {
|
||||||
.get("/")
|
.get("/")
|
||||||
.set("X-GitHub-Delivery", "a-b-c")
|
.set("X-GitHub-Delivery", "a-b-c")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((_res) => {
|
||||||
expect(output[0].req.id).toEqual("a-b-c");
|
expect(output[0].req.id).toEqual("a-b-c");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -92,14 +93,14 @@ describe("logging", () => {
|
||||||
test("sets ignorePaths option to ignore logging", () => {
|
test("sets ignorePaths option to ignore logging", () => {
|
||||||
options = {
|
options = {
|
||||||
autoLogging: {
|
autoLogging: {
|
||||||
ignorePaths: ["/"],
|
ignore: (req) => ["/"].includes(req.url!),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
applyMiddlewares();
|
applyMiddlewares();
|
||||||
return request(server)
|
return request(server)
|
||||||
.get("/")
|
.get("/")
|
||||||
.expect(200)
|
.expect(200)
|
||||||
.expect((res) => {
|
.expect((_res) => {
|
||||||
expect(output.length).toEqual(0);
|
expect(output.length).toEqual(0);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { RepositoryEditedEvent } from "@octokit/webhooks-types";
|
import { RepositoryEditedEvent } from "@octokit/webhooks-types";
|
||||||
import { expectType } from "tsd";
|
import { expectType } from "tsd";
|
||||||
import { Probot } from "../../src";
|
import { Probot } from "../../src/index.js";
|
||||||
|
|
||||||
const app = new Probot({});
|
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";
|
import { randomInt } from "node:crypto";
|
||||||
// tslint:disable-next-line:no-var-requires
|
import http from "node:http";
|
||||||
|
import net from "node:net";
|
||||||
|
|
||||||
|
import express, { type Response } from "express";
|
||||||
const sse: (
|
const sse: (
|
||||||
req: express.Request,
|
req: express.Request,
|
||||||
res: express.Response,
|
res: express.Response,
|
||||||
next: express.NextFunction
|
next: express.NextFunction,
|
||||||
) => void = require("connect-sse")();
|
) => void = require("connect-sse")();
|
||||||
|
import fetchMock from "fetch-mock";
|
||||||
import EventSource from "eventsource";
|
import EventSource from "eventsource";
|
||||||
import http from "http";
|
import { describe, expect, afterEach, test, vi } from "vitest";
|
||||||
import net from "net";
|
import { getLog } from "../src/helpers/get-log.js";
|
||||||
import nock from "nock";
|
import { createWebhookProxy } from "../src/helpers/webhook-proxy.js";
|
||||||
import { getLog } from "../src/helpers/get-log";
|
|
||||||
import { createWebhookProxy } from "../src/helpers/webhook-proxy";
|
|
||||||
|
|
||||||
const targetPort = 999999;
|
let targetPort = 999999;
|
||||||
|
|
||||||
interface SSEResponse extends Response {
|
interface SSEResponse extends Response {
|
||||||
json(body: any, status?: string): this;
|
json(body: any, status?: string): this;
|
||||||
}
|
}
|
||||||
|
|
||||||
jest.setTimeout(10000);
|
|
||||||
|
|
||||||
describe("webhook-proxy", () => {
|
describe("webhook-proxy", () => {
|
||||||
// tslint:disable-next-line:one-variable-per-declaration
|
let emit: SSEResponse["json"];
|
||||||
let emit: SSEResponse["json"], proxy: EventSource, server: http.Server;
|
let proxy: EventSource;
|
||||||
|
let server: http.Server;
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
server && server.close();
|
server && server.close();
|
||||||
|
@ -30,59 +31,111 @@ describe("webhook-proxy", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("with a valid proxy server", () => {
|
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();
|
const app = express();
|
||||||
|
|
||||||
app.get("/events", sse, (req, res: SSEResponse) => {
|
app.get("/events", sse, (_req, res: SSEResponse) => {
|
||||||
res.json({}, "ready");
|
res.json({}, "ready");
|
||||||
emit = res.json;
|
emit = res.json;
|
||||||
});
|
});
|
||||||
|
|
||||||
server = app.listen(0, () => {
|
server = app.listen(0, async () => {
|
||||||
const url = `http://127.0.0.1:${
|
targetPort = (server.address() as net.AddressInfo).port;
|
||||||
(server.address() as net.AddressInfo).port
|
const url = `http://127.0.0.1:${targetPort}/events`;
|
||||||
}/events`;
|
|
||||||
proxy = createWebhookProxy({
|
const fetch = fetchMock
|
||||||
|
.sandbox()
|
||||||
|
.postOnce(`http://localhost:${targetPort}/test`, {
|
||||||
|
status: 200,
|
||||||
|
then: () => {
|
||||||
|
finishedPromise.resolve!();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
proxy = (await createWebhookProxy({
|
||||||
url,
|
url,
|
||||||
port: targetPort,
|
port: targetPort,
|
||||||
path: "/test",
|
path: "/test",
|
||||||
logger: getLog({ level: "fatal" }),
|
logger: getLog({ level: "fatal" }),
|
||||||
})!;
|
fetch,
|
||||||
|
})) as EventSource;
|
||||||
|
|
||||||
// Wait for proxy to be ready
|
// Wait for proxy to be ready
|
||||||
proxy.addEventListener("ready", () => done());
|
proxy.addEventListener("ready", readyPromise.resolve!);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
test("forwards events to server", (done) => {
|
await readyPromise.promise;
|
||||||
nock(`http://localhost:${targetPort}`)
|
|
||||||
.post("/test")
|
|
||||||
.reply(200, () => {
|
|
||||||
done();
|
|
||||||
});
|
|
||||||
|
|
||||||
const body = { action: "foo" };
|
|
||||||
|
|
||||||
emit({
|
emit({
|
||||||
body,
|
body: { action: "foo" },
|
||||||
"x-github-event": "test",
|
"x-github-event": "test",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await finishedPromise.promise;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("logs an error when the proxy server is not found", (done) => {
|
test("logs an error when the proxy server is not found", async () => {
|
||||||
const url = "http://bad.proxy/events";
|
expect.assertions(2);
|
||||||
nock("http://bad.proxy").get("/events").reply(404);
|
|
||||||
|
|
||||||
const log = getLog({ level: "fatal" }).child({});
|
let finishedPromise = {
|
||||||
log.error = jest.fn();
|
promise: undefined,
|
||||||
|
reject: undefined,
|
||||||
|
resolve: undefined,
|
||||||
|
} as {
|
||||||
|
promise?: Promise<any>;
|
||||||
|
resolve?: (value?: any) => any;
|
||||||
|
reject?: (reason?: any) => any;
|
||||||
|
};
|
||||||
|
|
||||||
proxy = createWebhookProxy({ url, logger: log })!;
|
finishedPromise.promise = new Promise((resolve, reject) => {
|
||||||
|
finishedPromise.resolve = resolve;
|
||||||
proxy.addEventListener("error", (error: any) => {
|
finishedPromise.reject = reject;
|
||||||
expect(error.status).toBe(404);
|
|
||||||
expect(log.error).toHaveBeenCalledWith(error);
|
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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": {
|
"compilerOptions": {
|
||||||
|
"module": "Node16",
|
||||||
|
"verbatimModuleSyntax": false,
|
||||||
"noImplicitReturns": true,
|
"noImplicitReturns": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
|
@ -12,7 +14,10 @@
|
||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"declaration": true,
|
"declaration": true,
|
||||||
"resolveJsonModule": true
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true,
|
||||||
|
"lib": ["es2023", "dom"],
|
||||||
|
"moduleResolution": "node16"
|
||||||
},
|
},
|
||||||
"include": ["src/**/*"],
|
"include": ["src/**/*"],
|
||||||
"compileOnSave": false
|
"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