Conversation
- Add GitHub App authentication module (lib/github-app.js) - Support JWT generation and installation token caching - Update featurerequest to use GitHub App with fallback to personal token - Add better error messages for 401 (invalid token) errors - Fix duplicate workflow runs by adding concurrency group - Add config options: githubAppId, githubAppPrivateKey, githubAppInstallationId GitHub App credentials are hardcoded in github-app.js: - App ID: 2741767 - Installation ID: 106479987 - Private key: keys/github-app-private-key.pem
…skipped jobs - Remove preprocess from needs to prevent skipping when preprocess fails/is skipped - Always fetch issue data directly in generate-implementation - Ensures implementation plan is generated even if preprocess has issues
There was a problem hiding this comment.
Pull request overview
This PR primarily updates the feature-request enhancement workflow so that the generate-implementation job no longer depends on the preprocess job, and introduces GitHub App–based authentication as an option for creating feature-request issues from Slack.
Changes:
- Adjusts
.github/workflows/feature-request-enhance.ymlsogenerate-implementationonly depends oncheck-label, fetches issue data directly from the GitHub API, and adds a concurrency group per issue. - Adds a new
lib/github-app.jshelper and integrates it into the_featurerequestcommand inindex.jsto prefer GitHub App authentication with a personal access token as fallback, plus richer error handling and newsetconfigkeys. - Updates
.gitignoreto exclude akeys/directory intended to hold the GitHub App private key.
Reviewed changes
Copilot reviewed 3 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
lib/github-app.js |
New module that generates GitHub App JWTs, requests installation access tokens, and exposes a helper used to obtain an app-scoped GitHub token. |
index.js |
Imports the new GitHub App helper, updates the featurerequest command to try app auth before falling back to a PAT, enhances error messaging, and extends setconfig with GitHub App–related keys. |
.github/workflows/feature-request-enhance.yml |
Decouples generate-implementation from preprocess, adds per-issue concurrency control, and fetches the issue title directly to drive the implementation generation job. |
.gitignore |
Ignores the keys/ directory so GitHub App private keys stored there are not committed. |
Comments suppressed due to low confidence (1)
.github/workflows/feature-request-enhance.yml:16
- The
concurrencyblock has been inserted betweenworkflow_dispatch:and itsinputs:definition, withinputsnow indented underconcurrencyinstead ofon.workflow_dispatch. This breaks theworkflow_dispatchinput schema (manual runs will no longer prompt forissue_number), andconcurrency.inputsis not a recognized key. Please move the entireconcurrencyblock outside theon:section (as a top-level sibling) and keepinputsnested directly underworkflow_dispatch.
on:
issues:
types: [opened, labeled]
workflow_dispatch:
# Prevent parallel runs for the same issue
concurrency:
group: enhance-feature-request-${{ github.event.issue.number || inputs.issue_number }}
cancel-in-progress: false
inputs:
issue_number:
description: 'Issue number to process'
required: true
type: string
| // 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); |
There was a problem hiding this comment.
The GitHub App auth module currently uses hardcoded GITHUB_APP_ID, GITHUB_APP_INSTALLATION_ID, and a fixed private key path, and getGitHubAppToken() never reads the new githubAppId, githubAppPrivateKey, or githubAppInstallationId configuration keys you introduced in index.js. This means admins cannot actually reconfigure the app via setconfig as your feature request flow suggests. Please wire these values through nconf (e.g., read from config with sensible defaults) and, if you want to support a configurable private key path or inline key content, update getGitHubAppToken() accordingly instead of relying solely on keys/github-app-private-key.pem.
| '4. Configure via admin commands:\n' + | ||
| ' `setconfig githubAppId 2741767`\n' + | ||
| ' `setconfig githubAppPrivateKey /path/to/private-key.pem`\n' + | ||
| ' `setconfig githubAppInstallationId 106479987`\n\n' + |
There was a problem hiding this comment.
The GitHub App configuration instructions shown to users here (setconfig githubAppId ..., githubAppPrivateKey ..., githubAppInstallationId ...) do not match the current implementation in lib/github-app.js, which ignores these config keys and instead uses hardcoded IDs and a fixed private key path. As written, following these steps will not actually make the GitHub App authentication work. Either update the auth module to consume these config values or adjust the help text to reflect the real configuration mechanism.
| '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' + |
| 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 } |
There was a problem hiding this comment.
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 } |
| -H "Accept: application/vnd.github+json" \ | ||
| "https://api.github.com/repos/${{ github.repository }}/issues/$ISSUE_NUMBER") | ||
| TITLE=$(echo "$ISSUE_JSON" | jq -r '.title') | ||
| BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""') |
There was a problem hiding this comment.
In the Fetch issue data step, you parse the issue body into the BODY variable but never write it to $GITHUB_OUTPUT or 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.
| BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""') |
Fix: Generate Implementation Job Skipping
This PR fixes an issue where the
generate-implementationjob was being skipped when thepreprocessjob was skipped or failed.Changes
preprocessfromneedsdependency ingenerate-implementationjobcheck-labelto determine if it should runpreprocessoutputspreprocesshas issuesWhy this was needed
When
preprocessjob is skipped (due to conditions) or fails, GitHub Actions sets the outcome toskippedorfailure, notsuccess. The conditionneeds.preprocess.outcome == 'success'would then preventgenerate-implementationfrom running, even though the issue has the correct label and isn't already enhanced.Trade-off
preprocessFixes the issue where
generate-implementationwas skipped for issue #27.