Skip to content

Fix: Make generate-implementation independent of preprocess#234

Merged
htilly merged 2 commits intomasterfrom
develop
Jan 27, 2026
Merged

Fix: Make generate-implementation independent of preprocess#234
htilly merged 2 commits intomasterfrom
develop

Conversation

@htilly
Copy link
Owner

@htilly htilly commented Jan 27, 2026

Fix: Generate Implementation Job Skipping

This PR fixes an issue where the generate-implementation job was being skipped when the preprocess job was skipped or failed.

Changes

  • Removed preprocess from needs dependency in generate-implementation job
  • Job now only depends on check-label to determine if it should run
  • Always fetches issue data directly from GitHub API instead of relying on preprocess outputs
  • Ensures implementation plan is generated even if preprocess has issues

Why this was needed

When preprocess job is skipped (due to conditions) or fails, GitHub Actions sets the outcome to skipped or failure, not success. The condition needs.preprocess.outcome == 'success' would then prevent generate-implementation from running, even though the issue has the correct label and isn't already enhanced.

Trade-off

  • Uses issue title directly instead of the enhanced task from preprocess
  • But ensures the implementation plan is always generated when appropriate

Fixes the issue where generate-implementation was skipped for issue #27.

- 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
Copilot AI review requested due to automatic review settings January 27, 2026 21:26
@htilly htilly merged commit f23782d into master Jan 27, 2026
15 checks passed
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.yml so generate-implementation only depends on check-label, fetches issue data directly from the GitHub API, and adds a concurrency group per issue.
  • Adds a new lib/github-app.js helper and integrates it into the _featurerequest command in index.js to prefer GitHub App authentication with a personal access token as fallback, plus richer error handling and new setconfig keys.
  • Updates .gitignore to exclude a keys/ 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 concurrency block has been inserted between workflow_dispatch: and its inputs: definition, with inputs now indented under concurrency instead of on.workflow_dispatch. This breaks the workflow_dispatch input schema (manual runs will no longer prompt for issue_number), and concurrency.inputs is not a recognized key. Please move the entire concurrency block outside the on: section (as a top-level sibling) and keep inputs nested directly under workflow_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

Comment on lines +10 to +117
// 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);
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
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' +
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
'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 uses AI. Check for mistakes.
Comment on lines +5004 to +5007
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 }
Copy link

Copilot AI Jan 27, 2026

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.

Suggested change
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 }

Copilot uses AI. Check for mistakes.
-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 // ""')
Copy link

Copilot AI Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
BODY=$(echo "$ISSUE_JSON" | jq -r '.body // ""')

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants