-
Notifications
You must be signed in to change notification settings - Fork 37
Fix: Make generate-implementation independent of preprocess #234
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -47,6 +47,7 @@ const voting = require('./lib/voting'); | |||||||||||||
| const musicHelper = require('./lib/music-helper'); | ||||||||||||||
| const commandHandlers = require('./lib/command-handlers'); | ||||||||||||||
| const addHandlers = require('./lib/add-handlers'); | ||||||||||||||
| const githubApp = require('./lib/github-app'); | ||||||||||||||
| const gongMessage = fs.readFileSync('templates/messages/gong.txt', 'utf8').split('\n').filter(Boolean); | ||||||||||||||
| const voteMessage = fs.readFileSync('templates/messages/vote.txt', 'utf8').split('\n').filter(Boolean); | ||||||||||||||
| const ttsMessage = fs.readFileSync('templates/messages/tts.txt', 'utf8').split('\n').filter(Boolean); | ||||||||||||||
|
|
@@ -4744,31 +4745,57 @@ async function _featurerequest(input, channel, userName) { | |||||||||||||
|
|
||||||||||||||
| const featureDescription = input.slice(1).join(' '); | ||||||||||||||
|
|
||||||||||||||
| // Check if githubToken is configured | ||||||||||||||
| const githubToken = config.get('githubToken'); | ||||||||||||||
| if (!githubToken) { | ||||||||||||||
| logger.warn('[FEATUREREQUEST] githubToken not configured'); | ||||||||||||||
| _slackMessage( | ||||||||||||||
| '❌ *Feature request not configured*\n\n' + | ||||||||||||||
| 'To enable this feature, you need a GitHub Personal Access Token:\n\n' + | ||||||||||||||
| '1. Go to: https://github.com/settings/tokens\n' + | ||||||||||||||
| '2. Click *"Generate new token (classic)"*\n' + | ||||||||||||||
| '3. Select scope: `repo` (or `public_repo` for public repos only)\n' + | ||||||||||||||
| '4. Set the token via admin command:\n' + | ||||||||||||||
| ' `setconfig githubToken ghp_xxxxxxxxxxxx`\n\n' + | ||||||||||||||
| '📖 More info: https://github.com/htilly/SlackONOS#configuration', | ||||||||||||||
| channel | ||||||||||||||
| ); | ||||||||||||||
| return; | ||||||||||||||
| // Try GitHub App first, fallback to personal access token | ||||||||||||||
| let authToken = null; | ||||||||||||||
| let authMethod = null; | ||||||||||||||
|
|
||||||||||||||
| try { | ||||||||||||||
| const appToken = await githubApp.getGitHubAppToken(); | ||||||||||||||
| if (appToken) { | ||||||||||||||
| authToken = appToken; | ||||||||||||||
| authMethod = 'GitHub App'; | ||||||||||||||
| logger.info('[FEATUREREQUEST] Using GitHub App authentication'); | ||||||||||||||
| } | ||||||||||||||
| } catch (error) { | ||||||||||||||
| logger.warn(`[FEATUREREQUEST] GitHub App auth failed: ${error.message}, falling back to personal token`); | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| // Fallback to personal access token | ||||||||||||||
| if (!authToken) { | ||||||||||||||
| const githubToken = config.get('githubToken'); | ||||||||||||||
| if (!githubToken) { | ||||||||||||||
| logger.warn('[FEATUREREQUEST] No GitHub authentication configured'); | ||||||||||||||
| _slackMessage( | ||||||||||||||
| '❌ *Feature request not configured*\n\n' + | ||||||||||||||
| 'To enable this feature, configure either:\n\n' + | ||||||||||||||
| '*Option 1: GitHub App (Recommended)*\n' + | ||||||||||||||
| '1. Create GitHub App: https://github.com/settings/apps/new\n' + | ||||||||||||||
| '2. Set permissions: Issues: Write\n' + | ||||||||||||||
| '3. Install on repository\n' + | ||||||||||||||
| '4. Configure via admin commands:\n' + | ||||||||||||||
| ' `setconfig githubAppId 2741767`\n' + | ||||||||||||||
| ' `setconfig githubAppPrivateKey /path/to/private-key.pem`\n' + | ||||||||||||||
| ' `setconfig githubAppInstallationId 106479987`\n\n' + | ||||||||||||||
|
Comment on lines
+4775
to
+4778
|
||||||||||||||
| '4. Configure via admin commands:\n' + | |
| ' `setconfig githubAppId 2741767`\n' + | |
| ' `setconfig githubAppPrivateKey /path/to/private-key.pem`\n' + | |
| ' `setconfig githubAppInstallationId 106479987`\n\n' + | |
| '4. Configure the GitHub App credentials on the server (see `lib/github-app.js`).\n' + | |
| ' Note: GitHub App settings are not currently managed via `setconfig`.\n\n' + |
Copilot
AI
Jan 27, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The new githubAppId, githubAppPrivateKey, and githubAppInstallationId keys added to allowedConfigs are never read anywhere in the codebase (the GitHub App auth module uses hardcoded values instead). This makes setconfig appear to support these options without any effect at runtime. To avoid confusing admins, either hook these config values into lib/github-app.js (and any other relevant places) or remove them until the wiring is complete.
| githubToken: { type: 'string', minLen: 4, maxLen: 100, sensitive: true }, | |
| githubAppId: { type: 'string', minLen: 1, maxLen: 20 }, | |
| githubAppPrivateKey: { type: 'string', minLen: 50, maxLen: 5000, sensitive: true }, | |
| githubAppInstallationId: { type: 'string', minLen: 1, maxLen: 20 } | |
| githubToken: { type: 'string', minLen: 4, maxLen: 100, sensitive: true } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,126 @@ | ||
| /** | ||
| * GitHub App Authentication | ||
| * Handles JWT generation and installation token management | ||
| */ | ||
|
|
||
| const crypto = require('crypto'); | ||
| const fs = require('fs'); | ||
| const path = require('path'); | ||
|
|
||
| // Hardcoded GitHub App credentials | ||
| const GITHUB_APP_ID = '2741767'; | ||
| const GITHUB_APP_INSTALLATION_ID = '106479987'; | ||
| const GITHUB_APP_PRIVATE_KEY_PATH = path.join(__dirname, '..', 'keys', 'github-app-private-key.pem'); | ||
|
|
||
| let cachedToken = null; | ||
| let tokenExpiresAt = null; | ||
|
|
||
| /** | ||
| * Generate JWT for GitHub App authentication | ||
| * @param {string} appId - GitHub App ID | ||
| * @param {string} privateKey - Private key content (PEM format) | ||
| * @returns {string} JWT token | ||
| */ | ||
| function generateJWT(appId, privateKey) { | ||
| const now = Math.floor(Date.now() / 1000); | ||
| const payload = { | ||
| iat: now - 60, // Issued at: 60 seconds in the past | ||
| exp: now + 600, // Expires: 10 minutes in the future | ||
| iss: appId // Issuer: App ID | ||
| }; | ||
|
|
||
| const header = { | ||
| alg: 'RS256', | ||
| typ: 'JWT' | ||
| }; | ||
|
|
||
| // Encode header and payload (base64url = base64 without padding, with URL-safe chars) | ||
| const base64url = (str) => { | ||
| return Buffer.from(str) | ||
| .toString('base64') | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| .replace(/=/g, ''); | ||
| }; | ||
|
|
||
| const encodedHeader = base64url(JSON.stringify(header)); | ||
| const encodedPayload = base64url(JSON.stringify(payload)); | ||
| const signatureInput = `${encodedHeader}.${encodedPayload}`; | ||
|
|
||
| // Sign with private key | ||
| const sign = crypto.createSign('RSA-SHA256'); | ||
| sign.update(signatureInput); | ||
| sign.end(); | ||
| const signature = sign.sign(privateKey, 'base64') | ||
| .replace(/\+/g, '-') | ||
| .replace(/\//g, '_') | ||
| .replace(/=/g, ''); | ||
|
|
||
| return `${encodedHeader}.${encodedPayload}.${signature}`; | ||
| } | ||
|
|
||
| /** | ||
| * Get installation access token (cached for 1 hour) | ||
| * @param {string} appId - GitHub App ID | ||
| * @param {string} privateKey - Private key content | ||
| * @param {string} installationId - Installation ID | ||
| * @returns {Promise<string>} Installation access token | ||
| */ | ||
| async function getInstallationToken(appId, privateKey, installationId) { | ||
| // Return cached token if still valid (with 5 minute buffer) | ||
| if (cachedToken && tokenExpiresAt && Date.now() < tokenExpiresAt - 5 * 60 * 1000) { | ||
| return cachedToken; | ||
| } | ||
|
|
||
| try { | ||
| // Generate JWT | ||
| const jwt = generateJWT(appId, privateKey); | ||
|
|
||
| // Request installation token | ||
| const response = await fetch(`https://api.github.com/app/installations/${installationId}/access_tokens`, { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Authorization': `Bearer ${jwt}`, | ||
| 'Accept': 'application/vnd.github+json', | ||
| 'X-GitHub-Api-Version': '2022-11-28' | ||
| } | ||
| }); | ||
|
|
||
| if (!response.ok) { | ||
| const errorText = await response.text(); | ||
| throw new Error(`Failed to get installation token: ${response.status} - ${errorText}`); | ||
| } | ||
|
|
||
| const data = await response.json(); | ||
| cachedToken = data.token; | ||
| // Tokens expire after 1 hour, cache for 55 minutes to be safe | ||
| tokenExpiresAt = Date.now() + 55 * 60 * 1000; | ||
|
|
||
| return cachedToken; | ||
| } catch (error) { | ||
| throw new Error(`GitHub App authentication failed: ${error.message}`); | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get GitHub App access token (either from cache or by generating new one) | ||
| * @returns {Promise<string|null>} Access token or null if not configured | ||
| */ | ||
| async function getGitHubAppToken() { | ||
| // Check if private key file exists | ||
| if (!fs.existsSync(GITHUB_APP_PRIVATE_KEY_PATH)) { | ||
| return null; | ||
| } | ||
|
|
||
| try { | ||
| const privateKey = fs.readFileSync(GITHUB_APP_PRIVATE_KEY_PATH, 'utf8'); | ||
| return await getInstallationToken(GITHUB_APP_ID, privateKey, GITHUB_APP_INSTALLATION_ID); | ||
|
Comment on lines
+10
to
+117
|
||
| } catch (error) { | ||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| module.exports = { | ||
| getGitHubAppToken, | ||
| generateJWT // Exported for testing | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the
Fetch issue datastep, you parse the issue body into theBODYvariable but never write it to$GITHUB_OUTPUTor otherwise use it. If the body is not needed by later steps, consider removing this parsing to simplify the script; if it is intended as context for the implementation generator, expose it as an output or environment variable and consume it in.github/agent/generate-implementation.mjs.