From 2ccf259e64f7957fa0f1a4d6e659eb6a2dedc6a4 Mon Sep 17 00:00:00 2001 From: Tomas Srnka Date: Mon, 13 Apr 2026 12:44:13 +0200 Subject: [PATCH 1/5] feat: add reusable PR Slack notification workflow Reusable workflow that posts new PRs to a Slack channel, tagging a configurable user group. Any e2b-dev repo can call it with a 10-line workflow file and its own SLACK_CHANNEL_ID / SLACK_REVIEW_GROUP vars. Includes: - Slack mrkdwn injection prevention (escaped PR titles) - Pinned dependencies (package-lock.json + npm ci) - Draft PR skip Co-Authored-By: Claude Opus 4.6 (1M context) --- .../actions/pr-slack-notify/package-lock.json | 454 ++++++++++++++++++ .github/actions/pr-slack-notify/package.json | 10 + .github/actions/pr-slack-notify/script.js | 77 +++ .github/workflows/pr-slack-notify.yml | 46 ++ 4 files changed, 587 insertions(+) create mode 100644 .github/actions/pr-slack-notify/package-lock.json create mode 100644 .github/actions/pr-slack-notify/package.json create mode 100644 .github/actions/pr-slack-notify/script.js create mode 100644 .github/workflows/pr-slack-notify.yml diff --git a/.github/actions/pr-slack-notify/package-lock.json b/.github/actions/pr-slack-notify/package-lock.json new file mode 100644 index 0000000..72d7ba4 --- /dev/null +++ b/.github/actions/pr-slack-notify/package-lock.json @@ -0,0 +1,454 @@ +{ + "name": "pr-slack-notify", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "pr-slack-notify", + "version": "1.0.0", + "dependencies": { + "@slack/web-api": "^7.0.0" + } + }, + "node_modules/@slack/logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@slack/logger/-/logger-4.0.1.tgz", + "integrity": "sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==", + "license": "MIT", + "dependencies": { + "@types/node": ">=18" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@slack/types": { + "version": "2.20.1", + "resolved": "https://registry.npmjs.org/@slack/types/-/types-2.20.1.tgz", + "integrity": "sha512-eWX2mdt1ktpn8+40iiMc404uGrih+2fxiky3zBcPjtXKj6HLRdYlmhrPkJi7JTJm8dpXR6BWVWEDBXtaWMKD6A==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0", + "npm": ">= 6.12.0" + } + }, + "node_modules/@slack/web-api": { + "version": "7.15.0", + "resolved": "https://registry.npmjs.org/@slack/web-api/-/web-api-7.15.0.tgz", + "integrity": "sha512-va7zYIt3QHG1x9M/jqXXRPFMoOVlVSSRHC5YH+DzKYsrz5xUKOA3lR4THsu/Zxha9N1jOndbKFKLtr0WOPW1Vw==", + "license": "MIT", + "dependencies": { + "@slack/logger": "^4.0.1", + "@slack/types": "^2.20.1", + "@types/node": ">=18", + "@types/retry": "0.12.0", + "axios": "^1.13.5", + "eventemitter3": "^5.0.1", + "form-data": "^4.0.4", + "is-electron": "2.2.2", + "is-stream": "^2", + "p-queue": "^6", + "p-retry": "^4", + "retry": "^0.13.1" + }, + "engines": { + "node": ">= 18", + "npm": ">= 8.6.0" + } + }, + "node_modules/@types/node": { + "version": "25.5.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", + "integrity": "sha512-tO4ZIRKNC+MDWV4qKVZe3Ql/woTnmHDr5JD8UI5hn2pwBrHEwOEMZK7WlNb5RKB6EoJ02gwmQS9OrjuFnZYdpg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.14.0.tgz", + "integrity": "sha512-3Y8yrqLSwjuzpXuZ0oIYZ/XGgLwUIBU3uLvbcpb0pidD9ctpShJd43KSlEEkVQg6DS0G9NKyzOvBfUtDKEyHvQ==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^2.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-electron": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/is-electron/-/is-electron-2.2.2.tgz", + "integrity": "sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==", + "license": "MIT" + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/proxy-from-env": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", + "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + } + } +} diff --git a/.github/actions/pr-slack-notify/package.json b/.github/actions/pr-slack-notify/package.json new file mode 100644 index 0000000..22e2816 --- /dev/null +++ b/.github/actions/pr-slack-notify/package.json @@ -0,0 +1,10 @@ +{ + "name": "pr-slack-notify", + "version": "1.0.0", + "private": true, + "description": "Post new PRs to #code-review-requests Slack channel", + "main": "script.js", + "dependencies": { + "@slack/web-api": "^7.0.0" + } +} diff --git a/.github/actions/pr-slack-notify/script.js b/.github/actions/pr-slack-notify/script.js new file mode 100644 index 0000000..7fd7b65 --- /dev/null +++ b/.github/actions/pr-slack-notify/script.js @@ -0,0 +1,77 @@ +const { WebClient } = require("@slack/web-api"); +const fs = require("fs"); + +const slackToken = process.env.SLACK_BOT_TOKEN; +const channelId = process.env.SLACK_CHANNEL_ID; +const reviewGroup = process.env.SLACK_REVIEW_GROUP; + +if (!slackToken || !channelId || !reviewGroup) { + console.error("Missing required env vars: SLACK_BOT_TOKEN, SLACK_CHANNEL_ID, SLACK_REVIEW_GROUP"); + process.exit(1); +} + +const slack = new WebClient(slackToken); + +function getEvent() { + return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); +} + +function getPR(ev) { + // Support manual trigger via workflow_dispatch + if (process.env.PR_URL) { + return { + number: parseInt(process.env.PR_NUMBER, 10), + title: process.env.PR_TITLE || "PR", + html_url: process.env.PR_URL, + user: { login: process.env.PR_AUTHOR || "unknown" }, + }; + } + + const pr = ev.pull_request; + if (!pr) return null; + + return { + number: pr.number, + title: pr.title, + html_url: pr.html_url, + user: { login: pr.user.login }, + }; +} + +(async () => { + const ev = getEvent(); + const pr = getPR(ev); + + if (!pr) { + console.log("No PR in event; exiting."); + return; + } + + // Skip draft PRs + if (ev.pull_request?.draft) { + console.log(`PR #${pr.number} is a draft; skipping.`); + return; + } + + // Escape Slack mrkdwn control characters to prevent injection via PR title. + // A fork contributor could craft a title with > or to break + // formatting or trigger channel-wide pings. + const safeTitle = pr.title + .replace(/&/g, "&") + .replace(//g, ">"); + + const message = ` — new PR needs review\n*<${pr.html_url}|#${pr.number} — ${safeTitle}>* by ${pr.user.login}`; + + await slack.chat.postMessage({ + channel: channelId, + text: message, + unfurl_links: false, + unfurl_media: false, + }); + + console.log(`Posted PR #${pr.number} to channel ${channelId}.`); +})().catch((error) => { + console.error(error); + process.exit(1); +}); diff --git a/.github/workflows/pr-slack-notify.yml b/.github/workflows/pr-slack-notify.yml new file mode 100644 index 0000000..c718b68 --- /dev/null +++ b/.github/workflows/pr-slack-notify.yml @@ -0,0 +1,46 @@ +name: Post PR to Slack + +on: + workflow_call: + inputs: + channel_id: + description: 'Slack channel ID to post to' + required: true + type: string + review_group: + description: 'Slack user group ID to tag (e.g. @pr-infra)' + required: true + type: string + secrets: + SLACK_BOT_TOKEN: + description: 'Slack bot token' + required: true + +permissions: + contents: read + +jobs: + notify: + runs-on: ubuntu-latest + steps: + - name: Checkout .github repo + uses: actions/checkout@v5 + with: + repository: e2b-dev/.github + sparse-checkout: .github/actions/pr-slack-notify + + - name: Setup Node + uses: actions/setup-node@v6 + with: + node-version: 24 + + - name: Install deps + run: npm ci + working-directory: .github/actions/pr-slack-notify + + - name: Post to Slack + env: + SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} + SLACK_CHANNEL_ID: ${{ inputs.channel_id }} + SLACK_REVIEW_GROUP: ${{ inputs.review_group }} + run: node .github/actions/pr-slack-notify/script.js From 579197773db63c05dd96486b7558991056b20139 Mon Sep 17 00:00:00 2001 From: Tomas Srnka Date: Mon, 13 Apr 2026 12:47:37 +0200 Subject: [PATCH 2/5] fix: remove dead workflow_dispatch code from script The script had a manual trigger path (PR_URL/PR_NUMBER env vars) that was never reachable from the reusable workflow. Simplified to only read from the event payload. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/pr-slack-notify/script.js | 29 +++-------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/.github/actions/pr-slack-notify/script.js b/.github/actions/pr-slack-notify/script.js index 7fd7b65..6cb332e 100644 --- a/.github/actions/pr-slack-notify/script.js +++ b/.github/actions/pr-slack-notify/script.js @@ -16,39 +16,16 @@ function getEvent() { return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); } -function getPR(ev) { - // Support manual trigger via workflow_dispatch - if (process.env.PR_URL) { - return { - number: parseInt(process.env.PR_NUMBER, 10), - title: process.env.PR_TITLE || "PR", - html_url: process.env.PR_URL, - user: { login: process.env.PR_AUTHOR || "unknown" }, - }; - } - - const pr = ev.pull_request; - if (!pr) return null; - - return { - number: pr.number, - title: pr.title, - html_url: pr.html_url, - user: { login: pr.user.login }, - }; -} - (async () => { const ev = getEvent(); - const pr = getPR(ev); + const pr = ev.pull_request; if (!pr) { - console.log("No PR in event; exiting."); + console.log("No pull_request in event; exiting."); return; } - // Skip draft PRs - if (ev.pull_request?.draft) { + if (pr.draft) { console.log(`PR #${pr.number} is a draft; skipping.`); return; } From 3f76c75558dab3c80a7442f759c54d21e58b6497 Mon Sep 17 00:00:00 2001 From: Tomas Srnka Date: Mon, 13 Apr 2026 12:48:28 +0200 Subject: [PATCH 3/5] feat: include repo name in Slack message Shows which repo the PR is from (e.g. e2b-dev/infra#123) so the channel is useful once multiple repos use this workflow. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/pr-slack-notify/script.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/actions/pr-slack-notify/script.js b/.github/actions/pr-slack-notify/script.js index 6cb332e..ea0bd02 100644 --- a/.github/actions/pr-slack-notify/script.js +++ b/.github/actions/pr-slack-notify/script.js @@ -38,7 +38,8 @@ function getEvent() { .replace(//g, ">"); - const message = ` — new PR needs review\n*<${pr.html_url}|#${pr.number} — ${safeTitle}>* by ${pr.user.login}`; + const repo = process.env.GITHUB_REPOSITORY || "unknown"; + const message = ` — new PR needs review\n*<${pr.html_url}|${repo}#${pr.number} — ${safeTitle}>* by ${pr.user.login}`; await slack.chat.postMessage({ channel: channelId, From 817b6ca2d3e680bc0eff2fbe6c5a367294be2a9a Mon Sep 17 00:00:00 2001 From: Tomas Srnka Date: Mon, 13 Apr 2026 12:56:53 +0200 Subject: [PATCH 4/5] fix: add workflow_dispatch support, escape pipe in titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add PR_NUMBER/PR_TITLE/PR_URL/PR_AUTHOR optional inputs to the reusable workflow so callers can add workflow_dispatch for testing - Escape pipe (|) character in PR titles — it breaks Slack's link syntax () when used literally inside angle brackets - Extract escaping into escapeSlackLabel() function Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/pr-slack-notify/script.js | 37 +++++++++++++++++------ .github/workflows/pr-slack-notify.yml | 20 ++++++++++++ 2 files changed, 48 insertions(+), 9 deletions(-) diff --git a/.github/actions/pr-slack-notify/script.js b/.github/actions/pr-slack-notify/script.js index ea0bd02..39b3564 100644 --- a/.github/actions/pr-slack-notify/script.js +++ b/.github/actions/pr-slack-notify/script.js @@ -16,9 +16,35 @@ function getEvent() { return JSON.parse(fs.readFileSync(process.env.GITHUB_EVENT_PATH, "utf8")); } +// Escape characters that have special meaning in Slack mrkdwn link labels. +// Prevents injection via PR titles (e.g. or pipe breaking links). +function escapeSlackLabel(text) { + return text + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/\|/g, "|"); +} + +function getPR(ev) { + // Support manual trigger via workflow_dispatch. + // The caller workflow passes PR details as env vars. + if (process.env.PR_NUMBER) { + return { + number: parseInt(process.env.PR_NUMBER, 10), + title: process.env.PR_TITLE || "PR", + html_url: process.env.PR_URL || "", + user: { login: process.env.PR_AUTHOR || "unknown" }, + draft: false, + }; + } + + return ev.pull_request || null; +} + (async () => { const ev = getEvent(); - const pr = ev.pull_request; + const pr = getPR(ev); if (!pr) { console.log("No pull_request in event; exiting."); @@ -30,14 +56,7 @@ function getEvent() { return; } - // Escape Slack mrkdwn control characters to prevent injection via PR title. - // A fork contributor could craft a title with > or to break - // formatting or trigger channel-wide pings. - const safeTitle = pr.title - .replace(/&/g, "&") - .replace(//g, ">"); - + const safeTitle = escapeSlackLabel(pr.title); const repo = process.env.GITHUB_REPOSITORY || "unknown"; const message = ` — new PR needs review\n*<${pr.html_url}|${repo}#${pr.number} — ${safeTitle}>* by ${pr.user.login}`; diff --git a/.github/workflows/pr-slack-notify.yml b/.github/workflows/pr-slack-notify.yml index c718b68..57cf5cb 100644 --- a/.github/workflows/pr-slack-notify.yml +++ b/.github/workflows/pr-slack-notify.yml @@ -11,6 +11,22 @@ on: description: 'Slack user group ID to tag (e.g. @pr-infra)' required: true type: string + pr_number: + description: 'PR number (for manual trigger only)' + required: false + type: string + pr_title: + description: 'PR title (for manual trigger only)' + required: false + type: string + pr_url: + description: 'PR URL (for manual trigger only)' + required: false + type: string + pr_author: + description: 'PR author (for manual trigger only)' + required: false + type: string secrets: SLACK_BOT_TOKEN: description: 'Slack bot token' @@ -43,4 +59,8 @@ jobs: SLACK_BOT_TOKEN: ${{ secrets.SLACK_BOT_TOKEN }} SLACK_CHANNEL_ID: ${{ inputs.channel_id }} SLACK_REVIEW_GROUP: ${{ inputs.review_group }} + PR_NUMBER: ${{ inputs.pr_number }} + PR_TITLE: ${{ inputs.pr_title }} + PR_URL: ${{ inputs.pr_url }} + PR_AUTHOR: ${{ inputs.pr_author }} run: node .github/actions/pr-slack-notify/script.js From 30f4a6eeaabeea608aea3eec6534f1dcd9dae05b Mon Sep 17 00:00:00 2001 From: Tomas Srnka Date: Mon, 13 Apr 2026 13:15:10 +0200 Subject: [PATCH 5/5] fix: remove unnecessary pipe escaping from Slack label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slack only decodes & < > — | renders literally. Pipe in the label portion of doesn't break the link since Slack splits on the first | only. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/pr-slack-notify/script.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/actions/pr-slack-notify/script.js b/.github/actions/pr-slack-notify/script.js index 39b3564..e50c72a 100644 --- a/.github/actions/pr-slack-notify/script.js +++ b/.github/actions/pr-slack-notify/script.js @@ -17,13 +17,14 @@ function getEvent() { } // Escape characters that have special meaning in Slack mrkdwn link labels. -// Prevents injection via PR titles (e.g. or pipe breaking links). +// Prevents injection via PR titles (e.g. ). +// Slack only decodes & < > — no other HTML entities. +// Pipe (|) doesn't need escaping: Slack splits on the first | in . function escapeSlackLabel(text) { return text .replace(/&/g, "&") .replace(//g, ">") - .replace(/\|/g, "|"); + .replace(/>/g, ">"); } function getPR(ev) {