├── .github ├── renovate.json └── workflows │ ├── release.yml │ └── test.yml ├── test ├── ci.js ├── fixtures │ ├── push.json │ └── app.js └── index.test.js ├── index.d.ts ├── LICENSE.md ├── pino-transport-github-actions.js ├── package.json ├── index.js ├── .gitignore └── README.md /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>probot/.github" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /test/ci.js: -------------------------------------------------------------------------------- 1 | // This is run by .github/workflows/test.yml 2 | import { run } from "../index.js"; 3 | 4 | run(app); 5 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationFunction } from "probot"; 2 | 3 | declare function run( 4 | probotApp: ApplicationFunction | ApplicationFunction[] 5 | ): Promise; 6 | 7 | export { run }; 8 | export * from "probot"; 9 | -------------------------------------------------------------------------------- /test/fixtures/push.json: -------------------------------------------------------------------------------- 1 | { 2 | "head_commit": { 3 | "id": "headcommitsha123" 4 | }, 5 | "repository": { 6 | "name": "adapter-github-actions", 7 | "owner": { 8 | "login": "probot" 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixtures/app.js: -------------------------------------------------------------------------------- 1 | import { relative } from "node:path"; 2 | 3 | /** 4 | * @param {import('probot').Probot} app 5 | */ 6 | export default async function app(app) { 7 | app.log.debug("This is a debug message"); 8 | app.log.info("This is an info message"); 9 | app.log.warn("This is a warning message"); 10 | 11 | app.on("push", async (context) => { 12 | await context.octokit.request( 13 | "POST /repos/{owner}/{repo}/commits/{commit_sha}/comments", 14 | context.repo({ 15 | commit_sha: context.payload.head_commit.id, 16 | body: `Hello from ${relative(process.cwd(), __filename)}`, 17 | }) 18 | ); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | ISC License 2 | 3 | Copyright (c) 2019, Steve Winton (https://github.com/swinton) 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted, provided that the above 7 | copyright notice and this permission notice appear in all copies. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES 10 | WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 11 | MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR 12 | ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 13 | WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 14 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 15 | OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 16 | -------------------------------------------------------------------------------- /pino-transport-github-actions.js: -------------------------------------------------------------------------------- 1 | import { inspect } from "util"; 2 | 3 | import through from "through2"; 4 | import core from "@actions/core"; 5 | import pino from "pino"; 6 | 7 | const LEVEL_TO_ACTIONS_CORE_LOG_METHOD = { 8 | trace: "debug", 9 | debug: "debug", 10 | info: "info", 11 | warn: "warning", 12 | error: "error", 13 | fatal: "error", 14 | }; 15 | 16 | export const transport = through.obj(function (chunk, enc, cb) { 17 | const { level, hostname, pid, msg, time, name, ...meta } = JSON.parse(chunk); 18 | const levelLabel = pino.levels.labels[level] || level; 19 | const logMethodName = LEVEL_TO_ACTIONS_CORE_LOG_METHOD[levelLabel]; 20 | 21 | const output = [ 22 | msg, 23 | Object.keys(meta).length ? inspect(meta, { depth: Infinity }) : "", 24 | ] 25 | .join("\n") 26 | .trim(); 27 | 28 | if (logMethodName in core) { 29 | core[logMethodName](output); 30 | } else { 31 | core.error(`"${level}" is not a known log level - ${output}`); 32 | } 33 | 34 | cb(); 35 | }); 36 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | "on": 3 | push: 4 | branches: 5 | - master 6 | - next 7 | - beta 8 | - "*.x" 9 | 10 | # These are recommended by the semantic-release docs: https://github.com/semantic-release/npm#npm-provenance 11 | permissions: 12 | contents: write # to be able to publish a GitHub release 13 | issues: write # to be able to comment on released issues 14 | pull-requests: write # to be able to comment on released pull requests 15 | id-token: write # to enable use of OIDC for npm provenance 16 | 17 | jobs: 18 | release: 19 | name: release 20 | runs-on: ubuntu-latest 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: lts/* 26 | cache: npm 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npx semantic-release 30 | env: 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | NPM_TOKEN: ${{ secrets.PROBOTBOT_NPM_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | "on": 3 | push: {} 4 | pull_request: 5 | types: 6 | - opened 7 | - synchronize 8 | jobs: 9 | integration: 10 | runs-on: ubuntu-latest 11 | if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-node@v4 15 | with: 16 | cache: npm 17 | node-version: 18 18 | - run: npm ci 19 | - run: npm test 20 | createComment: 21 | runs-on: ubuntu-latest 22 | permissions: 23 | issues: write # to be able to comment on released issues 24 | pull-requests: write # to be able to comment on released pull requests 25 | if: github.event_name == 'push' 26 | steps: 27 | - uses: actions/checkout@v4 28 | - uses: actions/setup-node@v4 29 | with: 30 | cache: npm 31 | node-version: 18 32 | - run: npm ci 33 | - run: node test/fixtures/app.js 34 | env: 35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@probot/adapter-github-actions", 3 | "publishConfig": { 4 | "access": "public", 5 | "provenance": true 6 | }, 7 | "version": "0.0.0-development", 8 | "description": "Adapter to run a Probot application function in GitHub Actions", 9 | "main": "index.js", 10 | "scripts": { 11 | "test": "vitest", 12 | "lint": "prettier --check '*.{js,md,json}' '.github/**/*.yml'", 13 | "lint:fix": "prettier --write '*.{js,md,json}' '.github/**/*.yml'" 14 | }, 15 | "keywords": [ 16 | "probot", 17 | "probot-adapter", 18 | "github-actions" 19 | ], 20 | "author": "Steve Winton (https://github.com/swinton)", 21 | "contributors": [ 22 | "Gregor Martynus (https://github.com/gr2m)" 23 | ], 24 | "license": "ISC", 25 | "repository": "github:probot/adapter-github-actions", 26 | "devDependencies": { 27 | "nock": "^14.0.0-beta.5", 28 | "prettier": "^3.2.5", 29 | "vitest": "^4.0.0" 30 | }, 31 | "dependencies": { 32 | "@actions/core": "^1.10.1", 33 | "pino": "^10.0.0", 34 | "probot": "^14.0.2", 35 | "through2": "^4.0.2" 36 | }, 37 | "type": "module" 38 | } 39 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export * from "probot"; 2 | import { createProbot } from "probot"; 3 | import pino from "pino"; 4 | import { readFileSync } from "node:fs"; 5 | 6 | import { transport } from "./pino-transport-github-actions.js"; 7 | 8 | export async function run(app) { 9 | const log = pino({}, transport); 10 | 11 | const githubToken = 12 | process.env.GITHUB_TOKEN || 13 | process.env.INPUT_GITHUB_TOKEN || 14 | process.env.INPUT_TOKEN; 15 | 16 | if (!githubToken) { 17 | log.error( 18 | "[probot/adapter-github-actions] a token must be passed as `env.GITHUB_TOKEN` or `with.GITHUB_TOKEN` or `with.token`, see https://github.com/probot/adapter-github-actions#usage" 19 | ); 20 | return; 21 | } 22 | 23 | const envVariablesMissing = [ 24 | "GITHUB_RUN_ID", 25 | "GITHUB_EVENT_NAME", 26 | "GITHUB_EVENT_PATH", 27 | ].filter((name) => !process.env[name]); 28 | 29 | if (envVariablesMissing.length) { 30 | log.error( 31 | `[probot/adapter-github-actions] GitHub Action default environment variables missing: ${envVariablesMissing.join( 32 | ", " 33 | )}. See https://docs.github.com/en/free-pro-team@latest/actions/reference/environment-variables#default-environment-variables` 34 | ); 35 | return; 36 | } 37 | 38 | const probot = createProbot({ 39 | overrides: { 40 | githubToken, 41 | log, 42 | }, 43 | }); 44 | 45 | await probot.load(app); 46 | 47 | return probot 48 | .receive({ 49 | id: process.env.GITHUB_RUN_ID, 50 | name: process.env.GITHUB_EVENT_NAME, 51 | payload: JSON.parse(readFileSync(process.env.GITHUB_EVENT_PATH)), 52 | }) 53 | .catch((error) => { 54 | probot.log.error(error); 55 | }); 56 | } 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # next.js build output 79 | .next 80 | 81 | # nuxt.js build output 82 | .nuxt 83 | 84 | # gatsby files 85 | .cache/ 86 | public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :electric_plug: `@probot/adapter-github-actions` 2 | 3 | > Adapter to run a [Probot](https://probot.github.io/) application function in [GitHub Actions](https://github.com/features/actions) 4 | 5 | [![Build Status](https://github.com/probot/adapter-github-actions/workflows/Test/badge.svg)](https://github.com/probot/adapter-github-actions/actions) 6 | 7 | ## Usage 8 | 9 | Create your Probot Application as always 10 | 11 | ```js 12 | // app.js 13 | export default (app) => { 14 | app.on("issues.opened", async (context) => { 15 | const params = context.issue({ body: "Hello World!" }); 16 | await context.octokit.issues.createComment(params); 17 | }); 18 | }; 19 | ``` 20 | 21 | Then in the entrypoint of your GitHub Action, require `@probot/adapter-github-actions` instead of `probot` 22 | 23 | ```js 24 | // index.js 25 | import { run } from "@probot/adapter-github-actions"; 26 | import app from "./app.js"; 27 | 28 | run(app).catch((error) => { 29 | console.error(error); 30 | process.exit(1); 31 | }); 32 | ``` 33 | 34 | Then use `index.js` as your entrypoint in the `action.yml` file 35 | 36 | ```yaml 37 | name: "Probot app name" 38 | description: "Probot app description." 39 | runs: 40 | using: "node20" 41 | main: "index.js" 42 | ``` 43 | 44 | **Important**: Your external dependencies will not be installed, you have to either vendor them in by committing the contents of the `node_modules` folder, or compile the code to a single executable script (recommended). See [GitHub's documentation](https://docs.github.com/en/actions/creating-actions/creating-a-javascript-action#commit-tag-and-push-your-action-to-github) 45 | 46 | For an example Probot App that is continuously published as GitHub Action, see https://github.com/probot/example-github-action#readme 47 | 48 | ## How it works 49 | 50 | [Probot](https://probot.github.io/) is a framework for building [GitHub Apps](docs.github.com/apps), which is different to creating [GitHub Actions](https://docs.github.com/actions/) in many ways, but the functionality is the same: 51 | 52 | Both get notified about events on GitHub, which you can act on. While a GitHub App gets notified about a GitHub event via a webhook request sent by GitHub, a GitHub Action can receive the event payload by reading a JSON file from the file system. We can abstract away the differences, so the same hello world example app shown above works in both environments. 53 | 54 | Relevant differences for Probot applications: 55 | 56 | 1. You cannot authenticate as the app. The `probot` instance you receive is authenticated using a GitHub token. In most cases the token will be set to `secrets.GITHUB_TOKEN`, which is [an installation access token](https://docs.github.com/en/actions/reference/authentication-in-a-workflow#about-the-github_token-secret). The provided `GITHUB_TOKEN` expires when the job is done or after 6 hours, whichever comes first. You do not have access to an `APP_ID` or `PRIVATE_KEY`, you cannot create new tokens or renew the provided one. 57 | 2. `secrets.GITHUB_TOKEN` is scoped to the current repository. You cannot read data from other repositories unless they are public, you cannot update any other repositories, or access organization-level APIs. 58 | 3. You could provide a personal access token instead of `secrets.GITHUB_TOKEN` to workaround the limits of a repository-scoped token, but be sure you know what you are doing. 59 | 4. You don't need to configure `WEBHOOK_SECRET`, because no webhook request gets sent, the event information can directly be retrieved from environment variables and the local file system. 60 | 61 | For a more thorough comparison, see [@jasonetco's](https://github.com/jasonetco) posts: 62 | 63 | 1. [Probot App or GitHub Action](https://jasonet.co/posts/probot-app-or-github-action/) (Jan 2019) 64 | 2. [Update from April 2020](https://jasonet.co/posts/probot-app-or-github-action-v2/) 65 | 66 | ## License 67 | 68 | [ISC](LICENSE.md) 69 | -------------------------------------------------------------------------------- /test/index.test.js: -------------------------------------------------------------------------------- 1 | import nock from "nock"; 2 | import { describe, beforeEach, test, expect, vi } from "vitest"; 3 | import { dirname, join } from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | 6 | import { run } from "../index.js"; 7 | import app from "./fixtures/app.js"; 8 | 9 | nock.disableNetConnect(); 10 | 11 | const __dirname = dirname(fileURLToPath(import.meta.url)); 12 | 13 | describe("@probot/adapter-github-actions", () => { 14 | beforeEach(() => { 15 | process.env = {}; 16 | }); 17 | test("happy path", async () => { 18 | process.env.GITHUB_TOKEN = "token123"; 19 | process.env.GITHUB_RUN_ID = "1"; 20 | process.env.GITHUB_EVENT_NAME = "push"; 21 | process.env.GITHUB_EVENT_PATH = join(__dirname, "fixtures", "push.json"); 22 | 23 | const mock = nock("https://api.github.com") 24 | .post( 25 | "/repos/probot/adapter-github-actions/commits/headcommitsha123/comments", 26 | (requestBody) => { 27 | expect(requestBody).toStrictEqual({ 28 | body: "Hello from test/fixtures/app.js", 29 | }); 30 | 31 | return true; 32 | } 33 | ) 34 | .reply(201, {}); 35 | 36 | const output = []; 37 | const storeOutput = (data) => output.push(data); 38 | const origWrite = process.stdout.write; 39 | process.stdout.write = vi.fn(storeOutput); 40 | await run(app); 41 | process.stdout.write = origWrite; 42 | expect(output).toStrictEqual([ 43 | "This is an info message\n", 44 | "::warning::This is a warning message\n", 45 | ]); 46 | 47 | expect(mock.activeMocks()).toStrictEqual([]); 48 | }); 49 | 50 | test("GITHUB_TOKEN not set", async () => { 51 | const output = []; 52 | const storeOutput = (data) => output.push(data); 53 | const origWrite = process.stdout.write; 54 | process.stdout.write = vi.fn(storeOutput); 55 | await run(app); 56 | process.stdout.write = origWrite; 57 | expect(output).toStrictEqual([ 58 | "::error::[probot/adapter-github-actions] a token must be passed as `env.GITHUB_TOKEN` or `with.GITHUB_TOKEN` or `with.token`, see https://github.com/probot/adapter-github-actions#usage\n", 59 | ]); 60 | }); 61 | 62 | test("GITHUB_RUN_ID not set", async () => { 63 | process.env.GITHUB_TOKEN = "token123"; 64 | 65 | const output = []; 66 | const storeOutput = (data) => output.push(data); 67 | const origWrite = process.stdout.write; 68 | process.stdout.write = vi.fn(storeOutput); 69 | await run(app); 70 | process.stdout.write = origWrite; 71 | expect(output).toStrictEqual([ 72 | "::error::[probot/adapter-github-actions] GitHub Action default environment variables missing: GITHUB_RUN_ID, GITHUB_EVENT_NAME, GITHUB_EVENT_PATH. See https://docs.github.com/en/free-pro-team@latest/actions/reference/environment-variables#default-environment-variables\n", 73 | ]); 74 | }); 75 | 76 | test("error response", async () => { 77 | process.env.GITHUB_TOKEN = "token123"; 78 | process.env.GITHUB_RUN_ID = "1"; 79 | process.env.GITHUB_EVENT_NAME = "push"; 80 | process.env.GITHUB_EVENT_PATH = join(__dirname, "fixtures", "push.json"); 81 | 82 | const mock = nock("https://api.github.com") 83 | .post( 84 | "/repos/probot/adapter-github-actions/commits/headcommitsha123/comments", 85 | (requestBody) => { 86 | expect(requestBody).toStrictEqual({ 87 | body: "Hello from test/fixtures/app.js", 88 | }); 89 | 90 | return true; 91 | } 92 | ) 93 | .reply(403, { 94 | error: "nope", 95 | }); 96 | 97 | const output = []; 98 | const storeOutput = (data) => output.push(data); 99 | const origWrite = process.stdout.write; 100 | process.stdout.write = vi.fn(storeOutput); 101 | await run(app); 102 | process.stdout.write = origWrite; 103 | 104 | expect( 105 | output[2].startsWith('::error::Unknown error: {"error":"nope"}%0A{%0A') 106 | ).toBe(true); 107 | 108 | expect(mock.activeMocks()).toStrictEqual([]); 109 | }); 110 | 111 | test("unknown log level", async () => { 112 | process.env.GITHUB_TOKEN = "token123"; 113 | process.env.GITHUB_RUN_ID = "1"; 114 | process.env.GITHUB_EVENT_NAME = "push"; 115 | process.env.GITHUB_EVENT_PATH = join(__dirname, "fixtures", "push.json"); 116 | 117 | const output = []; 118 | const storeOutput = (data) => output.push(data); 119 | const origWrite = process.stdout.write; 120 | process.stdout.write = vi.fn(storeOutput); 121 | await run((app) => app.log.info({ level: "unknown" }, "oopsies")); 122 | process.stdout.write = origWrite; 123 | 124 | expect(output).toStrictEqual([ 125 | '::error::"unknown" is not a known log level - oopsies\n', 126 | ]); 127 | }); 128 | }); 129 | --------------------------------------------------------------------------------